diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..47d31c9
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,18 @@
+node_modules
+npm-debug.log
+dist
+.git
+.gitignore
+README.md
+.env
+.env.local
+.env.*.local
+.vscode
+.idea
+*.log
+.DS_Store
+coverage
+.nyc_output
+Claude.md
+.mcp.json
+.claude
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index 08bd47e..8518efd 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -13,10 +13,17 @@ jobs:
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- - name: Make target export directory
- run: mkdir -p public/
+ - name: Setup Node.js
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
+ with:
+ node-version: '20'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
- ## TODO: Add site build and export steps here, target public/ for export directory
+ - name: Build site
+ run: npm run build
- name: Copy CNAME
run: cp CNAME public
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c4fbe77
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,110 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# Dependencies
+node_modules
+.pnp
+.pnp.js
+
+# Build outputs
+dist
+dist-ssr
+build
+*.local
+
+# Environment variables
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Testing
+coverage
+.nyc_output
+*.lcov
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+.idea
+*.swp
+*.swo
+*~
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# OS files
+Thumbs.db
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Desktop.ini
+
+# Temporary files
+*.tmp
+*.temp
+.cache
+.parcel-cache
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env*.local
+
+# Vite
+.vite
+
+# TypeScript
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Storybook build outputs
+storybook-static
+
+# Temporary folders
+tmp/
+temp/
diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 0000000..e105e8a
--- /dev/null
+++ b/.mcp.json
@@ -0,0 +1,16 @@
+{
+ "mcpServers": {
+ "context7": {
+ "command": "npx",
+ "args": ["-y", "@upstash/context7-mcp"]
+ },
+ "playwright": {
+ "type": "stdio",
+ "command": "npx",
+ "args": [
+ "@playwright/mcp@latest"
+ ],
+ "env": {}
+ }
+ }
+ }
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..494e4b7
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,9 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=827846
+ // for the documentation about the extensions.json format
+ "recommendations": [
+ "Anthropic.claude-code",
+ "github.vscode-pull-request-github",
+ "RooVeterinaryInc.roo-cline"
+ ]
+}
diff --git a/Claude.md b/Claude.md
new file mode 100644
index 0000000..68a8420
--- /dev/null
+++ b/Claude.md
@@ -0,0 +1,57 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Overview
+
+This repository contains a single-page React application for creating devfiles through a user-friendly GUI wizard. The application uses React and Tailwind CSS to provide a step-by-step interface for generating devfile 2.3.0 compliant YAML files.
+
+**Key Resources:**
+- Devfile specification: https://devfile.io/docs/
+- Feature request: https://github.com/devfile/api/issues/1765
+- Devfile 2.3.0 spec should be the target version
+
+## Project Architecture
+
+**Application Type:** Single-page application (SPA) with no backend or persistent state
+**UI Framework:** React
+**Styling:** Tailwind CSS
+**Output:** Generated devfile.yaml files available for download
+
+The application is stateless - all devfile generation happens client-side with no data persistence.
+
+## Development Commands
+
+This section will be populated once the project structure is initialized. Expected commands include:
+- `npm install` - Install dependencies
+- `npm run dev` - Start development server
+- `npm run build` - Build for production
+- `npm run lint` - Run linter
+- `npm run test` - Run tests (if applicable)
+
+## MCP Servers
+
+This project uses the following MCP servers (configured in `.mcp.json`):
+
+- **context7**: Provides up-to-date React and Tailwind CSS documentation
+- **playwright**: For browser automation and testing
+
+## Key Implementation Requirements
+
+1. **Step-by-step wizard interface** - Guide users through devfile creation with clear, sequential steps
+2. **Devfile 2.3.0 compliance** - Generated YAML must conform to the devfile 2.3.0 specification
+3. **Download capability** - Users must be able to download the generated devfile.yaml
+4. **Single page design** - All functionality contained in one web page with no routing
+5. **No state persistence** - Application does not save or persist user data between sessions
+
+## Devfile Concepts to Understand
+
+When working with this codebase, familiarize yourself with these devfile concepts:
+- Components (containers, volumes, kubernetes resources)
+- Commands (exec, apply, composite)
+- Events (preStart, postStart, preStop, postStop)
+- Projects (git repositories)
+- StarterProjects (templates)
+- Metadata (name, version, description, attributes)
+
+Refer to https://devfile.io/docs/ for detailed schema and examples.
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..10b1166
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,50 @@
+# Multi-stage build for production
+
+# Stage 1: Build the application
+FROM node:18-alpine AS builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci
+
+# Copy source code
+COPY . .
+
+# Build the application
+RUN npm run build
+
+# Stage 2: Serve with nginx
+FROM nginx:alpine
+
+# Copy built assets from builder stage
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+# Copy nginx configuration
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+# Create nginx cache directories with world-writable permissions for OpenShift
+# OpenShift will assign a random UID, so we make directories writable by any user
+RUN mkdir -p /var/cache/nginx/client_temp \
+ && mkdir -p /var/cache/nginx/proxy_temp \
+ && mkdir -p /var/cache/nginx/fastcgi_temp \
+ && mkdir -p /var/cache/nginx/uwsgi_temp \
+ && mkdir -p /var/cache/nginx/scgi_temp \
+ && chmod -R 777 /var/cache/nginx \
+ && chmod -R 777 /var/log/nginx \
+ && chmod -R 755 /etc/nginx/conf.d \
+ && chmod -R 755 /usr/share/nginx/html \
+ && touch /var/run/nginx.pid \
+ && chmod 777 /var/run/nginx.pid
+
+# Don't set USER - let OpenShift assign the UID
+# OpenShift will enforce runAsNonRoot via security context
+
+# Expose port 8080 (non-root)
+EXPOSE 8080
+
+# Start nginx
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/Dockerfile.dev b/Dockerfile.dev
new file mode 100644
index 0000000..72ef604
--- /dev/null
+++ b/Dockerfile.dev
@@ -0,0 +1,19 @@
+# Development Dockerfile
+FROM node:18-alpine
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci
+
+# Copy source code
+COPY . .
+
+# Expose Vite dev server port
+EXPOSE 5173
+
+# Start development server
+CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4e24668
--- /dev/null
+++ b/README.md
@@ -0,0 +1,302 @@
+[](https://workspaces.openshift.com#https://github.com/ibuziuk/devfile-gui-wizard)
+[&logo=eclipseche&color=FDB940&labelColor=525C86)](https://che-dogfooding.apps.che-dev.x6e0.p1.openshiftapps.com#https://github.com/ibuziuk/devfile-gui-wizard)
+
+# Devfile GUI Wizard
+
+A user-friendly, modern web application for creating [devfile 2.3.0](https://devfile.io/) configuration files through an interactive step-by-step wizard interface.
+
+
+
+
+
+
+## Overview
+
+The Devfile GUI Wizard simplifies the process of creating devfile YAML configurations by providing an intuitive, guided experience. No need to memorize the devfile schema or YAML syntaxβjust fill out the forms and download your ready-to-use `devfile.yaml`.
+
+## Features
+
+- β¨ **7-Step Wizard Interface** - Guided workflow through all devfile sections
+- π― **Real-time YAML Preview** - See your devfile generated as you type
+- β
**Built-in Validation** - Ensures devfile 2.3.0 compliance
+- π₯ **One-Click Download** - Export your devfile.yaml instantly
+- π **Copy to Clipboard** - Quick copy functionality
+- π¨ **Responsive Design** - Works on desktop, tablet, and mobile
+- β©οΈ **Edit Any Step** - Navigate back to modify previous sections
+- βοΈ **Skip Optional Steps** - Streamlined workflow
+- π **Reset Wizard** - Start fresh at any time
+- π **No Backend Required** - Fully client-side application
+
+## Wizard Steps
+
+1. **Basic Information** - Project metadata (name, version, description, language)
+2. **Projects** - Source code repositories (Git or Zip)
+3. **Components** - Development environment containers, volumes, resources
+4. **Commands** - Build, run, test, and debug commands
+5. **Events** - Lifecycle event bindings (preStart, postStart, etc.)
+6. **Variables** - Key-value pairs for substitution
+7. **Review & Download** - Summary view with download functionality
+
+## Getting Started
+
+### Prerequisites
+
+- [Node.js](https://nodejs.org/) 18.x or higher
+- npm 9.x or higher
+
+### Installation
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/yourusername/devfile-gui-wizard.git
+ cd devfile-gui-wizard
+ ```
+
+2. Install dependencies:
+ ```bash
+ npm install
+ ```
+
+3. Start the development server:
+ ```bash
+ npm run dev
+ ```
+
+4. Open your browser and navigate to `http://localhost:5173`
+
+## Development Commands
+
+| Command | Description |
+|---------|-------------|
+| `npm install` | Install dependencies |
+| `npm run dev` | Start development server (with hot reload) |
+| `npm run build` | Build for production |
+| `npm run preview` | Preview production build locally |
+| `npm test` | Run end-to-end tests with Playwright |
+| `npm run test:ui` | Run tests in interactive UI mode |
+| `npm run test:debug` | Run tests in debug mode |
+
+## Testing
+
+The project includes end-to-end tests using [Playwright](https://playwright.dev/) to verify the wizard functionality and devfile generation.
+
+### Running Tests
+
+1. **First time setup** - Install Playwright browsers:
+ ```bash
+ npx playwright install
+ ```
+
+2. **Run tests**:
+ ```bash
+ npm test
+ ```
+
+3. **Interactive mode** (see tests run in real-time):
+ ```bash
+ npm run test:ui
+ ```
+
+### Test Coverage
+
+The test suite includes:
+- **Quarkus Devfile Generation** - Generates a simplified version of the [Quarkus API Example devfile](https://github.com/che-incubator/quarkus-api-example/blob/main/devfile.yaml)
+- **Schema Compliance** - Verifies generated devfiles comply with Devfile 2.3.0 specification
+
+See [`tests/README.md`](tests/README.md) for detailed testing documentation.
+
+## OpenShift Deployment
+
+The application can be deployed to OpenShift using Kustomize. The deployment manifests are located in the `openshift/` directory.
+
+### Prerequisites
+
+- OpenShift CLI (`oc`) or Kubernetes CLI (`kubectl`) installed
+- Access to an OpenShift cluster
+- Container image built and pushed to a registry (automatically built via GitHub Actions)
+
+### Quick Deploy
+
+1. **Set your namespace** (optional):
+ ```bash
+ oc project your-project-name
+ # or create a new project
+ oc new-project devfile-gui-wizard
+ ```
+
+2. **Deploy using Kustomize**:
+ ```bash
+ kubectl apply -k openshift
+ ```
+
+ Or with `oc`:
+ ```bash
+ oc apply -k openshift
+ ```
+
+3. **Get the application URL**:
+ ```bash
+ oc get route devfile-gui-wizard -o jsonpath='{.spec.host}'
+ ```
+
+### Verify Deployment
+
+```bash
+# Check deployment status
+kubectl get deployment devfile-gui-wizard
+
+# Check pods
+kubectl get pods -l app=devfile-gui-wizard
+
+# Check service
+kubectl get service devfile-gui-wizard
+
+# Check route (OpenShift)
+oc get route devfile-gui-wizard
+```
+
+### Customization
+
+The deployment uses the image `ghcr.io/ibuziuk/devfile-gui-wizard:latest` by default. To customize:
+
+1. **Update image tag**: Edit `openshift/kustomization.yaml`:
+ ```yaml
+ images:
+ - name: ghcr.io/ibuziuk/devfile-gui-wizard
+ newName: ghcr.io/ibuziuk/devfile-gui-wizard
+ newTag: v1.0.0 # or specific commit hash
+ ```
+
+2. **Adjust replica count**: Edit `openshift/kustomization.yaml`:
+ ```yaml
+ replicas:
+ - name: devfile-gui-wizard
+ count: 3
+ ```
+
+3. **Set namespace**: Edit `openshift/kustomization.yaml`:
+ ```yaml
+ namespace: my-project
+ ```
+
+### Preview Changes
+
+Before applying, preview what will be deployed:
+
+```bash
+kubectl kustomize openshift
+```
+
+### Cleanup
+
+To remove all resources:
+
+```bash
+kubectl delete -k openshift
+```
+
+## Technology Stack
+
+- **Frontend Framework:** React 18.3.1
+- **Styling:** Tailwind CSS 3.4.0 with @tailwindcss/forms plugin
+- **Build Tool:** Vite 5.4.0
+- **YAML Generation:** js-yaml 4.1.0
+- **State Management:** React useReducer + Context API
+- **Language:** JavaScript (ES6+)
+
+## Project Structure
+
+```
+devfile-gui-wizard/
+βββ src/
+β βββ components/
+β β βββ common/ # Reusable UI components (Button, Card, Alert)
+β β βββ forms/ # Form components (Input, Select, TextArea)
+β β βββ wizard/ # Wizard navigation and container
+β β βββ steps/ # Individual wizard step components
+β β βββ preview/ # YAML preview component
+β βββ hooks/ # Custom React hooks
+β β βββ useWizardState.jsx # State management
+β β βββ useYamlGenerator.jsx # YAML generation
+β βββ utils/ # Utility functions
+β β βββ devfileGenerator.js # Devfile cleaning/validation
+β β βββ validation.js # Form validation rules
+β β βββ downloadFile.js # File download utility
+β β βββ constants.js # App constants
+β βββ App.jsx # Root component
+β βββ main.jsx # Application entry point
+β βββ index.css # Global styles
+βββ public/ # Static assets
+βββ index.html # HTML entry point
+βββ package.json # Dependencies and scripts
+βββ vite.config.js # Vite configuration
+βββ tailwind.config.js # Tailwind configuration
+βββ postcss.config.js # PostCSS configuration
+βββ CLAUDE.md # Claude Code instructions
+βββ README.md # This file
+```
+
+## How It Works
+
+1. **State Management**: The application uses React's `useReducer` hook combined with Context API to manage the wizard state and devfile data.
+
+2. **Validation**: Each step includes field-level validation to ensure data integrity and devfile 2.3.0 compliance.
+
+3. **YAML Generation**: The `js-yaml` library converts the structured JavaScript object into properly formatted YAML, with custom cleaning to remove empty fields.
+
+4. **Download**: The browser's Blob API creates a downloadable file from the generated YAML content.
+
+## Devfile 2.3.0 Support
+
+This wizard supports all major devfile 2.3.0 features:
+
+- **Metadata**: name, version, displayName, description, language, provider, tags, website, supportUrl
+- **Projects**: Git and Zip sources with checkoutFrom options
+- **Components**:
+ - Container (with image, env vars, volume mounts, endpoints, resource limits)
+ - Volume (with size and ephemeral options)
+ - Kubernetes/OpenShift resources
+ - Image builds
+- **Commands**:
+ - Exec (command execution)
+ - Apply (resource application)
+ - Composite (command grouping)
+- **Events**: preStart, postStart, preStop, postStop
+- **Variables**: String substitution with `{{variable}}` syntax
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+1. Fork the repository
+2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
+3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
+4. Push to the branch (`git push origin feature/AmazingFeature`)
+5. Open a Pull Request
+
+## Resources
+
+- [Devfile Official Documentation](https://devfile.io/docs/)
+- [Devfile 2.3.0 Schema](https://devfile.io/docs/2.3.0/devfile-schema)
+- [GitHub Issue #1765 - Original Feature Request](https://github.com/devfile/api/issues/1765)
+- [React Documentation](https://react.dev/)
+- [Tailwind CSS Documentation](https://tailwindcss.com/)
+- [Vite Documentation](https://vitejs.dev/)
+
+## License
+
+This project is open source and available under the [Apache License Version 2.0](LICENSE).
+
+## Acknowledgments
+
+- Built with [Claude Code](https://claude.ai/code)
+- Devfile specification by the [Devfile Community](https://devfile.io/)
+- UI components styled with [Tailwind CSS](https://tailwindcss.com/)
+
+---
+
+## π Community Support
+
+For issues and questions use the main Devfile [issue tracker](https://github.com/devfile/api/issues)
+
+**Made with β€οΈ for the Devfile community**
diff --git a/SCHEMA_COVERAGE.md b/SCHEMA_COVERAGE.md
new file mode 100644
index 0000000..5cc22a6
--- /dev/null
+++ b/SCHEMA_COVERAGE.md
@@ -0,0 +1,80 @@
+# Devfile 2.3.0 Schema Coverage
+
+This document tracks what's implemented vs what's missing from the devfile 2.3.0 schema.
+
+## β
Currently Implemented
+
+### Metadata
+- β
name (required)
+- β
version
+- β
displayName
+- β
description
+- β
language
+- β
projectType
+- β
provider
+- β
tags
+- β
website
+- β
supportUrl
+
+### Projects
+- β
name (required)
+- β
clonePath
+- β
git.remotes
+- β
git.checkoutFrom.revision
+- β
git.checkoutFrom.remote
+- β
zip.location
+- β attributes
+
+### Components
+- β
container (basic)
+ - β
name (required)
+ - β
image (required)
+ - β
mountSources
+ - β
memoryLimit
+ - β
cpuLimit
+ - β env
+ - β volumeMounts
+ - β endpoints
+ - β sourceMapping
+ - β dedicatedPod
+ - β deployByDefault
+ - β args
+ - β commands
+- β
volume (basic)
+ - β
name (required)
+ - β
size
+ - β ephemeral
+- β kubernetes component type
+- β openshift component type
+- β image component type
+
+### Commands
+- β
exec (basic)
+ - β
id (required)
+ - β
component (required)
+ - β
commandLine (required)
+ - β
workingDir
+ - β env
+ - β group
+ - β label
+ - β hotReloadCapable
+- β apply command type
+- β composite command type
+
+### Events
+- β
preStart
+- β
postStart
+- β
preStop
+- β
postStop
+
+### Variables
+- β
Basic key-value pairs
+
+### Other
+- β starterProjects (not implemented)
+- β parent (not implemented)
+- β top-level attributes (in state but no UI)
+
+## π In Progress
+
+See TODO list for current implementation status.
diff --git a/devfile.yaml b/devfile.yaml
new file mode 100644
index 0000000..fb5b5b8
--- /dev/null
+++ b/devfile.yaml
@@ -0,0 +1,80 @@
+schemaVersion: 2.3.0
+metadata:
+ generateName: devfile-gui-wizard
+ version: 1.0.0
+ displayName: Devfile GUI Wizard
+ description: A React + Tailwind web application for creating devfile 2.3.0 YAML files
+ language: JavaScript
+ projectType: application
+ provider: Devfile Community
+ tags:
+ - devfile
+ - wizard
+ - react
+ - tailwind
+ - yaml-generator
+ website: https://devfile.io
+attributes:
+ controller.devfile.io/storage-type: ephemeral
+components:
+ - name: nodejs-dev
+ container:
+ image: quay.io/devfile/universal-developer-image:ubi10-latest
+ mountSources: true
+ sourceMapping: /projects
+ endpoints:
+ - name: dev-server
+ targetPort: 5173
+ protocol: http
+ exposure: public
+ env:
+ - name: NODE_ENV
+ value: development
+ memoryLimit: 2Gi
+ cpuLimit: 1000m
+ - name: node-modules
+ volume:
+ size: 1Gi
+commands:
+ - id: install-dependencies
+ exec:
+ component: nodejs-dev
+ commandLine: npm install
+ workingDir: ${PROJECT_SOURCE}
+ group:
+ kind: build
+ - id: start-dev-server
+ exec:
+ component: nodejs-dev
+ commandLine: npm run dev
+ workingDir: ${PROJECT_SOURCE}
+ group:
+ kind: run
+ isDefault: true
+ - id: build-production
+ exec:
+ component: nodejs-dev
+ commandLine: npm run build
+ workingDir: ${PROJECT_SOURCE}
+ group:
+ kind: build
+ - id: preview-build
+ exec:
+ component: nodejs-dev
+ commandLine: npm run preview
+ workingDir: ${PROJECT_SOURCE}
+ group:
+ kind: run
+ - id: deploy
+ exec:
+ component: nodejs-dev
+ commandLine: kubectl apply -k openshift
+ workingDir: ${PROJECT_SOURCE}
+ - id: cleanup
+ exec:
+ component: nodejs-dev
+ commandLine: kubectl delete -k openshift
+ workingDir: ${PROJECT_SOURCE}
+events:
+ postStart:
+ - install-dependencies
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..13a79e0
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,28 @@
+version: '3.8'
+
+services:
+ # Production build
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "8080:80"
+ container_name: devfile-gui-wizard
+ restart: unless-stopped
+
+ # Development server (optional)
+ dev:
+ build:
+ context: .
+ dockerfile: Dockerfile.dev
+ ports:
+ - "5173:5173"
+ volumes:
+ - .:/app
+ - /app/node_modules
+ container_name: devfile-gui-wizard-dev
+ environment:
+ - NODE_ENV=development
+ profiles:
+ - dev
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6b6549b
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Devfile Wizard - Create Devfile 2.3.0 YAML Files
+
+
+
+
+
+
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..4eaea9e
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,34 @@
+server {
+ listen 8080;
+ server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Gzip compression
+ gzip on;
+ gzip_vary on;
+ gzip_min_length 1024;
+ gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+
+ # Handle client-side routing - serve index.html for all routes
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Cache static assets
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # Don't cache HTML files
+ location ~* \.html$ {
+ expires -1;
+ add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
+ }
+}
diff --git a/openshift/deployment.yaml b/openshift/deployment.yaml
new file mode 100644
index 0000000..5654fc2
--- /dev/null
+++ b/openshift/deployment.yaml
@@ -0,0 +1,57 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: devfile-gui-wizard
+ labels:
+ app: devfile-gui-wizard
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: devfile-gui-wizard
+ template:
+ metadata:
+ labels:
+ app: devfile-gui-wizard
+ spec:
+ securityContext:
+ runAsNonRoot: true
+ containers:
+ - name: devfile-gui-wizard
+ image: ghcr.io/ibuziuk/devfile-gui-wizard:latest
+ imagePullPolicy: Always
+ securityContext:
+ allowPrivilegeEscalation: false
+ runAsNonRoot: true
+ capabilities:
+ drop:
+ - ALL
+ ports:
+ - containerPort: 8080
+ protocol: TCP
+ resources:
+ requests:
+ memory: "64Mi"
+ cpu: "100m"
+ limits:
+ memory: "256Mi"
+ cpu: "500m"
+ livenessProbe:
+ httpGet:
+ path: /
+ port: 8080
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 3
+ readinessProbe:
+ httpGet:
+ path: /
+ port: 8080
+ initialDelaySeconds: 10
+ periodSeconds: 5
+ timeoutSeconds: 3
+ failureThreshold: 3
+ env:
+ - name: NODE_ENV
+ value: "production"
diff --git a/openshift/kustomization.yaml b/openshift/kustomization.yaml
new file mode 100644
index 0000000..b5f38de
--- /dev/null
+++ b/openshift/kustomization.yaml
@@ -0,0 +1,27 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+namespace: devfile-gui-wizard
+
+resources:
+ - deployment.yaml
+ - service.yaml
+ - route.yaml
+
+labels:
+ - includeSelectors: true
+ includeTemplates: true
+ pairs:
+ app: devfile-gui-wizard
+ app.kubernetes.io/name: devfile-gui-wizard
+ app.kubernetes.io/component: web
+ app.kubernetes.io/part-of: devfile-gui-wizard
+
+images:
+ - name: ghcr.io/ibuziuk/devfile-gui-wizard
+ newName: ghcr.io/ibuziuk/devfile-gui-wizard
+ newTag: latest
+
+replicas:
+ - name: devfile-gui-wizard
+ count: 2
diff --git a/openshift/route.yaml b/openshift/route.yaml
new file mode 100644
index 0000000..3a46157
--- /dev/null
+++ b/openshift/route.yaml
@@ -0,0 +1,16 @@
+apiVersion: route.openshift.io/v1
+kind: Route
+metadata:
+ name: devfile-gui-wizard
+ labels:
+ app: devfile-gui-wizard
+spec:
+ to:
+ kind: Service
+ name: devfile-gui-wizard
+ weight: 100
+ port:
+ targetPort: http
+ tls:
+ termination: edge
+ insecureEdgeTerminationPolicy: Redirect
diff --git a/openshift/service.yaml b/openshift/service.yaml
new file mode 100644
index 0000000..8c70b20
--- /dev/null
+++ b/openshift/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: devfile-gui-wizard
+ labels:
+ app: devfile-gui-wizard
+spec:
+ type: ClusterIP
+ ports:
+ - port: 80
+ targetPort: 8080
+ protocol: TCP
+ name: http
+ selector:
+ app: devfile-gui-wizard
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..52ef018
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2719 @@
+{
+ "name": "devfile-gui-wizard",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "devfile-gui-wizard",
+ "version": "1.0.0",
+ "dependencies": {
+ "js-yaml": "^4.1.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.58.0",
+ "@tailwindcss/forms": "^0.5.7",
+ "@vitejs/plugin-react": "^4.3.0",
+ "autoprefixer": "^10.4.0",
+ "playwright": "^1.58.0",
+ "postcss": "^8.4.0",
+ "tailwindcss": "^3.4.0",
+ "vite": "^5.4.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.58.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz",
+ "integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.58.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
+ "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
+ "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
+ "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
+ "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
+ "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
+ "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
+ "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
+ "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
+ "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
+ "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
+ "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
+ "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
+ "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
+ "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
+ "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
+ "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
+ "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
+ "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
+ "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
+ "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
+ "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
+ "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/forms": {
+ "version": "0.5.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz",
+ "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mini-svg-data-uri": "^1.2.3"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.23",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+ "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001760",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.14",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
+ "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001764",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
+ "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.267",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mini-svg-data-uri": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
+ "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mini-svg-data-uri": "cli.js"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
+ "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
+ "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
+ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.55.1",
+ "@rollup/rollup-android-arm64": "4.55.1",
+ "@rollup/rollup-darwin-arm64": "4.55.1",
+ "@rollup/rollup-darwin-x64": "4.55.1",
+ "@rollup/rollup-freebsd-arm64": "4.55.1",
+ "@rollup/rollup-freebsd-x64": "4.55.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.55.1",
+ "@rollup/rollup-linux-arm64-musl": "4.55.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.55.1",
+ "@rollup/rollup-linux-loong64-musl": "4.55.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.55.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.55.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-musl": "4.55.1",
+ "@rollup/rollup-openbsd-x64": "4.55.1",
+ "@rollup/rollup-openharmony-arm64": "4.55.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.55.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.55.1",
+ "@rollup/rollup-win32-x64-gnu": "4.55.1",
+ "@rollup/rollup-win32-x64-msvc": "4.55.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6c0c432
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "devfile-gui-wizard",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "test": "playwright test",
+ "test:ui": "playwright test --ui",
+ "test:debug": "playwright test --debug"
+ },
+ "dependencies": {
+ "js-yaml": "^4.1.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.58.0",
+ "@tailwindcss/forms": "^0.5.7",
+ "@vitejs/plugin-react": "^4.3.0",
+ "autoprefixer": "^10.4.0",
+ "playwright": "^1.58.0",
+ "postcss": "^8.4.0",
+ "tailwindcss": "^3.4.0",
+ "vite": "^5.4.0"
+ }
+}
diff --git a/playwright.config.js b/playwright.config.js
new file mode 100644
index 0000000..22a6ed8
--- /dev/null
+++ b/playwright.config.js
@@ -0,0 +1,27 @@
+import { defineConfig, devices } from '@playwright/test'
+
+export default defineConfig({
+ testDir: './tests',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: 'html',
+ use: {
+ baseURL: 'http://localhost:5173',
+ trace: 'on-first-retry',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+
+ webServer: {
+ command: 'npm run dev',
+ url: 'http://localhost:5173',
+ reuseExistingServer: !process.env.CI,
+ },
+})
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 0000000..d9eb52f
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,77 @@
+import { WizardProvider } from './hooks/useWizardState'
+import WizardContainer from './components/wizard/WizardContainer'
+import YamlPreview from './components/preview/YamlPreview'
+
+function App() {
+ return (
+
+
+
+
+
+
+
+ Devfile Wizard
+
+
+ Create devfile 2.3.0 configuration files step-by-step
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/src/components/common/Alert.jsx b/src/components/common/Alert.jsx
new file mode 100644
index 0000000..7e6185d
--- /dev/null
+++ b/src/components/common/Alert.jsx
@@ -0,0 +1,56 @@
+export default function Alert({ type = 'info', title, children, onClose }) {
+ const styles = {
+ error: {
+ container: 'bg-red-50 border-red-200',
+ icon: 'text-red-600',
+ title: 'text-red-800',
+ text: 'text-red-700'
+ },
+ warning: {
+ container: 'bg-yellow-50 border-yellow-200',
+ icon: 'text-yellow-600',
+ title: 'text-yellow-800',
+ text: 'text-yellow-700'
+ },
+ success: {
+ container: 'bg-green-50 border-green-200',
+ icon: 'text-green-600',
+ title: 'text-green-800',
+ text: 'text-green-700'
+ },
+ info: {
+ container: 'bg-blue-50 border-blue-200',
+ icon: 'text-blue-600',
+ title: 'text-blue-800',
+ text: 'text-blue-700'
+ }
+ }
+
+ const currentStyle = styles[type]
+
+ return (
+
+
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {children}
+
+
+ {onClose && (
+
+ Dismiss
+ Γ
+
+ )}
+
+
+ )
+}
diff --git a/src/components/common/Badge.jsx b/src/components/common/Badge.jsx
new file mode 100644
index 0000000..1eef75d
--- /dev/null
+++ b/src/components/common/Badge.jsx
@@ -0,0 +1,15 @@
+export default function Badge({ children, variant = 'default', className = '' }) {
+ const variants = {
+ default: 'bg-gray-100 text-gray-800',
+ primary: 'bg-blue-100 text-blue-800',
+ success: 'bg-green-100 text-green-800',
+ warning: 'bg-yellow-100 text-yellow-800',
+ error: 'bg-red-100 text-red-800'
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/common/Button.jsx b/src/components/common/Button.jsx
new file mode 100644
index 0000000..e5253c2
--- /dev/null
+++ b/src/components/common/Button.jsx
@@ -0,0 +1,29 @@
+export default function Button({
+ children,
+ onClick,
+ variant = 'primary',
+ type = 'button',
+ disabled = false,
+ className = ''
+}) {
+ const baseStyles = 'px-4 py-2 rounded-md font-medium transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'
+
+ const variants = {
+ primary: 'bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500',
+ secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800 focus:ring-gray-500',
+ danger: 'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500',
+ success: 'bg-green-600 hover:bg-green-700 text-white focus:ring-green-500',
+ outline: 'border-2 border-gray-300 hover:border-gray-400 text-gray-700 focus:ring-gray-500'
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/common/Card.jsx b/src/components/common/Card.jsx
new file mode 100644
index 0000000..b206f2f
--- /dev/null
+++ b/src/components/common/Card.jsx
@@ -0,0 +1,12 @@
+export default function Card({ children, title, className = '' }) {
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+ {children}
+
+ )
+}
diff --git a/src/components/forms/ArrayFieldManager.jsx b/src/components/forms/ArrayFieldManager.jsx
new file mode 100644
index 0000000..6503b58
--- /dev/null
+++ b/src/components/forms/ArrayFieldManager.jsx
@@ -0,0 +1,46 @@
+import Button from '../common/Button'
+
+export default function ArrayFieldManager({
+ items = [],
+ onAdd,
+ onRemove,
+ onUpdate,
+ renderItem,
+ addButtonText = 'Add Item',
+ emptyMessage = 'No items added yet.'
+}) {
+ return (
+
+ {items.length === 0 ? (
+
+ ) : (
+
+ {items.map((item, index) => (
+
+
+ Item {index + 1}
+ onRemove(index)}
+ className="text-xs px-2 py-1"
+ >
+ Remove
+
+
+ {renderItem(item, index, (field, value) => onUpdate(index, field, value))}
+
+ ))}
+
+ )}
+
+ {addButtonText}
+
+
+ )
+}
diff --git a/src/components/forms/FormCheckbox.jsx b/src/components/forms/FormCheckbox.jsx
new file mode 100644
index 0000000..0627d3e
--- /dev/null
+++ b/src/components/forms/FormCheckbox.jsx
@@ -0,0 +1,32 @@
+export default function FormCheckbox({
+ name,
+ label,
+ checked,
+ onChange,
+ helpText
+}) {
+ return (
+
+
+
+
+
+
+
+ {label}
+
+ {helpText && (
+
{helpText}
+ )}
+
+
+
+ )
+}
diff --git a/src/components/forms/FormInput.jsx b/src/components/forms/FormInput.jsx
new file mode 100644
index 0000000..ed79ec3
--- /dev/null
+++ b/src/components/forms/FormInput.jsx
@@ -0,0 +1,44 @@
+export default function FormInput({
+ name,
+ label,
+ value,
+ onChange,
+ error,
+ required = false,
+ placeholder = '',
+ pattern,
+ maxLength,
+ type = 'text',
+ helpText
+}) {
+ return (
+
+
+ {label}
+ {required && * }
+
+
+ {helpText && !error && (
+
{helpText}
+ )}
+ {error && (
+
{error}
+ )}
+
+ )
+}
diff --git a/src/components/forms/FormSection.jsx b/src/components/forms/FormSection.jsx
new file mode 100644
index 0000000..896affb
--- /dev/null
+++ b/src/components/forms/FormSection.jsx
@@ -0,0 +1,15 @@
+export default function FormSection({ title, description, children }) {
+ return (
+
+ {title && (
+
+
{title}
+ {description && (
+
{description}
+ )}
+
+ )}
+ {children}
+
+ )
+}
diff --git a/src/components/forms/FormSelect.jsx b/src/components/forms/FormSelect.jsx
new file mode 100644
index 0000000..849d508
--- /dev/null
+++ b/src/components/forms/FormSelect.jsx
@@ -0,0 +1,45 @@
+export default function FormSelect({
+ name,
+ label,
+ value,
+ onChange,
+ options,
+ error,
+ required = false,
+ placeholder = 'Select an option',
+ helpText
+}) {
+ return (
+
+
+ {label}
+ {required && * }
+
+
+ {placeholder}
+ {options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+ {helpText && !error && (
+
{helpText}
+ )}
+ {error && (
+
{error}
+ )}
+
+ )
+}
diff --git a/src/components/forms/FormTextarea.jsx b/src/components/forms/FormTextarea.jsx
new file mode 100644
index 0000000..8c888c2
--- /dev/null
+++ b/src/components/forms/FormTextarea.jsx
@@ -0,0 +1,40 @@
+export default function FormTextarea({
+ name,
+ label,
+ value,
+ onChange,
+ error,
+ required = false,
+ placeholder = '',
+ rows = 4,
+ helpText
+}) {
+ return (
+
+
+ {label}
+ {required && * }
+
+
+ {helpText && !error && (
+
{helpText}
+ )}
+ {error && (
+
{error}
+ )}
+
+ )
+}
diff --git a/src/components/forms/KeyValueArrayManager.jsx b/src/components/forms/KeyValueArrayManager.jsx
new file mode 100644
index 0000000..e0da592
--- /dev/null
+++ b/src/components/forms/KeyValueArrayManager.jsx
@@ -0,0 +1,82 @@
+import { useState } from 'react'
+import Button from '../common/Button'
+import FormInput from './FormInput'
+
+export default function KeyValueArrayManager({
+ items = [],
+ onUpdate,
+ keyLabel = 'Key',
+ valueLabel = 'Value',
+ keyPlaceholder = 'key',
+ valuePlaceholder = 'value',
+ addButtonText = 'Add Item',
+ emptyMessage = 'No items added yet.'
+}) {
+ const handleAdd = () => {
+ const newItems = [...items, { name: '', value: '' }]
+ onUpdate(newItems)
+ }
+
+ const handleRemove = (index) => {
+ const newItems = items.filter((_, i) => i !== index)
+ onUpdate(newItems)
+ }
+
+ const handleUpdate = (index, field, value) => {
+ const newItems = items.map((item, i) =>
+ i === index ? { ...item, [field]: value } : item
+ )
+ onUpdate(newItems)
+ }
+
+ return (
+
+ {items.length === 0 ? (
+
+ ) : (
+
+ {items.map((item, index) => (
+
+
+ handleUpdate(index, 'name', e.target.value)}
+ placeholder={keyPlaceholder}
+ />
+
+
+ handleUpdate(index, 'value', e.target.value)}
+ placeholder={valuePlaceholder}
+ />
+
+
+ handleRemove(index)}
+ className="text-xs px-2 py-1"
+ >
+ Remove
+
+
+
+ ))}
+
+ )}
+
+ {addButtonText}
+
+
+ )
+}
diff --git a/src/components/preview/YamlPreview.jsx b/src/components/preview/YamlPreview.jsx
new file mode 100644
index 0000000..a5b1b37
--- /dev/null
+++ b/src/components/preview/YamlPreview.jsx
@@ -0,0 +1,52 @@
+import { useState, useEffect } from 'react'
+import { useWizard } from '../../hooks/useWizardState'
+import { useYamlGenerator } from '../../hooks/useYamlGenerator'
+import { copyToClipboard } from '../../utils/downloadFile'
+import Button from '../common/Button'
+import Card from '../common/Card'
+
+export default function YamlPreview({ inSidebar = false }) {
+ const { state } = useWizard()
+ const { generateYaml } = useYamlGenerator()
+ const [yamlContent, setYamlContent] = useState('')
+ const [copied, setCopied] = useState(false)
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ const yaml = generateYaml(state.devfileData)
+ setYamlContent(yaml)
+ }, 500)
+
+ return () => clearTimeout(timer)
+ }, [state.devfileData, generateYaml])
+
+ const handleCopy = async () => {
+ try {
+ await copyToClipboard(yamlContent)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ } catch (err) {
+ console.error('Failed to copy:', err)
+ }
+ }
+
+ return (
+
+
+
YAML Preview
+
+ {copied ? 'Copied!' : 'Copy'}
+
+
+
+
+ )
+}
diff --git a/src/components/steps/CommandsStep.jsx b/src/components/steps/CommandsStep.jsx
new file mode 100644
index 0000000..59bf24a
--- /dev/null
+++ b/src/components/steps/CommandsStep.jsx
@@ -0,0 +1,226 @@
+import { useWizard } from '../../hooks/useWizardState'
+import { COMMAND_TYPES, COMMAND_GROUPS } from '../../utils/constants'
+import FormInput from '../forms/FormInput'
+import FormSelect from '../forms/FormSelect'
+import ArrayFieldManager from '../forms/ArrayFieldManager'
+import WizardNavigation from '../wizard/WizardNavigation'
+
+export default function CommandsStep() {
+ const { state, dispatch, actions } = useWizard()
+ const commands = state.devfileData.commands
+ const components = state.devfileData.components
+
+ const componentOptions = components.map(c => ({
+ value: c.name,
+ label: c.name
+ }))
+
+ // Helper to determine command type from the property that exists
+ const getCommandType = (command) => {
+ if (command.exec) return 'exec'
+ if (command.apply) return 'apply'
+ if (command.composite) return 'composite'
+ return 'exec' // default
+ }
+
+ const createNewCommand = (type = 'exec') => {
+ const command = { id: '' }
+
+ switch (type) {
+ case 'exec':
+ command.exec = {
+ component: '',
+ commandLine: '',
+ workingDir: ''
+ }
+ break
+ case 'apply':
+ command.apply = {
+ component: ''
+ }
+ break
+ case 'composite':
+ command.composite = {
+ commands: [],
+ parallel: false
+ }
+ break
+ default:
+ command.exec = {
+ component: '',
+ commandLine: ''
+ }
+ }
+
+ return command
+ }
+
+ const handleAddCommand = () => {
+ dispatch({
+ type: actions.ADD_COMMAND,
+ payload: createNewCommand()
+ })
+ }
+
+ const handleRemoveCommand = (index) => {
+ dispatch({
+ type: actions.REMOVE_COMMAND,
+ payload: index
+ })
+ }
+
+ const handleUpdateCommand = (index, field, value) => {
+ const command = commands[index]
+ let updates = {}
+
+ if (field === 'commandType') {
+ // When changing type, replace the entire command structure
+ const newCommand = createNewCommand(value)
+ newCommand.id = command.id // preserve the id
+ updates = newCommand
+ } else if (field === 'id') {
+ updates = { id: value }
+ } else if (field.startsWith('exec.')) {
+ const execField = field.split('.')[1]
+ updates = {
+ exec: {
+ ...command.exec,
+ [execField]: value
+ }
+ }
+ } else if (field.startsWith('apply.')) {
+ const applyField = field.split('.')[1]
+ updates = {
+ apply: {
+ ...command.apply,
+ [applyField]: value
+ }
+ }
+ } else if (field.startsWith('composite.')) {
+ const compositeField = field.split('.')[1]
+ updates = {
+ composite: {
+ ...command.composite,
+ [compositeField]: value
+ }
+ }
+ }
+
+ dispatch({
+ type: actions.UPDATE_COMMAND,
+ payload: { index, data: updates }
+ })
+ }
+
+ const renderCommandItem = (command, index, updateField) => {
+ const commandType = getCommandType(command)
+
+ return (
+
+
updateField('id', e.target.value)}
+ required
+ placeholder="build-project"
+ helpText="Unique identifier for this command"
+ />
+
+ updateField('commandType', e.target.value)}
+ options={COMMAND_TYPES}
+ />
+
+ {commandType === 'exec' && (
+ <>
+ updateField('exec.component', e.target.value)}
+ options={componentOptions}
+ placeholder="Select a component"
+ required
+ helpText="The component to execute this command in"
+ />
+
+ updateField('exec.commandLine', e.target.value)}
+ placeholder="npm install"
+ required
+ helpText="The command to execute"
+ />
+
+ updateField('exec.workingDir', e.target.value)}
+ placeholder="/projects"
+ helpText="Working directory for the command"
+ />
+ >
+ )}
+
+ {commandType === 'apply' && (
+ updateField('apply.component', e.target.value)}
+ options={componentOptions}
+ placeholder="Select a component"
+ required
+ helpText="The component to apply"
+ />
+ )}
+
+ {commandType === 'composite' && (
+
+
+ Composite commands allow you to execute multiple commands sequentially or in parallel.
+ This feature requires additional implementation.
+
+
+ )}
+
+ )
+ }
+
+ return (
+
+
Commands
+
+ Define commands to build, run, test, or debug your project.
+
+
+ {componentOptions.length === 0 && (
+
+
+ You haven't defined any components yet. Commands need components to run in.
+ Consider going back to add components first.
+
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/src/components/steps/ComponentsStep.jsx b/src/components/steps/ComponentsStep.jsx
new file mode 100644
index 0000000..152d99c
--- /dev/null
+++ b/src/components/steps/ComponentsStep.jsx
@@ -0,0 +1,200 @@
+import { useWizard } from '../../hooks/useWizardState'
+import { COMPONENT_TYPES, ENDPOINT_PROTOCOLS } from '../../utils/constants'
+import FormInput from '../forms/FormInput'
+import FormSelect from '../forms/FormSelect'
+import FormCheckbox from '../forms/FormCheckbox'
+import FormTextarea from '../forms/FormTextarea'
+import FormSection from '../forms/FormSection'
+import ArrayFieldManager from '../forms/ArrayFieldManager'
+import KeyValueArrayManager from '../forms/KeyValueArrayManager'
+import WizardNavigation from '../wizard/WizardNavigation'
+
+export default function ComponentsStep() {
+ const { state, dispatch, actions } = useWizard()
+ const components = state.devfileData.components
+
+ // Helper to determine component type from the property that exists
+ const getComponentType = (component) => {
+ if (component.container) return 'container'
+ if (component.volume) return 'volume'
+ if (component.kubernetes) return 'kubernetes'
+ if (component.openshift) return 'openshift'
+ if (component.image) return 'image'
+ return 'container' // default
+ }
+
+ const createNewComponent = (type = 'container') => {
+ const component = { name: '' }
+
+ switch (type) {
+ case 'container':
+ component.container = {
+ image: '',
+ mountSources: true,
+ memoryLimit: '',
+ cpuLimit: ''
+ }
+ break
+ case 'volume':
+ component.volume = {
+ size: ''
+ }
+ break
+ // Add other types as needed
+ default:
+ component.container = {
+ image: '',
+ mountSources: true
+ }
+ }
+
+ return component
+ }
+
+ const handleAddComponent = () => {
+ dispatch({
+ type: actions.ADD_COMPONENT,
+ payload: createNewComponent()
+ })
+ }
+
+ const handleRemoveComponent = (index) => {
+ dispatch({
+ type: actions.REMOVE_COMPONENT,
+ payload: index
+ })
+ }
+
+ const handleUpdateComponent = (index, field, value) => {
+ const component = components[index]
+ let updates = {}
+
+ if (field === 'componentType') {
+ // When changing type, replace the entire component structure
+ const newComponent = createNewComponent(value)
+ newComponent.name = component.name // preserve the name
+ updates = newComponent
+ } else if (field === 'name') {
+ updates = { name: value }
+ } else if (field.startsWith('container.')) {
+ const containerField = field.split('.')[1]
+ updates = {
+ container: {
+ ...component.container,
+ [containerField]: containerField === 'mountSources' ? value : value
+ }
+ }
+ } else if (field.startsWith('volume.')) {
+ const volumeField = field.split('.')[1]
+ updates = {
+ volume: {
+ ...component.volume,
+ [volumeField]: value
+ }
+ }
+ }
+
+ dispatch({
+ type: actions.UPDATE_COMPONENT,
+ payload: { index, data: updates }
+ })
+ }
+
+ const renderComponentItem = (component, index, updateField) => {
+ const componentType = getComponentType(component)
+
+ return (
+
+
updateField('name', e.target.value)}
+ required
+ placeholder="dev-container"
+ />
+
+ updateField('componentType', e.target.value)}
+ options={COMPONENT_TYPES}
+ />
+
+ {componentType === 'container' && (
+ <>
+ updateField('container.image', e.target.value)}
+ placeholder="docker.io/library/node:18"
+ required
+ helpText="Full container image reference"
+ />
+
+ updateField('container.mountSources', e.target.checked)}
+ helpText="Mount project sources at /projects"
+ />
+
+
+ updateField('container.memoryLimit', e.target.value)}
+ placeholder="1Gi"
+ />
+
+ updateField('container.cpuLimit', e.target.value)}
+ placeholder="1000m"
+ />
+
+ >
+ )}
+
+ {componentType === 'volume' && (
+ updateField('volume.size', e.target.value)}
+ placeholder="2Gi"
+ required
+ helpText="Storage size (e.g., 1Gi, 500Mi)"
+ />
+ )}
+
+ )
+ }
+
+ return (
+
+
Components
+
+ Define containers, volumes, and other components for your development environment.
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/steps/EventsStep.jsx b/src/components/steps/EventsStep.jsx
new file mode 100644
index 0000000..097da00
--- /dev/null
+++ b/src/components/steps/EventsStep.jsx
@@ -0,0 +1,92 @@
+import { useWizard } from '../../hooks/useWizardState'
+import FormSection from '../forms/FormSection'
+import WizardNavigation from '../wizard/WizardNavigation'
+
+export default function EventsStep() {
+ const { state, dispatch, actions } = useWizard()
+ const events = state.devfileData.events
+ const commands = state.devfileData.commands
+
+ const commandIds = commands.map(c => c.id).filter(Boolean)
+
+ const handleEventChange = (eventType, commandId, checked) => {
+ const currentCommands = events[eventType] || []
+
+ let updatedCommands
+ if (checked) {
+ updatedCommands = [...currentCommands, commandId]
+ } else {
+ updatedCommands = currentCommands.filter(id => id !== commandId)
+ }
+
+ dispatch({
+ type: actions.UPDATE_EVENTS,
+ payload: {
+ [eventType]: updatedCommands
+ }
+ })
+ }
+
+ const renderEventSection = (eventType, title, description) => {
+ const selectedCommands = events[eventType] || []
+
+ return (
+
+ {commandIds.length === 0 ? (
+
+ No commands available. Add commands in the previous step to bind them to events.
+
+ ) : (
+
+ {commandIds.map(commandId => (
+
+ handleEventChange(eventType, commandId, e.target.checked)}
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ />
+ {commandId}
+
+ ))}
+
+ )}
+
+ )
+ }
+
+ return (
+
+
Events
+
+ Bind commands to lifecycle events. Commands will execute at specific points in the workspace lifecycle.
+
+
+ {renderEventSection(
+ 'preStart',
+ 'Pre Start',
+ 'Commands executed before the workspace starts'
+ )}
+
+ {renderEventSection(
+ 'postStart',
+ 'Post Start',
+ 'Commands executed after the workspace starts'
+ )}
+
+ {renderEventSection(
+ 'preStop',
+ 'Pre Stop',
+ 'Commands executed before the workspace stops'
+ )}
+
+ {renderEventSection(
+ 'postStop',
+ 'Post Stop',
+ 'Commands executed after the workspace stops'
+ )}
+
+
+
+ )
+}
diff --git a/src/components/steps/MetadataStep.jsx b/src/components/steps/MetadataStep.jsx
new file mode 100644
index 0000000..e61b7dd
--- /dev/null
+++ b/src/components/steps/MetadataStep.jsx
@@ -0,0 +1,162 @@
+import { useState, useEffect } from 'react'
+import { useWizard } from '../../hooks/useWizardState'
+import { validateMetadata } from '../../utils/validation'
+import { LANGUAGES, PROJECT_TYPES } from '../../utils/constants'
+import FormInput from '../forms/FormInput'
+import FormTextarea from '../forms/FormTextarea'
+import FormSelect from '../forms/FormSelect'
+import FormSection from '../forms/FormSection'
+import WizardNavigation from '../wizard/WizardNavigation'
+import Alert from '../common/Alert'
+
+export default function MetadataStep() {
+ const { state, dispatch, actions } = useWizard()
+ const [errors, setErrors] = useState({})
+ const [tags, setTags] = useState(state.devfileData.metadata.tags?.join(', ') || '')
+
+ const metadata = state.devfileData.metadata
+
+ const handleChange = (e) => {
+ const { name, value } = e.target
+ dispatch({
+ type: actions.UPDATE_METADATA,
+ payload: { [name]: value }
+ })
+ }
+
+ const handleTagsChange = (e) => {
+ const value = e.target.value
+ setTags(value)
+ const tagsArray = value.split(',').map(t => t.trim()).filter(Boolean)
+ dispatch({
+ type: actions.UPDATE_METADATA,
+ payload: { tags: tagsArray }
+ })
+ }
+
+ const handleNext = () => {
+ const validationErrors = validateMetadata(metadata)
+ setErrors(validationErrors)
+
+ if (Object.keys(validationErrors).length > 0) {
+ return false
+ }
+
+ return true
+ }
+
+ const canProceed = metadata.name && metadata.name.trim() !== ''
+
+ return (
+
+
Basic Information
+
+ Enter the basic metadata for your devfile. The name field is required.
+
+
+ {Object.keys(errors).length > 0 && (
+
+ Please fix the errors below before continuing.
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/steps/ProjectsStep.jsx b/src/components/steps/ProjectsStep.jsx
new file mode 100644
index 0000000..6759cf1
--- /dev/null
+++ b/src/components/steps/ProjectsStep.jsx
@@ -0,0 +1,166 @@
+import { useState } from 'react'
+import { useWizard } from '../../hooks/useWizardState'
+import FormInput from '../forms/FormInput'
+import FormSelect from '../forms/FormSelect'
+import FormCheckbox from '../forms/FormCheckbox'
+import ArrayFieldManager from '../forms/ArrayFieldManager'
+import WizardNavigation from '../wizard/WizardNavigation'
+import Alert from '../common/Alert'
+
+export default function ProjectsStep() {
+ const { state, dispatch, actions } = useWizard()
+ const projects = state.devfileData.projects
+
+ const createNewProject = () => ({
+ name: '',
+ clonePath: '',
+ sourceType: 'git',
+ git: {
+ remotes: { origin: '' },
+ checkoutFrom: { revision: '' }
+ },
+ zip: {
+ location: ''
+ }
+ })
+
+ const handleAddProject = () => {
+ dispatch({
+ type: actions.ADD_PROJECT,
+ payload: createNewProject()
+ })
+ }
+
+ const handleRemoveProject = (index) => {
+ dispatch({
+ type: actions.REMOVE_PROJECT,
+ payload: index
+ })
+ }
+
+ const handleUpdateProject = (index, field, value) => {
+ const project = projects[index]
+ let updates = {}
+
+ if (field === 'sourceType') {
+ updates = { sourceType: value }
+ } else if (field === 'name' || field === 'clonePath') {
+ updates = { [field]: value }
+ } else if (field === 'git.url') {
+ updates = {
+ git: {
+ ...project.git,
+ remotes: { origin: value }
+ }
+ }
+ } else if (field === 'git.revision') {
+ updates = {
+ git: {
+ ...project.git,
+ checkoutFrom: { revision: value }
+ }
+ }
+ } else if (field === 'zip.location') {
+ updates = {
+ zip: { location: value }
+ }
+ }
+
+ dispatch({
+ type: actions.UPDATE_PROJECT,
+ payload: { index, data: updates }
+ })
+ }
+
+ const renderProjectItem = (project, index, updateField) => {
+ return (
+
+ updateField('name', e.target.value)}
+ required
+ placeholder="my-project"
+ />
+
+ updateField('clonePath', e.target.value)}
+ placeholder="relative/path"
+ helpText="Relative path within /projects/ directory"
+ />
+
+ updateField('sourceType', e.target.value)}
+ options={[
+ { value: 'git', label: 'Git Repository' },
+ { value: 'zip', label: 'Zip Archive' }
+ ]}
+ />
+
+ {project.sourceType === 'git' && (
+ <>
+ updateField('git.url', e.target.value)}
+ placeholder="https://github.com/org/repo.git"
+ required
+ />
+
+ updateField('git.revision', e.target.value)}
+ placeholder="main"
+ />
+ >
+ )}
+
+ {project.sourceType === 'zip' && (
+ updateField('zip.location', e.target.value)}
+ placeholder="https://example.com/archive.zip"
+ required
+ />
+ )}
+
+ )
+ }
+
+ return (
+
+
Projects
+
+ Add source code repositories (Git or Zip) to clone into your workspace.
+
+
+
+ Note: Project definition is optional. If the devfile is located in the root of the repository, it is recommended to omit the 'projects' section and skip this step.
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/steps/ReviewStep.jsx b/src/components/steps/ReviewStep.jsx
new file mode 100644
index 0000000..a9f39b6
--- /dev/null
+++ b/src/components/steps/ReviewStep.jsx
@@ -0,0 +1,188 @@
+import { useWizard } from '../../hooks/useWizardState'
+import { useYamlGenerator } from '../../hooks/useYamlGenerator'
+import { downloadYamlFile } from '../../utils/downloadFile'
+import { validateDevfile } from '../../utils/devfileGenerator'
+import { WIZARD_STEPS } from '../../utils/constants'
+import Button from '../common/Button'
+import Card from '../common/Card'
+import Badge from '../common/Badge'
+import Alert from '../common/Alert'
+import YamlPreview from '../preview/YamlPreview'
+
+export default function ReviewStep() {
+ const { state, dispatch, actions } = useWizard()
+ const { generateYaml } = useYamlGenerator()
+
+ const metadata = state.devfileData.metadata
+ const projects = state.devfileData.projects
+ const components = state.devfileData.components
+ const commands = state.devfileData.commands
+ const events = state.devfileData.events
+ const variables = state.devfileData.variables
+
+ const validationErrors = validateDevfile(state.devfileData)
+
+ const handleDownload = () => {
+ const yamlContent = generateYaml(state.devfileData)
+ downloadYamlFile(yamlContent, 'devfile.yaml')
+ }
+
+ const handleReset = () => {
+ if (window.confirm('Are you sure you want to reset the wizard? All data will be lost.')) {
+ dispatch({ type: actions.RESET_WIZARD })
+ }
+ }
+
+ const handleEdit = (stepIndex) => {
+ dispatch({ type: actions.GOTO_STEP, payload: stepIndex })
+ }
+
+ const renderSummarySection = (title, stepIndex, hasContent, children) => {
+ return (
+
+
+
+
{title}
+ {hasContent ? (
+ Configured
+ ) : (
+ Not configured
+ )}
+
+
handleEdit(stepIndex)}
+ className="text-xs px-2 py-1"
+ >
+ Edit
+
+
+ {children}
+
+ )
+ }
+
+ return (
+
+
Review & Download
+
+ Review your devfile configuration and download the YAML file.
+
+
+ {validationErrors.length > 0 && (
+
+
+ {validationErrors.map((error, index) => (
+ {error}
+ ))}
+
+
+ )}
+
+
+
+ {renderSummarySection(
+ 'Basic Information',
+ 0,
+ metadata.name !== '',
+
+ {metadata.name &&
Name: {metadata.name}
}
+ {metadata.version &&
Version: {metadata.version}
}
+ {metadata.displayName &&
Display Name: {metadata.displayName}
}
+ {metadata.language &&
Language: {metadata.language}
}
+
+ )}
+
+ {renderSummarySection(
+ 'Projects',
+ 1,
+ projects.length > 0,
+
+ {projects.length > 0 ? (
+
{projects.length} project(s) configured
+ ) : (
+
No projects added
+ )}
+
+ )}
+
+ {renderSummarySection(
+ 'Components',
+ 2,
+ components.length > 0,
+
+ {components.length > 0 ? (
+
{components.length} component(s) configured
+ ) : (
+
No components added
+ )}
+
+ )}
+
+
+
+ {renderSummarySection(
+ 'Commands',
+ 3,
+ commands.length > 0,
+
+ {commands.length > 0 ? (
+
{commands.length} command(s) configured
+ ) : (
+
No commands added
+ )}
+
+ )}
+
+ {renderSummarySection(
+ 'Events',
+ 4,
+ Object.values(events).some(arr => arr.length > 0),
+
+ {events.preStart?.length > 0 &&
Pre Start: {events.preStart.length} command(s)
}
+ {events.postStart?.length > 0 &&
Post Start: {events.postStart.length} command(s)
}
+ {events.preStop?.length > 0 &&
Pre Stop: {events.preStop.length} command(s)
}
+ {events.postStop?.length > 0 &&
Post Stop: {events.postStop.length} command(s)
}
+ {!Object.values(events).some(arr => arr.length > 0) && (
+
No events configured
+ )}
+
+ )}
+
+ {renderSummarySection(
+ 'Variables',
+ 5,
+ Object.keys(variables).length > 0,
+
+ {Object.keys(variables).length > 0 ? (
+
{Object.keys(variables).length} variable(s) defined
+ ) : (
+
No variables defined
+ )}
+
+ )}
+
+
+
+
+
+
+
+ Reset Wizard
+
+
+ 0}
+ className="px-6"
+ >
+ Download devfile.yaml
+
+
+
+ )
+}
diff --git a/src/components/steps/VariablesStep.jsx b/src/components/steps/VariablesStep.jsx
new file mode 100644
index 0000000..694a6e2
--- /dev/null
+++ b/src/components/steps/VariablesStep.jsx
@@ -0,0 +1,120 @@
+import { useWizard } from '../../hooks/useWizardState'
+import FormInput from '../forms/FormInput'
+import Button from '../common/Button'
+import WizardNavigation from '../wizard/WizardNavigation'
+import Alert from '../common/Alert'
+
+export default function VariablesStep() {
+ const { state, dispatch, actions } = useWizard()
+ const variables = state.devfileData.variables || {}
+
+ const variableEntries = Object.entries(variables)
+
+ const handleAddVariable = () => {
+ const newVars = { ...variables, [`VAR_${Object.keys(variables).length + 1}`]: '' }
+ dispatch({
+ type: actions.UPDATE_VARIABLES,
+ payload: newVars
+ })
+ }
+
+ const handleRemoveVariable = (key) => {
+ const newVars = { ...variables }
+ delete newVars[key]
+ dispatch({
+ type: actions.UPDATE_VARIABLES,
+ payload: newVars
+ })
+ }
+
+ const handleUpdateKey = (oldKey, newKey) => {
+ if (oldKey === newKey) return
+
+ const newVars = {}
+ for (const [k, v] of Object.entries(variables)) {
+ if (k === oldKey) {
+ newVars[newKey] = v
+ } else {
+ newVars[k] = v
+ }
+ }
+
+ dispatch({
+ type: actions.UPDATE_VARIABLES,
+ payload: newVars
+ })
+ }
+
+ const handleUpdateValue = (key, value) => {
+ dispatch({
+ type: actions.UPDATE_VARIABLES,
+ payload: {
+ ...variables,
+ [key]: value
+ }
+ })
+ }
+
+ return (
+
+
Variables
+
+ Define variables that can be referenced throughout your devfile using the syntax: {'{{variable-name}}'}
+
+
+
+ Variables are replaced in string fields throughout the devfile. Use them to avoid repetition and make your devfile more maintainable.
+
+
+
+ {variableEntries.length === 0 ? (
+
+
No variables defined yet.
+
+ ) : (
+ variableEntries.map(([key, value], index) => (
+
+
+ handleUpdateKey(key, e.target.value)}
+ placeholder="VARIABLE_NAME"
+ />
+
+
+ handleUpdateValue(key, e.target.value)}
+ placeholder="variable value"
+ />
+
+
+ handleRemoveVariable(key)}
+ className="text-xs px-2 py-1"
+ >
+ Remove
+
+
+
+ ))
+ )}
+
+
+ Add Variable
+
+
+
+
+
+ )
+}
diff --git a/src/components/wizard/WizardContainer.jsx b/src/components/wizard/WizardContainer.jsx
new file mode 100644
index 0000000..07b6dfe
--- /dev/null
+++ b/src/components/wizard/WizardContainer.jsx
@@ -0,0 +1,46 @@
+import { useWizard } from '../../hooks/useWizardState'
+import WizardProgressBar from './WizardProgressBar'
+import Card from '../common/Card'
+
+// Import step components (will be created next)
+import MetadataStep from '../steps/MetadataStep'
+import ProjectsStep from '../steps/ProjectsStep'
+import ComponentsStep from '../steps/ComponentsStep'
+import CommandsStep from '../steps/CommandsStep'
+import EventsStep from '../steps/EventsStep'
+import VariablesStep from '../steps/VariablesStep'
+import ReviewStep from '../steps/ReviewStep'
+
+export default function WizardContainer() {
+ const { state } = useWizard()
+
+ const renderStep = () => {
+ switch (state.currentStep) {
+ case 0:
+ return
+ case 1:
+ return
+ case 2:
+ return
+ case 3:
+ return
+ case 4:
+ return
+ case 5:
+ return
+ case 6:
+ return
+ default:
+ return
+ }
+ }
+
+ return (
+
+
+
+ {renderStep()}
+
+
+ )
+}
diff --git a/src/components/wizard/WizardNavigation.jsx b/src/components/wizard/WizardNavigation.jsx
new file mode 100644
index 0000000..fe4a973
--- /dev/null
+++ b/src/components/wizard/WizardNavigation.jsx
@@ -0,0 +1,69 @@
+import { useWizard } from '../../hooks/useWizardState'
+import { WIZARD_STEPS } from '../../utils/constants'
+import Button from '../common/Button'
+
+export default function WizardNavigation({ onNext, canProceed = true }) {
+ const { state, dispatch, actions } = useWizard()
+
+ const currentStepConfig = WIZARD_STEPS[state.currentStep]
+ const isFirstStep = state.currentStep === 0
+ const isLastStep = state.currentStep === WIZARD_STEPS.length - 1
+
+ const handlePrevious = () => {
+ dispatch({ type: actions.PREV_STEP })
+ }
+
+ const handleNext = () => {
+ if (onNext) {
+ const proceed = onNext()
+ if (proceed !== false) {
+ dispatch({ type: actions.MARK_STEP_COMPLETED, payload: state.currentStep })
+ dispatch({ type: actions.NEXT_STEP })
+ }
+ } else {
+ dispatch({ type: actions.MARK_STEP_COMPLETED, payload: state.currentStep })
+ dispatch({ type: actions.NEXT_STEP })
+ }
+ }
+
+ const handleSkip = () => {
+ dispatch({ type: actions.MARK_STEP_COMPLETED, payload: state.currentStep })
+ dispatch({ type: actions.NEXT_STEP })
+ }
+
+ return (
+
+
+ {!isFirstStep && (
+
+ Previous
+
+ )}
+
+
+
+ {!isLastStep && !currentStepConfig.required && (
+
+ Skip
+
+ )}
+
+ {!isLastStep && (
+
+ Next
+
+ )}
+
+
+ )
+}
diff --git a/src/components/wizard/WizardProgressBar.jsx b/src/components/wizard/WizardProgressBar.jsx
new file mode 100644
index 0000000..07f7a09
--- /dev/null
+++ b/src/components/wizard/WizardProgressBar.jsx
@@ -0,0 +1,75 @@
+import { useWizard } from '../../hooks/useWizardState'
+import { WIZARD_STEPS } from '../../utils/constants'
+
+export default function WizardProgressBar() {
+ const { state, dispatch, actions } = useWizard()
+
+ const handleStepClick = (stepIndex) => {
+ // Can only navigate to completed steps or current step
+ if (state.completedSteps.includes(stepIndex) || stepIndex === state.currentStep) {
+ dispatch({ type: actions.GOTO_STEP, payload: stepIndex })
+ }
+ }
+
+ return (
+
+
+ {/* Step circles */}
+
+ {WIZARD_STEPS.map((step, index) => {
+ const isCompleted = state.completedSteps.includes(index)
+ const isCurrent = state.currentStep === index
+ const isClickable = isCompleted || isCurrent
+
+ return (
+
+ {/* Connecting line - extends from center of this circle to center of next circle */}
+ {index < WIZARD_STEPS.length - 1 && (
+
+ )}
+
isClickable && handleStepClick(index)}
+ title={step.title}
+ >
+ {isCompleted && !isCurrent ? 'β' : index + 1}
+
+
+ {step.title}
+
+
+ )
+ })}
+
+
+
+ Step {state.currentStep + 1} of {WIZARD_STEPS.length}
+
+
+ )
+}
diff --git a/src/hooks/useWizardState.jsx b/src/hooks/useWizardState.jsx
new file mode 100644
index 0000000..2a9a225
--- /dev/null
+++ b/src/hooks/useWizardState.jsx
@@ -0,0 +1,274 @@
+import { createContext, useContext, useReducer } from 'react'
+
+const WizardContext = createContext()
+
+const initialState = {
+ currentStep: 0,
+ completedSteps: [],
+ devfileData: {
+ schemaVersion: '2.3.0',
+ metadata: {
+ name: '',
+ version: '',
+ displayName: '',
+ description: '',
+ language: '',
+ projectType: '',
+ provider: '',
+ tags: [],
+ website: '',
+ supportUrl: ''
+ },
+ projects: [],
+ components: [],
+ commands: [],
+ events: {
+ preStart: [],
+ postStart: [],
+ preStop: [],
+ postStop: []
+ },
+ variables: {},
+ starterProjects: [],
+ attributes: {}
+ },
+ validationErrors: {}
+}
+
+const WIZARD_ACTIONS = {
+ NEXT_STEP: 'NEXT_STEP',
+ PREV_STEP: 'PREV_STEP',
+ GOTO_STEP: 'GOTO_STEP',
+ UPDATE_METADATA: 'UPDATE_METADATA',
+ ADD_PROJECT: 'ADD_PROJECT',
+ REMOVE_PROJECT: 'REMOVE_PROJECT',
+ UPDATE_PROJECT: 'UPDATE_PROJECT',
+ ADD_COMPONENT: 'ADD_COMPONENT',
+ REMOVE_COMPONENT: 'REMOVE_COMPONENT',
+ UPDATE_COMPONENT: 'UPDATE_COMPONENT',
+ ADD_COMMAND: 'ADD_COMMAND',
+ REMOVE_COMMAND: 'REMOVE_COMMAND',
+ UPDATE_COMMAND: 'UPDATE_COMMAND',
+ UPDATE_EVENTS: 'UPDATE_EVENTS',
+ UPDATE_VARIABLES: 'UPDATE_VARIABLES',
+ SET_VALIDATION_ERRORS: 'SET_VALIDATION_ERRORS',
+ RESET_WIZARD: 'RESET_WIZARD',
+ MARK_STEP_COMPLETED: 'MARK_STEP_COMPLETED'
+}
+
+function wizardReducer(state, action) {
+ switch (action.type) {
+ case WIZARD_ACTIONS.NEXT_STEP:
+ return {
+ ...state,
+ currentStep: Math.min(state.currentStep + 1, 6),
+ completedSteps: [...new Set([...state.completedSteps, state.currentStep])]
+ }
+
+ case WIZARD_ACTIONS.PREV_STEP:
+ return {
+ ...state,
+ currentStep: Math.max(state.currentStep - 1, 0)
+ }
+
+ case WIZARD_ACTIONS.GOTO_STEP:
+ return {
+ ...state,
+ currentStep: action.payload
+ }
+
+ case WIZARD_ACTIONS.MARK_STEP_COMPLETED:
+ return {
+ ...state,
+ completedSteps: [...new Set([...state.completedSteps, action.payload])]
+ }
+
+ case WIZARD_ACTIONS.UPDATE_METADATA:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ metadata: {
+ ...state.devfileData.metadata,
+ ...action.payload
+ }
+ }
+ }
+
+ case WIZARD_ACTIONS.ADD_PROJECT:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ projects: [...state.devfileData.projects, action.payload]
+ }
+ }
+
+ case WIZARD_ACTIONS.REMOVE_PROJECT:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ projects: state.devfileData.projects.filter((_, index) => index !== action.payload)
+ }
+ }
+
+ case WIZARD_ACTIONS.UPDATE_PROJECT:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ projects: state.devfileData.projects.map((project, index) =>
+ index === action.payload.index
+ ? { ...project, ...action.payload.data }
+ : project
+ )
+ }
+ }
+
+ case WIZARD_ACTIONS.ADD_COMPONENT:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ components: [...state.devfileData.components, action.payload]
+ }
+ }
+
+ case WIZARD_ACTIONS.REMOVE_COMPONENT:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ components: state.devfileData.components.filter((_, index) => index !== action.payload)
+ }
+ }
+
+ case WIZARD_ACTIONS.UPDATE_COMPONENT:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ components: state.devfileData.components.map((component, index) => {
+ if (index !== action.payload.index) {
+ return component
+ }
+
+ // If the update contains a type-specific property (container, volume, etc.),
+ // it's a full replacement (e.g., changing component type)
+ const isFullReplacement = ['container', 'volume', 'kubernetes', 'openshift', 'image']
+ .some(key => key in action.payload.data)
+
+ if (isFullReplacement) {
+ // Full replacement: use only the new data, preserving the name if it exists
+ return {
+ name: component.name,
+ ...action.payload.data
+ }
+ }
+
+ // Partial update: merge with existing component
+ return { ...component, ...action.payload.data }
+ })
+ }
+ }
+
+ case WIZARD_ACTIONS.ADD_COMMAND:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ commands: [...state.devfileData.commands, action.payload]
+ }
+ }
+
+ case WIZARD_ACTIONS.REMOVE_COMMAND:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ commands: state.devfileData.commands.filter((_, index) => index !== action.payload)
+ }
+ }
+
+ case WIZARD_ACTIONS.UPDATE_COMMAND:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ commands: state.devfileData.commands.map((command, index) => {
+ if (index !== action.payload.index) {
+ return command
+ }
+
+ // If the update contains a type-specific property (exec, apply, composite),
+ // it's a full replacement (e.g., changing command type)
+ const isFullReplacement = ['exec', 'apply', 'composite']
+ .some(key => key in action.payload.data)
+
+ if (isFullReplacement) {
+ // Full replacement: use only the new data, preserving the id if it exists
+ return {
+ id: command.id,
+ ...action.payload.data
+ }
+ }
+
+ // Partial update: merge with existing command
+ return { ...command, ...action.payload.data }
+ })
+ }
+ }
+
+ case WIZARD_ACTIONS.UPDATE_EVENTS:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ events: {
+ ...state.devfileData.events,
+ ...action.payload
+ }
+ }
+ }
+
+ case WIZARD_ACTIONS.UPDATE_VARIABLES:
+ return {
+ ...state,
+ devfileData: {
+ ...state.devfileData,
+ variables: action.payload
+ }
+ }
+
+ case WIZARD_ACTIONS.SET_VALIDATION_ERRORS:
+ return {
+ ...state,
+ validationErrors: action.payload
+ }
+
+ case WIZARD_ACTIONS.RESET_WIZARD:
+ return initialState
+
+ default:
+ return state
+ }
+}
+
+export function WizardProvider({ children }) {
+ const [state, dispatch] = useReducer(wizardReducer, initialState)
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useWizard() {
+ const context = useContext(WizardContext)
+ if (!context) {
+ throw new Error('useWizard must be used within WizardProvider')
+ }
+ return context
+}
diff --git a/src/hooks/useYamlGenerator.jsx b/src/hooks/useYamlGenerator.jsx
new file mode 100644
index 0000000..8a5cfe0
--- /dev/null
+++ b/src/hooks/useYamlGenerator.jsx
@@ -0,0 +1,19 @@
+import yaml from 'js-yaml'
+import { cleanDevfileData } from '../utils/devfileGenerator'
+
+export function useYamlGenerator() {
+ const generateYaml = (devfileData) => {
+ const cleaned = cleanDevfileData(devfileData)
+
+ const yamlString = yaml.dump(cleaned, {
+ indent: 2,
+ lineWidth: 80,
+ noRefs: true,
+ sortKeys: false
+ })
+
+ return yamlString
+ }
+
+ return { generateYaml }
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..8c3e7b5
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,19 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ body {
+ @apply bg-gray-50 text-gray-900;
+ }
+}
+
+@layer components {
+ .wizard-step {
+ @apply transition-all duration-300 ease-in-out;
+ }
+
+ .yaml-preview {
+ @apply font-mono text-sm bg-gray-900 text-gray-100 p-4 rounded-lg overflow-auto;
+ }
+}
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 0000000..54b39dd
--- /dev/null
+++ b/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/src/utils/constants.js b/src/utils/constants.js
new file mode 100644
index 0000000..d1612e9
--- /dev/null
+++ b/src/utils/constants.js
@@ -0,0 +1,67 @@
+export const WIZARD_STEPS = [
+ { id: 'metadata', title: 'Basic Information', required: true },
+ { id: 'projects', title: 'Projects', required: false },
+ { id: 'components', title: 'Components', required: false },
+ { id: 'commands', title: 'Commands', required: false },
+ { id: 'events', title: 'Events', required: false },
+ { id: 'variables', title: 'Variables', required: false },
+ { id: 'review', title: 'Review & Download', required: true }
+]
+
+export const LANGUAGES = [
+ { value: 'JavaScript', label: 'JavaScript' },
+ { value: 'TypeScript', label: 'TypeScript' },
+ { value: 'Python', label: 'Python' },
+ { value: 'Java', label: 'Java' },
+ { value: 'Go', label: 'Go' },
+ { value: 'C#', label: 'C#' },
+ { value: 'C++', label: 'C++' },
+ { value: 'Ruby', label: 'Ruby' },
+ { value: 'PHP', label: 'PHP' },
+ { value: 'Rust', label: 'Rust' },
+ { value: 'Swift', label: 'Swift' },
+ { value: 'Kotlin', label: 'Kotlin' }
+]
+
+export const PROJECT_TYPES = [
+ { value: 'application', label: 'Application' },
+ { value: 'library', label: 'Library' },
+ { value: 'tool', label: 'Tool' },
+ { value: 'framework', label: 'Framework' }
+]
+
+export const COMPONENT_TYPES = [
+ { value: 'container', label: 'Container' },
+ { value: 'volume', label: 'Volume' },
+ { value: 'kubernetes', label: 'Kubernetes' },
+ { value: 'openshift', label: 'OpenShift' },
+ { value: 'image', label: 'Image' }
+]
+
+export const COMMAND_TYPES = [
+ { value: 'exec', label: 'Exec' },
+ { value: 'apply', label: 'Apply' },
+ { value: 'composite', label: 'Composite' }
+]
+
+export const COMMAND_GROUPS = [
+ { value: 'build', label: 'Build' },
+ { value: 'run', label: 'Run' },
+ { value: 'test', label: 'Test' },
+ { value: 'debug', label: 'Debug' }
+]
+
+export const ENDPOINT_PROTOCOLS = [
+ { value: 'http', label: 'HTTP' },
+ { value: 'https', label: 'HTTPS' },
+ { value: 'tcp', label: 'TCP' },
+ { value: 'udp', label: 'UDP' },
+ { value: 'ws', label: 'WebSocket' }
+]
+
+export const EVENT_TYPES = [
+ { value: 'preStart', label: 'Pre Start' },
+ { value: 'postStart', label: 'Post Start' },
+ { value: 'preStop', label: 'Pre Stop' },
+ { value: 'postStop', label: 'Post Stop' }
+]
diff --git a/src/utils/devfileGenerator.js b/src/utils/devfileGenerator.js
new file mode 100644
index 0000000..1bcace9
--- /dev/null
+++ b/src/utils/devfileGenerator.js
@@ -0,0 +1,169 @@
+function hasContent(obj) {
+ if (!obj) return false
+ if (typeof obj !== 'object') return !!obj
+ if (Array.isArray(obj)) return obj.length > 0
+ return Object.keys(obj).length > 0 && Object.values(obj).some(v => v !== '' && v !== null && v !== undefined)
+}
+
+function cleanObject(obj) {
+ if (!obj || typeof obj !== 'object') return obj
+
+ if (Array.isArray(obj)) {
+ const cleaned = obj.map(cleanObject).filter(item => item !== null && item !== undefined)
+ return cleaned.length > 0 ? cleaned : undefined
+ }
+
+ const result = {}
+ for (const [key, value] of Object.entries(obj)) {
+ if (value === null || value === undefined || value === '') continue
+
+ if (Array.isArray(value)) {
+ if (value.length > 0) {
+ result[key] = cleanObject(value)
+ }
+ } else if (typeof value === 'object') {
+ const cleaned = cleanObject(value)
+ if (cleaned && Object.keys(cleaned).length > 0) {
+ result[key] = cleaned
+ }
+ } else {
+ result[key] = value
+ }
+ }
+
+ return Object.keys(result).length > 0 ? result : undefined
+}
+
+export function cleanDevfileData(data) {
+ const result = {
+ schemaVersion: data.schemaVersion
+ }
+
+ // Add metadata if any fields are populated
+ if (hasContent(data.metadata)) {
+ const metadata = cleanObject(data.metadata)
+ if (metadata && Object.keys(metadata).length > 0) {
+ result.metadata = metadata
+ }
+ }
+
+ // Add projects if array not empty
+ if (data.projects && data.projects.length > 0) {
+ const projects = cleanObject(data.projects)
+ if (projects && projects.length > 0) {
+ result.projects = projects
+ }
+ }
+
+ // Add starterProjects if array not empty
+ if (data.starterProjects && data.starterProjects.length > 0) {
+ const starterProjects = cleanObject(data.starterProjects)
+ if (starterProjects && starterProjects.length > 0) {
+ result.starterProjects = starterProjects
+ }
+ }
+
+ // Add components if array not empty
+ if (data.components && data.components.length > 0) {
+ const components = cleanObject(data.components)
+ if (components && components.length > 0) {
+ result.components = components
+ }
+ }
+
+ // Add commands if array not empty
+ if (data.commands && data.commands.length > 0) {
+ const commands = cleanObject(data.commands)
+ if (commands && commands.length > 0) {
+ result.commands = commands
+ }
+ }
+
+ // Add events if any are defined
+ if (hasContent(data.events)) {
+ const events = {}
+ if (data.events.preStart && data.events.preStart.length > 0) {
+ events.preStart = data.events.preStart
+ }
+ if (data.events.postStart && data.events.postStart.length > 0) {
+ events.postStart = data.events.postStart
+ }
+ if (data.events.preStop && data.events.preStop.length > 0) {
+ events.preStop = data.events.preStop
+ }
+ if (data.events.postStop && data.events.postStop.length > 0) {
+ events.postStop = data.events.postStop
+ }
+ if (Object.keys(events).length > 0) {
+ result.events = events
+ }
+ }
+
+ // Add variables if any are defined
+ if (hasContent(data.variables)) {
+ result.variables = data.variables
+ }
+
+ // Add attributes if any are defined
+ if (hasContent(data.attributes)) {
+ const attributes = cleanObject(data.attributes)
+ if (attributes && Object.keys(attributes).length > 0) {
+ result.attributes = attributes
+ }
+ }
+
+ return result
+}
+
+export function validateDevfile(data) {
+ const errors = []
+
+ // Schema version is always required
+ if (!data.schemaVersion) {
+ errors.push('Schema version is required')
+ }
+
+ // Check for unique component names
+ if (data.components && data.components.length > 0) {
+ const names = data.components.map(c => c.name).filter(Boolean)
+ const duplicates = names.filter((name, index) => names.indexOf(name) !== index)
+ if (duplicates.length > 0) {
+ errors.push(`Duplicate component names: ${duplicates.join(', ')}`)
+ }
+ }
+
+ // Check for unique command IDs
+ if (data.commands && data.commands.length > 0) {
+ const ids = data.commands.map(c => c.id).filter(Boolean)
+ const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index)
+ if (duplicates.length > 0) {
+ errors.push(`Duplicate command IDs: ${duplicates.join(', ')}`)
+ }
+ }
+
+ // Check for unique project names
+ if (data.projects && data.projects.length > 0) {
+ const names = data.projects.map(p => p.name).filter(Boolean)
+ const duplicates = names.filter((name, index) => names.indexOf(name) !== index)
+ if (duplicates.length > 0) {
+ errors.push(`Duplicate project names: ${duplicates.join(', ')}`)
+ }
+ }
+
+ // Check that event-referenced commands exist
+ if (data.events) {
+ const commandIds = (data.commands || []).map(c => c.id).filter(Boolean)
+ const allEventCommands = [
+ ...(data.events.preStart || []),
+ ...(data.events.postStart || []),
+ ...(data.events.preStop || []),
+ ...(data.events.postStop || [])
+ ]
+ const invalidRefs = allEventCommands.filter(cmd => !commandIds.includes(cmd))
+ if (invalidRefs.length > 0) {
+ errors.push(`Event references non-existent commands: ${invalidRefs.join(', ')}`)
+ }
+ }
+
+ return errors
+}
diff --git a/src/utils/downloadFile.js b/src/utils/downloadFile.js
new file mode 100644
index 0000000..5cda38a
--- /dev/null
+++ b/src/utils/downloadFile.js
@@ -0,0 +1,22 @@
+export function downloadYamlFile(content, filename = 'devfile.yaml') {
+ // Create blob with YAML content
+ const blob = new Blob([content], { type: 'text/yaml;charset=utf-8' })
+
+ // Create temporary download link
+ const url = window.URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+
+ // Trigger download
+ document.body.appendChild(link)
+ link.click()
+
+ // Cleanup
+ document.body.removeChild(link)
+ window.URL.revokeObjectURL(url)
+}
+
+export function copyToClipboard(text) {
+ return navigator.clipboard.writeText(text)
+}
diff --git a/src/utils/validation.js b/src/utils/validation.js
new file mode 100644
index 0000000..10e7561
--- /dev/null
+++ b/src/utils/validation.js
@@ -0,0 +1,152 @@
+export const validators = {
+ required: (value) => {
+ if (!value || (typeof value === 'string' && value.trim() === '')) {
+ return 'This field is required'
+ }
+ return null
+ },
+
+ devfileName: (value) => {
+ if (!value) {
+ return 'Name is required'
+ }
+
+ const pattern = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/
+ if (!pattern.test(value)) {
+ return 'Name must be lowercase alphanumeric with hyphens only'
+ }
+
+ if (value.length > 63) {
+ return 'Name must be 63 characters or less'
+ }
+
+ return null
+ },
+
+ url: (value) => {
+ if (!value) return null // URL is optional
+
+ try {
+ new URL(value)
+ return null
+ } catch {
+ return 'Must be a valid URL (e.g., https://example.com)'
+ }
+ },
+
+ uniqueId: (value, existingIds = []) => {
+ if (!value) {
+ return 'ID is required'
+ }
+
+ if (existingIds.includes(value)) {
+ return 'ID must be unique'
+ }
+
+ return null
+ },
+
+ alphanumeric: (value) => {
+ if (!value) return null
+
+ const pattern = /^[a-zA-Z0-9_-]+$/
+ if (!pattern.test(value)) {
+ return 'Must contain only letters, numbers, underscores, and hyphens'
+ }
+
+ return null
+ }
+}
+
+export function validateMetadata(metadata) {
+ const errors = {}
+
+ const nameError = validators.devfileName(metadata.name)
+ if (nameError) errors.name = nameError
+
+ if (metadata.website) {
+ const websiteError = validators.url(metadata.website)
+ if (websiteError) errors.website = websiteError
+ }
+
+ if (metadata.supportUrl) {
+ const supportUrlError = validators.url(metadata.supportUrl)
+ if (supportUrlError) errors.supportUrl = supportUrlError
+ }
+
+ return errors
+}
+
+export function validateProject(project, existingNames = []) {
+ const errors = {}
+
+ if (!project.name) {
+ errors.name = 'Project name is required'
+ } else if (existingNames.includes(project.name)) {
+ errors.name = 'Project name must be unique'
+ }
+
+ const hasGit = project.git && project.git.remotes && project.git.remotes.origin
+ const hasZip = project.zip && project.zip.location
+
+ if (!hasGit && !hasZip) {
+ errors.source = 'Either Git or Zip source is required'
+ }
+
+ if (hasGit) {
+ const urlError = validators.url(project.git.remotes.origin)
+ if (urlError) errors.gitUrl = urlError
+ }
+
+ if (hasZip) {
+ const urlError = validators.url(project.zip.location)
+ if (urlError) errors.zipLocation = urlError
+ }
+
+ return errors
+}
+
+export function validateComponent(component, existingNames = []) {
+ const errors = {}
+
+ if (!component.name) {
+ errors.name = 'Component name is required'
+ } else if (existingNames.includes(component.name)) {
+ errors.name = 'Component name must be unique'
+ }
+
+ if (component.container && !component.container.image) {
+ errors.image = 'Container image is required'
+ }
+
+ if (component.volume && !component.volume.size) {
+ errors.size = 'Volume size is required'
+ }
+
+ return errors
+}
+
+export function validateCommand(command, existingIds = []) {
+ const errors = {}
+
+ if (!command.id) {
+ errors.id = 'Command ID is required'
+ } else if (existingIds.includes(command.id)) {
+ errors.id = 'Command ID must be unique'
+ }
+
+ if (command.exec) {
+ if (!command.exec.commandLine) {
+ errors.commandLine = 'Command line is required'
+ }
+ if (!command.exec.component) {
+ errors.component = 'Component reference is required'
+ }
+ }
+
+ if (command.composite && (!command.composite.commands || command.composite.commands.length === 0)) {
+ errors.commands = 'At least one command reference is required'
+ }
+
+ return errors
+}
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..7380cc2
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,20 @@
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,jsx}"
+ ],
+ theme: {
+ extend: {
+ colors: {
+ devfile: {
+ primary: '#4A90E2',
+ secondary: '#50E3C2',
+ accent: '#F5A623'
+ }
+ }
+ },
+ },
+ plugins: [
+ require('@tailwindcss/forms')
+ ],
+}
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..91faf9c
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,104 @@
+# Devfile GUI Wizard - Tests
+
+This directory contains end-to-end tests for the Devfile GUI Wizard using Playwright.
+
+## Running Tests
+
+### Prerequisites
+
+1. Install dependencies:
+ ```bash
+ npm install
+ ```
+
+2. Install Playwright browsers (first time only):
+ ```bash
+ npx playwright install
+ ```
+
+### Run Tests
+
+```bash
+# Run all tests in headless mode
+npm test
+
+# Run tests in UI mode (interactive)
+npm run test:ui
+
+# Run tests in debug mode
+npm run test:debug
+
+# Run specific test file
+npx playwright test quarkus-devfile.spec.js
+
+# Run tests in headed mode (see the browser)
+npx playwright test --headed
+```
+
+## Test Files
+
+### `quarkus-devfile.spec.js`
+
+Tests the generation of a Quarkus-like devfile through the wizard interface.
+
+**Test 1: Quarkus-like devfile generation**
+- Creates a simplified version of the [Quarkus API Example devfile](https://github.com/che-incubator/quarkus-api-example/blob/main/devfile.yaml)
+- Verifies:
+ - Schema version is 2.3.0
+ - Metadata is correctly set
+ - Components (tools container, postgresql container, m2 volume)
+ - Commands (package, startdev)
+ - Events (postStart: package)
+ - Schema compliance (no `type` field in components, no `kind` field in commands)
+
+**Test 2: Schema compliance**
+- Verifies that minimal devfiles comply with Devfile 2.3.0 specification
+- Ensures no deprecated fields are present in the generated YAML
+
+## Current Limitations
+
+The test generates a simplified version of the Quarkus devfile because the following features are not yet implemented in the UI:
+
+### Not Yet Supported:
+- `generateName` (uses `name` instead)
+- Component features:
+ - Environment variables (`env`)
+ - Endpoints
+ - Volume mounts (`volumeMounts`)
+ - Resource requests (`memoryRequest`, `cpuRequest`)
+- Command features:
+ - Labels
+ - Groups (`group.kind`, `group.isDefault`)
+- Root-level attributes
+
+These features will be added in future enhancements to match the full Quarkus devfile specification.
+
+## Test Output
+
+When tests run successfully, you'll see:
+- β
Test passed indicators
+- Generated YAML output in the console
+- Detailed test reports in `playwright-report/` directory
+
+## Debugging Tests
+
+To debug a failing test:
+
+1. Run in debug mode:
+ ```bash
+ npm run test:debug
+ ```
+
+2. Or use the Playwright Inspector:
+ ```bash
+ npx playwright test --debug
+ ```
+
+3. View the test report:
+ ```bash
+ npx playwright show-report
+ ```
+
+## CI/CD
+
+Tests are configured to run in CI environments. The configuration in `playwright.config.js` automatically detects CI environments and adjusts behavior accordingly (e.g., retries, parallel execution).
diff --git a/tests/quarkus-devfile.spec.js b/tests/quarkus-devfile.spec.js
new file mode 100644
index 0000000..f1611cf
--- /dev/null
+++ b/tests/quarkus-devfile.spec.js
@@ -0,0 +1,209 @@
+import { test, expect } from '@playwright/test'
+import yaml from 'js-yaml'
+
+/**
+ * This test generates a simplified version of the Quarkus API example devfile
+ * using the GUI wizard.
+ *
+ * Original devfile: https://github.com/che-incubator/quarkus-api-example/blob/main/devfile.yaml
+ *
+ * NOTE: This test uses 'name' instead of 'generateName' and doesn't include:
+ * - Component env variables, endpoints, volumeMounts, memoryRequest, cpuRequest
+ * - Command labels and groups (kind, isDefault)
+ * - Root-level attributes
+ *
+ * These features will be added to the UI in future enhancements.
+ */
+test.describe('Quarkus API Example Devfile Generation', () => {
+ test('should generate a basic Quarkus-like devfile through the wizard', async ({ page }) => {
+ // Navigate to the wizard
+ await page.goto('/')
+
+ // Wait for the wizard to load
+ await expect(page.locator('h2:has-text("Basic Information")')).toBeVisible()
+
+ // Step 1: Metadata
+ // NOTE: Using 'name' field since 'generateName' is not yet implemented in UI
+ await page.fill('input[name="name"]', 'quarkus-api-example')
+
+ // Navigate to next step
+ await page.click('button:has-text("Next")')
+
+ // Step 2: Projects - skip
+ await expect(page.locator('h2:has-text("Projects")')).toBeVisible()
+ await page.click('button:has-text("Next")')
+
+ // Step 3: Components
+ await expect(page.locator('h2:has-text("Components")')).toBeVisible()
+
+ // Component 1: tools container
+ await page.click('button:has-text("Add Component")')
+ await page.fill('input[name="component-name-0"]', 'tools')
+ await page.selectOption('select[name="component-type-0"]', 'container')
+ await page.fill('input[name="component-image-0"]', 'quay.io/devfile/universal-developer-image:ubi9-latest')
+ await page.fill('input[name="component-memoryLimit-0"]', '6G')
+ await page.fill('input[name="component-cpuLimit-0"]', '4000m')
+
+ // Component 2: postgresql container
+ await page.click('button:has-text("Add Component")')
+ await page.fill('input[name="component-name-1"]', 'postgresql')
+ await page.selectOption('select[name="component-type-1"]', 'container')
+ await page.fill('input[name="component-image-1"]', 'quay.io/centos7/postgresql-13-centos7@sha256:994f5c622e2913bda1c4a7fa3b0c7e7f75e7caa3ac66ff1ed70ccfe65c40dd75')
+
+ // Component 3: m2 volume
+ await page.click('button:has-text("Add Component")')
+ await page.fill('input[name="component-name-2"]', 'm2')
+ await page.selectOption('select[name="component-type-2"]', 'volume')
+ // Wait for the volume size field to appear after type change
+ await expect(page.locator('input[name="component-size-2"]')).toBeVisible()
+ await page.fill('input[name="component-size-2"]', '1G')
+
+ // Navigate to next step
+ await page.click('button:has-text("Next")')
+
+ // Step 4: Commands
+ await expect(page.locator('h2:has-text("Commands")')).toBeVisible()
+
+ // Command 1: package (build command)
+ await page.click('button:has-text("Add Command")')
+ await page.fill('input[name="command-id-0"]', 'package')
+ await page.selectOption('select[name="command-type-0"]', 'exec')
+ await page.selectOption('select[name="command-component-0"]', 'tools')
+ await page.fill('input[name="command-commandLine-0"]', './mvnw clean package -DskipTests=true')
+ await page.fill('input[name="command-workingDir-0"]', '${PROJECTS_ROOT}/quarkus-api-example')
+
+ // Command 2: startdev (run command)
+ await page.click('button:has-text("Add Command")')
+ await page.fill('input[name="command-id-1"]', 'startdev')
+ await page.selectOption('select[name="command-type-1"]', 'exec')
+ await page.selectOption('select[name="command-component-1"]', 'tools')
+ await page.fill('input[name="command-commandLine-1"]', './mvnw compile quarkus:dev')
+ await page.fill('input[name="command-workingDir-1"]', '${PROJECTS_ROOT}/quarkus-api-example')
+
+ // Navigate to next step
+ await page.click('button:has-text("Next")')
+
+ // Step 5: Events
+ await expect(page.locator('h2:has-text("Events")')).toBeVisible()
+
+ // Select package command for postStart event
+ // Find all checkboxes labeled "package" and select the one under "Post Start" (index 1)
+ await page.locator('label:has-text("package") input[type="checkbox"]').nth(1).check()
+
+ // Navigate to next step
+ await page.click('button:has-text("Next")')
+
+ // Step 6: Variables - skip
+ await expect(page.locator('h2:has-text("Variables")')).toBeVisible()
+ await page.click('button:has-text("Next")')
+
+ // Step 7: Review & Download
+ await expect(page.locator('h2:has-text("Review & Download")')).toBeVisible()
+
+ // Wait for YAML preview to be visible
+ const yamlPreview = page.locator('pre, code').first()
+ await expect(yamlPreview).toBeVisible()
+
+ // Get the generated YAML content from the preview
+ const yamlContent = await yamlPreview.textContent()
+
+ // Parse the YAML
+ const generatedDevfile = yaml.load(yamlContent)
+
+ // Verify basic structure
+ expect(generatedDevfile.schemaVersion).toBe('2.3.0')
+ expect(generatedDevfile.metadata.name).toBe('quarkus-api-example')
+
+ // Verify components
+ expect(generatedDevfile.components.length).toBeGreaterThanOrEqual(3)
+
+ const toolsComponent = generatedDevfile.components.find(c => c.name === 'tools')
+ expect(toolsComponent).toBeDefined()
+ expect(toolsComponent.container).toBeDefined()
+ expect(toolsComponent.container.image).toBe('quay.io/devfile/universal-developer-image:ubi9-latest')
+ expect(toolsComponent.container.memoryLimit).toBe('6G')
+ expect(toolsComponent.container.cpuLimit).toBe('4000m')
+
+ const postgresComponent = generatedDevfile.components.find(c => c.name === 'postgresql')
+ expect(postgresComponent).toBeDefined()
+ expect(postgresComponent.container).toBeDefined()
+ expect(postgresComponent.container.image).toContain('postgresql-13-centos7')
+
+ const m2Component = generatedDevfile.components.find(c => c.name === 'm2')
+ expect(m2Component).toBeDefined()
+ expect(m2Component.volume).toBeDefined()
+ expect(m2Component.volume.size).toBe('1G')
+
+ // Verify commands
+ expect(generatedDevfile.commands.length).toBeGreaterThanOrEqual(2)
+
+ const packageCmd = generatedDevfile.commands.find(c => c.id === 'package')
+ expect(packageCmd).toBeDefined()
+ expect(packageCmd.exec).toBeDefined()
+ expect(packageCmd.exec.component).toBe('tools')
+ expect(packageCmd.exec.commandLine).toContain('./mvnw clean package')
+ expect(packageCmd.exec.workingDir).toBe('${PROJECTS_ROOT}/quarkus-api-example')
+
+ const startdevCmd = generatedDevfile.commands.find(c => c.id === 'startdev')
+ expect(startdevCmd).toBeDefined()
+ expect(startdevCmd.exec).toBeDefined()
+ expect(startdevCmd.exec.commandLine).toContain('quarkus:dev')
+
+ // Verify events
+ expect(generatedDevfile.events).toBeDefined()
+ expect(generatedDevfile.events.postStart).toBeDefined()
+ expect(generatedDevfile.events.postStart).toContain('package')
+
+ // Verify schema compliance: no 'type' field in components
+ generatedDevfile.components.forEach(component => {
+ expect(component.type).toBeUndefined()
+ })
+
+ // Verify schema compliance: no 'kind' field in commands
+ generatedDevfile.commands.forEach(command => {
+ expect(command.kind).toBeUndefined()
+ })
+
+ console.log('β
Quarkus-like devfile generated successfully!')
+ console.log('\nGenerated YAML:')
+ console.log(yamlContent)
+ })
+
+ test('should verify devfile schema 2.3.0 compliance', async ({ page }) => {
+ // This test verifies that the generated devfile is compliant with schema 2.3.0
+ await page.goto('/')
+
+ // Fill minimal data
+ await page.fill('input[name="name"]', 'test-devfile')
+ await page.click('button:has-text("Next")') // Skip projects
+ await page.click('button:has-text("Next")') // Skip components
+ await page.click('button:has-text("Next")') // Skip commands
+ await page.click('button:has-text("Next")') // Skip events
+ await page.click('button:has-text("Next")') // Skip variables
+ await page.click('button:has-text("Next")') // Go to review
+
+ // Get generated YAML
+ const yamlPreview = page.locator('pre, code').first()
+ await expect(yamlPreview).toBeVisible()
+ const yamlContent = await yamlPreview.textContent()
+ const devfile = yaml.load(yamlContent)
+
+ // Verify required fields
+ expect(devfile.schemaVersion).toBe('2.3.0')
+ expect(devfile.metadata).toBeDefined()
+ expect(devfile.metadata.name).toBe('test-devfile')
+
+ // Verify that type/kind fields are not present
+ if (devfile.components) {
+ devfile.components.forEach(component => {
+ expect(component.type).toBeUndefined()
+ })
+ }
+
+ if (devfile.commands) {
+ devfile.commands.forEach(command => {
+ expect(command.kind).toBeUndefined()
+ })
+ }
+ })
+})
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..102d614
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ base: './',
+ build: {
+ outDir: 'public'
+ },
+ server: {
+ allowedHosts: 'all'
+ }
+})