diff --git a/.dockerignore b/.dockerignore
index 93c22e0..b6130d3 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -10,3 +10,5 @@ exports/*
!exports/.gitkeep
dist
.cache
+.env
+.env.*
diff --git a/.env.example b/.env.example
index 664ae02..15c2468 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,3 @@
-# Server Configuration
PORT=3001
NODE_ENV=development
-
-# Client Configuration
-VITE_API_URL=http://localhost:3001
+# CORS_ORIGIN=https://your-domain.com
diff --git a/.gitignore b/.gitignore
index d6de9f9..b5b4526 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,33 +1,16 @@
-# Dependencies
node_modules/
-*/node_modules/
-
-# Logs
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# Build outputs
dist/
-*/dist/
-
-# Environment
+npm-debug.log*
.env
.env.local
.env.*.local
-
-# OS
.DS_Store
Thumbs.db
-
-# IDE
.idea/
.vscode/
*.swp
*.swo
-
-# Uploads and exports (keep .gitkeep)
-server/uploads/*
-!server/uploads/.gitkeep
-server/exports/*
-!server/exports/.gitkeep
+uploads/*
+!uploads/.gitkeep
+exports/*
+!exports/.gitkeep
diff --git a/Dockerfile b/Dockerfile
index e936228..fbfe3b3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,54 +1,28 @@
-# Build stage
FROM node:20-alpine AS builder
+RUN apk add --no-cache cairo-dev pango-dev libjpeg-turbo-dev giflib-dev librsvg-dev pixman-dev python3 make g++
+
WORKDIR /app
-
-# Copy package files
COPY package*.json ./
-
-# Install all dependencies (including client devDependencies for build)
RUN npm install
-
-# Copy client source for build
-COPY client/ ./client/
-
-# Build the client
+COPY . .
RUN npm run build
-# Production stage
FROM node:20-alpine
-# Install system dependencies for node-canvas and sharp
-RUN apk add --no-cache \
- cairo-dev \
- pango-dev \
- libjpeg-turbo-dev \
- giflib-dev \
- librsvg-dev \
- pixman-dev \
- python3 \
- make \
- g++
+RUN apk add --no-cache cairo-dev pango-dev libjpeg-turbo-dev giflib-dev librsvg-dev pixman-dev python3 make g++
WORKDIR /app
-
-# Copy package files and install production dependencies only
COPY package*.json ./
-RUN npm install --production
+RUN npm install --omit=dev && apk del python3 make g++
-# Copy server source
-COPY server/ ./server/
+COPY server.js ./
+COPY --from=builder /app/dist ./dist
+RUN mkdir -p /app/uploads /app/exports
-# Copy built client from builder
-COPY --from=builder /app/client/dist ./server/dist
-
-# Create data directories
-RUN mkdir -p /app/server/uploads /app/server/exports
-
-# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/api/health || exit 1
EXPOSE 3001
-
-CMD ["node", "server/index.js"]
+ENV NODE_ENV=production
+CMD ["node", "server.js"]
diff --git a/README.md b/README.md
index faf4447..b10da5b 100644
--- a/README.md
+++ b/README.md
@@ -4,80 +4,75 @@ T-shirt customization editor with drag-and-drop design, background removal, and
## Features
-- **Canvas Editor** - React-Konva based drag/drop/resize/rotate for images and text
-- **Background Removal** - Client-side AI using Transformers.js (RMBG-1.4 model)
-- **Photo Pre-Editor** - Filerobot integration for crop, filters, and adjustments
-- **Stickers** - 40+ emoji stickers across 6 categories
-- **Text Tool** - Multiple fonts, sizes, and colors
-- **Templates** - 8 pre-designed templates across various categories
-- **Undo/Redo** - Full history with 50-state limit
-- **High-Res Export** - 4500x4500px PNG @ 300 DPI (15"x15" print size)
-- **PWA Support** - Offline caching for models, fonts, and assets
+- **Canvas Editor** — React-Konva based drag/drop/resize/rotate for images and text
+- **Background Removal** — Client-side AI using Transformers.js (RMBG-1.4, 8-bit quantized)
+- **Photo Pre-Editor** — Filerobot integration for crop, filters, and adjustments
+- **Stickers** — 140+ emoji stickers across 6 categories
+- **Text Tool** — 20 Google Fonts, sizes, and colors
+- **Templates** — 8 pre-designed templates across various categories
+- **Undo/Redo** — Full history with keyboard shortcuts (Ctrl+Z / Ctrl+Shift+Z)
+- **High-Res Export** — 4500×4500px PNG @ 300 DPI (15"×15" print size)
+- **PWA Support** — Offline caching for models, fonts, and assets
## Tech Stack
-**Frontend:**
-- React 19 + Vite
-- React-Konva (canvas)
-- Transformers.js (background removal)
-- Filerobot Image Editor
-- Workbox (PWA caching)
-
-**Backend:**
-- Express.js
-- Multer (file uploads)
-- Sharp (image processing)
-- Node-Canvas (high-res export)
+| Layer | Choice |
+|-------|--------|
+| Frontend | React 19, Vite, react-konva 19, Konva 10 |
+| Background Removal | @huggingface/transformers, RMBG-1.4 (q8) |
+| Photo Editor | Filerobot Image Editor |
+| PWA | Workbox via vite-plugin-pwa |
+| Server | Express, Multer, Sharp |
+| Export | node-canvas (4500×4500 server-side render) |
## Getting Started
-### Prerequisites
-
-- Node.js 18+
-- npm or yarn
-- Docker (optional, for containerized deployment)
-
-### Installation
-
```bash
-# Clone the repository
-git clone https://git.kadil.dev/khalidadil/apparel-designer.git
-cd apparel-designer
-
# Install dependencies
npm install
-# Start development servers (client on :3000, server on :3001)
+# Start development (client on :3000, server on :3001)
npm run dev
+
+# macOS Sharp fix is built into the dev script
+# On Windows use:
+npm run dev:win
```
-### Docker
+## Docker
```bash
-docker-compose up --build
+docker compose up --build
```
## Project Structure
```
apparel-designer/
-├── client/ # React frontend
-│ ├── src/
-│ │ ├── components/ # UI components
-│ │ │ ├── canvas/ # DesignCanvas, ImageElement, TextElement
-│ │ │ ├── sidebar/ # Tabs: Upload, Stickers, Text, Templates
-│ │ │ ├── panels/ # LayersPanel, PropertiesPanel
-│ │ │ └── editor/ # PhotoPreEditor (Filerobot)
-│ │ ├── hooks/ # useDesignEditor, useExport, useBackgroundRemoval
-│ │ └── constants/ # Templates, stickers data
-│ ├── vite.config.js # Vite + PWA config
-│ └── package.json
-├── server/
-│ ├── index.js # Express server
-│ ├── uploads/ # Uploaded files
-│ └── exports/ # Exported designs
-├── docker-compose.yml
-└── package.json
+├── server.js # Express API (upload, export, health)
+├── vite.config.js # Vite + PWA config
+├── package.json # Single package — all deps
+├── index.html # Entry HTML with Google Fonts
+├── src/
+│ ├── main.jsx # React entry + SW registration
+│ ├── App.jsx # Root layout (sidebar / canvas / properties)
+│ ├── App.css
+│ ├── index.css # Design tokens + layout styles
+│ ├── components/
+│ │ ├── canvas/ # DesignCanvas, ImageElement, TextElement, TShirtSVG, TemplateLayer, SlotPlaceholder
+│ │ ├── sidebar/ # Sidebar, UploadTab, StickersTab, TextTab, TemplatesTab, BackgroundRemovalButton
+│ │ ├── panels/ # LayersPanel, PropertiesPanel
+│ │ ├── editor/ # PhotoPreEditor (Filerobot wrapper)
+│ │ ├── PWAInstall.jsx
+│ │ └── OfflineIndicator.jsx
+│ ├── hooks/ # useDesignEditor, useBackgroundRemoval, useExport, useTemplate
+│ └── constants/ # fonts, stickers, templates
+├── public/ # Favicon, PWA icons
+├── uploads/ # User uploads (gitignored)
+├── exports/ # Exported PNGs (gitignored)
+├── docs/ # Template JSON schema
+├── Dockerfile
+└── docker-compose.yml
```
## API Endpoints
@@ -85,21 +80,9 @@ apparel-designer/
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/health` | Health check |
-| POST | `/api/upload` | Upload image (20MB max) |
-| POST | `/api/export` | Export design as PNG |
-
-## Build Plan Status
-
-- [x] Phase 1: Project Setup & Upload API
-- [x] Phase 2: Canvas Editor Core (react-konva)
-- [x] Phase 3: Sidebar & Properties Panel
-- [x] Phase 4: Background Removal (Transformers.js)
-- [x] Phase 5: Photo Pre-Editor (Filerobot)
-- [x] Phase 6: Template System
-- [x] Phase 7: Undo/Redo
-- [x] Phase 8: High-Resolution Export
-- [x] Phase 9: PWA & Workbox Caching
-- [x] Phase 10: Polish & QA
+| POST | `/api/upload` | Upload image (20MB max, JPEG/PNG/WebP) |
+| POST | `/api/export` | Export design as 4500×4500 PNG |
+| GET | `/api/download/:filename` | Download exported file |
## License
diff --git a/client/.gitignore b/client/.gitignore
deleted file mode 100644
index a547bf3..0000000
--- a/client/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/client/README.md b/client/README.md
deleted file mode 100644
index a36934d..0000000
--- a/client/README.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# React + Vite
-
-This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
-
-Currently, two official plugins are available:
-
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
-
-## React Compiler
-
-The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
-
-## Expanding the ESLint configuration
-
-If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
diff --git a/client/package-lock.json b/client/package-lock.json
deleted file mode 100644
index d3031f4..0000000
--- a/client/package-lock.json
+++ /dev/null
@@ -1,2617 +0,0 @@
-{
- "name": "client",
- "version": "0.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "client",
- "version": "0.0.0",
- "dependencies": {
- "react": "^19.2.5",
- "react-dom": "^19.2.5"
- },
- "devDependencies": {
- "@eslint/js": "^9.39.4",
- "@types/react": "^19.2.14",
- "@types/react-dom": "^19.2.3",
- "@vitejs/plugin-react": "^6.0.1",
- "eslint": "^9.39.4",
- "eslint-plugin-react-hooks": "^7.1.1",
- "eslint-plugin-react-refresh": "^0.5.2",
- "globals": "^17.5.0",
- "vite": "^8.0.9"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
- "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.28.5",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
- "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
- "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.0",
- "@babel/generator": "^7.29.0",
- "@babel/helper-compilation-targets": "^7.28.6",
- "@babel/helper-module-transforms": "^7.28.6",
- "@babel/helpers": "^7.28.6",
- "@babel/parser": "^7.29.0",
- "@babel/template": "^7.28.6",
- "@babel/traverse": "^7.29.0",
- "@babel/types": "^7.29.0",
- "@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.29.1",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
- "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.0",
- "@babel/types": "^7.29.0",
- "@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.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
- "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.28.6",
- "@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.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
- "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
- "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-validator-identifier": "^7.28.5",
- "@babel/traverse": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.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.29.2",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
- "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.29.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.29.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
- "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.0"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
- "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
- "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.0",
- "@babel/generator": "^7.29.0",
- "@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.29.0",
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.29.0",
- "debug": "^4.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
- "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
- "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/@emnapi/core": {
- "version": "1.9.2",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
- "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.1",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.9.2",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
- "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
- "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@eslint-community/eslint-utils": {
- "version": "4.9.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
- "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eslint-visitor-keys": "^3.4.3"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- },
- "peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
- }
- },
- "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint-community/regexpp": {
- "version": "4.12.2",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
- "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
- }
- },
- "node_modules/@eslint/config-array": {
- "version": "0.21.2",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
- "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/object-schema": "^2.1.7",
- "debug": "^4.3.1",
- "minimatch": "^3.1.5"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/config-helpers": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
- "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.17.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/core": {
- "version": "0.17.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
- "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/eslintrc": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
- "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "^6.14.0",
- "debug": "^4.3.2",
- "espree": "^10.0.1",
- "globals": "^14.0.0",
- "ignore": "^5.2.0",
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.1",
- "minimatch": "^3.1.5",
- "strip-json-comments": "^3.1.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint/eslintrc/node_modules/globals": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@eslint/js": {
- "version": "9.39.4",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
- "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- }
- },
- "node_modules/@eslint/object-schema": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
- "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/plugin-kit": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
- "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.17.0",
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@humanfs/core": {
- "version": "0.19.2",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
- "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@humanfs/types": "^0.15.0"
- },
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node": {
- "version": "0.16.8",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
- "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@humanfs/core": "^0.19.2",
- "@humanfs/types": "^0.15.0",
- "@humanwhocodes/retry": "^0.4.0"
- },
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/types": {
- "version": "0.15.0",
- "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
- "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanwhocodes/module-importer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
- "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.22"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/retry": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
- "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "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/@napi-rs/wasm-runtime": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
- "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@tybys/wasm-util": "^0.10.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- },
- "peerDependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1"
- }
- },
- "node_modules/@oxc-project/types": {
- "version": "0.126.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
- "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz",
- "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz",
- "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz",
- "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz",
- "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz",
- "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz",
- "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz",
- "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz",
- "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz",
- "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz",
- "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz",
- "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz",
- "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz",
- "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "1.9.2",
- "@emnapi/runtime": "1.9.2",
- "@napi-rs/wasm-runtime": "^1.1.4"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz",
- "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz",
- "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.7",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
- "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tybys/wasm-util": {
- "version": "0.10.1",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
- "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "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/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/react": {
- "version": "19.2.14",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
- "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.2.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "19.2.3",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
- "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "^19.2.0"
- }
- },
- "node_modules/@vitejs/plugin-react": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
- "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@rolldown/pluginutils": "1.0.0-rc.7"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "peerDependencies": {
- "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
- "babel-plugin-react-compiler": "^1.0.0",
- "vite": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "@rolldown/plugin-babel": {
- "optional": true
- },
- "babel-plugin-react-compiler": {
- "optional": true
- }
- }
- },
- "node_modules/acorn": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
- "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/ajv": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
- "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "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==",
- "dev": true,
- "license": "Python-2.0"
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/baseline-browser-mapping": {
- "version": "2.10.20",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
- "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "baseline-browser-mapping": "dist/cli.cjs"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/browserslist": {
- "version": "4.28.2",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
- "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
- "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.10.12",
- "caniuse-lite": "^1.0.30001782",
- "electron-to-chromium": "^1.5.328",
- "node-releases": "^2.0.36",
- "update-browserslist-db": "^1.2.3"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001788",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
- "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
- "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/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
- "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/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/csstype": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
- "license": "MIT"
- },
- "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/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.340",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz",
- "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==",
- "dev": true,
- "license": "ISC"
- },
- "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/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint": {
- "version": "9.39.4",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
- "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.8.0",
- "@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.21.2",
- "@eslint/config-helpers": "^0.4.2",
- "@eslint/core": "^0.17.0",
- "@eslint/eslintrc": "^3.3.5",
- "@eslint/js": "9.39.4",
- "@eslint/plugin-kit": "^0.4.1",
- "@humanfs/node": "^0.16.6",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.4.2",
- "@types/estree": "^1.0.6",
- "ajv": "^6.14.0",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.4.0",
- "eslint-visitor-keys": "^4.2.1",
- "espree": "^10.4.0",
- "esquery": "^1.5.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.5",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "jiti": "*"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-plugin-react-hooks": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
- "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.24.4",
- "@babel/parser": "^7.24.4",
- "hermes-parser": "^0.25.1",
- "zod": "^3.25.0 || ^4.0.0",
- "zod-validation-error": "^3.5.0 || ^4.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
- }
- },
- "node_modules/eslint-plugin-react-refresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
- "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "eslint": "^9 || ^10"
- }
- },
- "node_modules/eslint-scope": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
- "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/espree": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
- "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "acorn": "^8.15.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/esquery": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
- "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "estraverse": "^5.1.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true,
- "license": "MIT"
- },
- "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/file-entry-cache": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flat-cache": "^4.0.0"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
- },
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/flatted": {
- "version": "3.4.2",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
- "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
- "dev": true,
- "license": "ISC"
- },
- "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/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/globals": {
- "version": "17.5.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz",
- "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/hermes-estree": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
- "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/hermes-parser": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
- "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "hermes-estree": "0.25.1"
- }
- },
- "node_modules/ignore": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/import-fresh": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
- "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "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/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
- "license": "ISC"
- },
- "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==",
- "dev": true,
- "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==",
- "dev": true,
- "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/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true,
- "license": "MIT"
- },
- "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/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "json-buffer": "3.0.1"
- }
- },
- "node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/lightningcss": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
- "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
- "dev": true,
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.32.0",
- "lightningcss-darwin-arm64": "1.32.0",
- "lightningcss-darwin-x64": "1.32.0",
- "lightningcss-freebsd-x64": "1.32.0",
- "lightningcss-linux-arm-gnueabihf": "1.32.0",
- "lightningcss-linux-arm64-gnu": "1.32.0",
- "lightningcss-linux-arm64-musl": "1.32.0",
- "lightningcss-linux-x64-gnu": "1.32.0",
- "lightningcss-linux-x64-musl": "1.32.0",
- "lightningcss-win32-arm64-msvc": "1.32.0",
- "lightningcss-win32-x64-msvc": "1.32.0"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
- "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
- "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
- "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
- "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
- "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
- "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
- "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
- "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
- "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
- "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
- "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true,
- "license": "MIT"
- },
- "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/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "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/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/natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/node-releases": {
- "version": "2.0.37",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
- "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/optionator": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
- "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.5"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "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": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.10",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
- "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
- "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/prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/react": {
- "version": "19.2.5",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
- "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "19.2.5",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
- "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
- "license": "MIT",
- "dependencies": {
- "scheduler": "^0.27.0"
- },
- "peerDependencies": {
- "react": "^19.2.5"
- }
- },
- "node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/rolldown": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz",
- "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "=0.126.0",
- "@rolldown/pluginutils": "1.0.0-rc.16"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.0.0-rc.16",
- "@rolldown/binding-darwin-arm64": "1.0.0-rc.16",
- "@rolldown/binding-darwin-x64": "1.0.0-rc.16",
- "@rolldown/binding-freebsd-x64": "1.0.0-rc.16",
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16",
- "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16",
- "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16",
- "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16",
- "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16",
- "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16",
- "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16",
- "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16",
- "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16",
- "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16",
- "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16"
- }
- },
- "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.16",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz",
- "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/scheduler": {
- "version": "0.27.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
- "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
- "license": "MIT"
- },
- "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/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "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/strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/tinyglobby": {
- "version": "0.2.16",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
- "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
- "license": "0BSD",
- "optional": true
- },
- "node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.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/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
- "node_modules/vite": {
- "version": "8.0.9",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
- "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "lightningcss": "^1.32.0",
- "picomatch": "^4.0.4",
- "postcss": "^8.5.10",
- "rolldown": "1.0.0-rc.16",
- "tinyglobby": "^0.2.16"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.1.0",
- "esbuild": "^0.27.0 || ^0.28.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "@vitejs/devtools": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/word-wrap": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
- "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "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"
- },
- "node_modules/yocto-queue": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/zod": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
- "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
- },
- "node_modules/zod-validation-error": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
- "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18.0.0"
- },
- "peerDependencies": {
- "zod": "^3.25.0 || ^4.0.0"
- }
- }
- }
-}
diff --git a/client/public/favicon.svg b/client/public/favicon.svg
deleted file mode 100644
index 6893eb1..0000000
--- a/client/public/favicon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/client/public/icons.svg b/client/public/icons.svg
deleted file mode 100644
index e952219..0000000
--- a/client/public/icons.svg
+++ /dev/null
@@ -1,24 +0,0 @@
-
diff --git a/client/public/pwa-192x192.svg b/client/public/pwa-192x192.svg
deleted file mode 100644
index 13d04d5..0000000
--- a/client/public/pwa-192x192.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/client/public/pwa-512x512.svg b/client/public/pwa-512x512.svg
deleted file mode 100644
index f2dbd73..0000000
--- a/client/public/pwa-512x512.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/client/src/App.css b/client/src/App.css
deleted file mode 100644
index f90339d..0000000
--- a/client/src/App.css
+++ /dev/null
@@ -1,184 +0,0 @@
-.counter {
- font-size: 16px;
- padding: 5px 10px;
- border-radius: 5px;
- color: var(--accent);
- background: var(--accent-bg);
- border: 2px solid transparent;
- transition: border-color 0.3s;
- margin-bottom: 24px;
-
- &:hover {
- border-color: var(--accent-border);
- }
- &:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
- }
-}
-
-.hero {
- position: relative;
-
- .base,
- .framework,
- .vite {
- inset-inline: 0;
- margin: 0 auto;
- }
-
- .base {
- width: 170px;
- position: relative;
- z-index: 0;
- }
-
- .framework,
- .vite {
- position: absolute;
- }
-
- .framework {
- z-index: 1;
- top: 34px;
- height: 28px;
- transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
- scale(1.4);
- }
-
- .vite {
- z-index: 0;
- top: 107px;
- height: 26px;
- width: auto;
- transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
- scale(0.8);
- }
-}
-
-#center {
- display: flex;
- flex-direction: column;
- gap: 25px;
- place-content: center;
- place-items: center;
- flex-grow: 1;
-
- @media (max-width: 1024px) {
- padding: 32px 20px 24px;
- gap: 18px;
- }
-}
-
-#next-steps {
- display: flex;
- border-top: 1px solid var(--border);
- text-align: left;
-
- & > div {
- flex: 1 1 0;
- padding: 32px;
- @media (max-width: 1024px) {
- padding: 24px 20px;
- }
- }
-
- .icon {
- margin-bottom: 16px;
- width: 22px;
- height: 22px;
- }
-
- @media (max-width: 1024px) {
- flex-direction: column;
- text-align: center;
- }
-}
-
-#docs {
- border-right: 1px solid var(--border);
-
- @media (max-width: 1024px) {
- border-right: none;
- border-bottom: 1px solid var(--border);
- }
-}
-
-#next-steps ul {
- list-style: none;
- padding: 0;
- display: flex;
- gap: 8px;
- margin: 32px 0 0;
-
- .logo {
- height: 18px;
- }
-
- a {
- color: var(--text-h);
- font-size: 16px;
- border-radius: 6px;
- background: var(--social-bg);
- display: flex;
- padding: 6px 12px;
- align-items: center;
- gap: 8px;
- text-decoration: none;
- transition: box-shadow 0.3s;
-
- &:hover {
- box-shadow: var(--shadow);
- }
- .button-icon {
- height: 18px;
- width: 18px;
- }
- }
-
- @media (max-width: 1024px) {
- margin-top: 20px;
- flex-wrap: wrap;
- justify-content: center;
-
- li {
- flex: 1 1 calc(50% - 8px);
- }
-
- a {
- width: 100%;
- justify-content: center;
- box-sizing: border-box;
- }
- }
-}
-
-#spacer {
- height: 88px;
- border-top: 1px solid var(--border);
- @media (max-width: 1024px) {
- height: 48px;
- }
-}
-
-.ticks {
- position: relative;
- width: 100%;
-
- &::before,
- &::after {
- content: '';
- position: absolute;
- top: -4.5px;
- border: 5px solid transparent;
- }
-
- &::before {
- left: 0;
- border-left-color: var(--border);
- }
- &::after {
- right: 0;
- border-right-color: var(--border);
- }
-}
diff --git a/client/src/App.jsx b/client/src/App.jsx
deleted file mode 100644
index 7ce0d97..0000000
--- a/client/src/App.jsx
+++ /dev/null
@@ -1,294 +0,0 @@
-import { useEffect, useState } from 'react';
-import { DesignCanvas } from './components/canvas/DesignCanvas';
-import { Sidebar } from './components/sidebar/Sidebar';
-import { LayersPanel } from './components/panels/LayersPanel';
-import { PropertiesPanel } from './components/panels/PropertiesPanel';
-import { PWAInstall } from './components/PWAInstall';
-import { OfflineIndicator } from './components/OfflineIndicator';
-import { PhotoPreEditor } from './components/editor/PhotoPreEditor';
-import { useDesignEditor } from './hooks/useDesignEditor';
-import { useExport } from './hooks/useExport';
-import { useTemplate } from './hooks/useTemplate';
-import { TEMPLATES } from './constants/templates';
-
-function App() {
- const [editingElement, setEditingElement] = useState(null);
-
- const {
- elements,
- selectedId,
- addElement,
- updateElement,
- deleteElement,
- selectElement,
- deselectAll,
- commitHistory,
- undo,
- redo,
- canUndo,
- canRedo,
- initializeHistory,
- } = useDesignEditor();
-
- const { exporting, progress, exportDesign, error, clearExport } = useExport();
-
- // Template management
- const {
- currentTemplate,
- currentTemplateId,
- assignedSlots,
- loadTemplate,
- clearTemplate,
- getSlots,
- assignImageToSlot,
- getDragBoundFunc,
- isSlotFilled,
- } = useTemplate(TEMPLATES);
-
- const selectedElement = elements.find((el) => el.id === selectedId);
-
- // Initialize history on mount
- useEffect(() => {
- initializeHistory();
- }, [initializeHistory]);
-
- // Keyboard shortcuts
- useEffect(() => {
- const handleKeyDown = (e) => {
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
- return;
- }
-
- // Undo: Ctrl/Cmd + Z
- if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
- e.preventDefault();
- if (canUndo) undo();
- return;
- }
-
- // Redo: Ctrl/Cmd + Shift + Z or Ctrl/Cmd + Y
- if ((e.ctrlKey || e.metaKey) && ((e.key === 'z' && e.shiftKey) || e.key === 'y')) {
- e.preventDefault();
- if (canRedo) redo();
- return;
- }
-
- // Delete/Backspace removes selected element
- if (e.key === 'Delete' || e.key === 'Backspace') {
- if (selectedId) {
- deleteElement(selectedId);
- }
- }
- };
-
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [selectedId, deleteElement, undo, redo, canUndo, canRedo]);
-
- // Handler callbacks for sidebar tabs
- const handleAddImage = (imageData) => {
- addElement(imageData);
- };
-
- const handleAddSticker = (stickerData) => {
- addElement(stickerData);
- };
-
- const handleAddText = (textData) => {
- addElement(textData);
- };
-
- const handleAddTemplate = (templateId) => {
- if (templateId === 'freeform') {
- clearTemplate();
- return;
- }
-
- // Load template using useTemplate hook
- const success = loadTemplate(templateId);
- if (success) {
- const template = TEMPLATES.find(t => t.id === templateId);
- // Clear existing elements first
- // Apply template elements to canvas
- if (template?.elements) {
- template.elements.forEach((el, index) => {
- setTimeout(() => addElement({ ...el }), index * 50);
- });
- }
- }
- };
-
- // Handle image upload for slot-based templates
- const handleSlotImageUpload = (slotId, imageData) => {
- const elementData = assignImageToSlot(slotId, imageData);
- if (elementData) {
- addElement(elementData);
- }
- };
-
- // Handle photo editing
- const handleEditPhoto = (element) => {
- setEditingElement(element);
- };
-
- const handlePhotoEditComplete = (editedImageUrl) => {
- if (editingElement) {
- updateElement(editingElement.id, { src: editedImageUrl });
- }
- setEditingElement(null);
- };
-
- const handlePhotoEditClose = () => {
- setEditingElement(null);
- };
-
- return (
-
- {/* Offline Indicator */}
-
-
- {/* PWA Install Prompt */}
-
-
- {/* Left Sidebar */}
-
-
- {/* Center Canvas Area */}
-
-
-
- Apparel Designer
-
-
- T-shirt customization editor
-
-
-
- {/* Action buttons */}
-
-
-
-
-
-
- {/* Export error banner */}
- {error && (
-
- ⚠️ Export failed: {error}
-
-
- )}
-
-
updateElement(id, attrs)}
- onCommit={commitHistory}
- currentTemplate={currentTemplate}
- assignedSlots={assignedSlots}
- getDragBoundFunc={getDragBoundFunc}
- />
-
- {/* Layers panel below canvas */}
-
-
-
-
-
- {/* Right Properties Panel */}
-
updateElement(selectedId, attrs)}
- onDelete={deleteElement}
- onEditPhoto={handleEditPhoto}
- />
-
- {/* Photo Pre-Editor Modal */}
- {editingElement && (
-
- )}
-
- );
-}
-
-export default App;
diff --git a/client/src/assets/hero.png b/client/src/assets/hero.png
deleted file mode 100644
index 02251f4..0000000
Binary files a/client/src/assets/hero.png and /dev/null differ
diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/client/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/client/src/assets/vite.svg b/client/src/assets/vite.svg
deleted file mode 100644
index 5101b67..0000000
--- a/client/src/assets/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/client/src/components/PWAInstall.jsx b/client/src/components/PWAInstall.jsx
deleted file mode 100644
index 3cc41ad..0000000
--- a/client/src/components/PWAInstall.jsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import { useState, useEffect } from 'react';
-
-export function PWAInstall() {
- const [deferredPrompt, setDeferredPrompt] = useState(null);
- const [showInstall, setShowInstall] = useState(false);
- const [updateAvailable, setUpdateAvailable] = useState(false);
- const [newWorker, setNewWorker] = useState(null);
-
- useEffect(() => {
- const handleBeforeInstallPrompt = (e) => {
- e.preventDefault();
- setDeferredPrompt(e);
- setShowInstall(true);
- };
-
- window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
-
- // Listen for service worker updates
- window.addEventListener('swUpdated', handleSWUpdated);
-
- return () => {
- window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
- window.removeEventListener('swUpdated', handleSWUpdated);
- };
- }, []);
-
- const handleSWUpdated = (event) => {
- setNewWorker(event.detail);
- setUpdateAvailable(true);
- };
-
- const handleInstall = async () => {
- if (!deferredPrompt) return;
-
- deferredPrompt.prompt();
- const { outcome } = await deferredPrompt.userChoice;
-
- if (outcome === 'accepted') {
- setShowInstall(false);
- setDeferredPrompt(null);
- }
- };
-
- const handleUpdate = () => {
- if (newWorker) {
- newWorker.postMessage({ type: 'SKIP_WAITING' });
- window.location.reload();
- }
- };
-
- const dismissUpdate = () => {
- setUpdateAvailable(false);
- setNewWorker(null);
- };
-
- if (!showInstall && !updateAvailable) return null;
-
- return (
- <>
- {showInstall && (
-
-
Install Apparel Designer for offline access!
-
-
-
-
-
- )}
-
- {updateAvailable && (
-
- 🔄 New version available!
-
-
-
- )}
- >
- );
-}
diff --git a/client/src/components/canvas/DesignCanvas.jsx b/client/src/components/canvas/DesignCanvas.jsx
deleted file mode 100644
index 0f5ddc1..0000000
--- a/client/src/components/canvas/DesignCanvas.jsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import { Stage, Layer } from 'react-konva';
-import { TShirtSVG } from './TShirtSVG';
-import { ImageElement } from './ImageElement';
-import { TextElement } from './TextElement';
-import { TemplateLayer } from './TemplateLayer';
-import { SlotPlaceholder, SlotBoundsGuide } from './SlotPlaceholder';
-import { useRef, useEffect, memo } from 'react';
-
-const CANVAS_SIZE = 300;
-
-export const DesignCanvas = memo(function DesignCanvas({
- elements,
- selectedId,
- onSelect,
- onDeselect,
- onUpdate,
- onCommit,
- currentTemplate,
- assignedSlots,
- getDragBoundFunc,
-}) {
- // Get slots from current template
- const slots = currentTemplate?.slots || [];
-
- return (
-
- {/* T-shirt SVG background */}
-
-
- {/* Canvas Stage */}
-
- {/* Template Layer - Background and Overlay */}
-
- {currentTemplate && (
-
- )}
-
-
- {/* Slot Bounds Guides - visible even when slots have content */}
-
- {slots.map((slot) => (
-
- ))}
-
-
- {/* User Elements Layer */}
-
- {elements.map((el) => {
- if (el.type === 'image') {
- return (
- onSelect(el.id)}
- onUpdate={(attrs) => onUpdate(el.id, attrs)}
- onCommit={onCommit}
- dragBoundFunc={el.slotId ? getDragBoundFunc?.(el.slotId, { width: el.width, height: el.height }) : null}
- />
- );
- }
- if (el.type === 'text') {
- return (
- onSelect(el.id)}
- onUpdate={(attrs) => onUpdate(el.id, attrs)}
- onCommit={onCommit}
- />
- );
- }
- return null;
- })}
-
-
- {/* Slot Placeholders - show when slots are empty */}
-
- {slots.map((slot) => {
- const isFilled = !!assignedSlots?.[slot.id];
- return (
-
- );
- })}
-
-
-
- {/* Canvas info bar */}
-
- Design Area: 15" × 15" • Export: 4500 × 4500px @ 300 DPI
-
-
- );
-});
diff --git a/client/src/components/canvas/SlotPlaceholder.jsx b/client/src/components/canvas/SlotPlaceholder.jsx
deleted file mode 100644
index ccedd9d..0000000
--- a/client/src/components/canvas/SlotPlaceholder.jsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { Group, Rect, Text, Line } from 'react-konva';
-
-/**
- * SlotPlaceholder - Visual indicator for empty template slots
- * Shows a dashed border with label when slot is empty
- *
- * @param {Object} slot - Slot configuration
- * @param {string} slot.id - Unique slot identifier
- * @param {Object} slot.bounds - Slot bounds {x, y, width, height}
- * @param {string} slot.label - Human-readable label
- * @param {boolean} isEmpty - Whether slot has no image assigned
- */
-export function SlotPlaceholder({ slot, isEmpty = true }) {
- const { bounds, label } = slot;
- const { x, y, width, height } = bounds;
-
- if (!isEmpty) return null; // Don't show placeholder when slot has content
-
- return (
-
- {/* Dashed border rectangle */}
-
-
- {/* Background fill (semi-transparent) */}
-
-
- {/* Drop icon */}
-
-
- {/* Label text */}
-
-
- {/* Corner markers */}
-
-
-
-
-
-
-
-
-
- );
-}
-
-/**
- * SlotBoundsGuide - Shows the slot boundary during design
- * Less prominent than placeholder, visible even when slot has content
- *
- * @param {Object} slot - Slot configuration
- * @param {Object} slot.bounds - Slot bounds {x, y, width, height}
- * @param {string} slot.id - Slot identifier
- */
-export function SlotBoundsGuide({ slot }) {
- const { bounds, id } = slot;
- const { x, y, width, height } = bounds;
-
- return (
-
-
-
- );
-}
diff --git a/client/src/components/canvas/TShirtSVG.jsx b/client/src/components/canvas/TShirtSVG.jsx
deleted file mode 100644
index 2055ab0..0000000
--- a/client/src/components/canvas/TShirtSVG.jsx
+++ /dev/null
@@ -1,59 +0,0 @@
-export function TShirtSVG({ size = 300 }) {
- const padding = size * 0.1;
- const innerSize = size - padding * 2;
-
- return (
-
- );
-}
diff --git a/client/src/components/canvas/TemplateLayer.jsx b/client/src/components/canvas/TemplateLayer.jsx
deleted file mode 100644
index 0548c40..0000000
--- a/client/src/components/canvas/TemplateLayer.jsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { Group, Image as KonvaImage, Rect, Text as KonvaText } from 'react-konva';
-import useImage from 'use-image';
-
-// Helper component to load and render images
-function TemplateImage({ src, x, y, width, height, opacity = 1, listening = false }) {
- const [img] = useImage(src, 'anonymous');
-
- return (
-
- );
-}
-
-// Helper component for text elements
-function TemplateText({ text, x, y, fontSize, fontFamily, fill, rotation = 0 }) {
- return (
-
- );
-}
-
-/**
- * TemplateLayer - Renders background and overlay layers for templates
- * Background: Base image/color that appears behind user elements
- * Overlay: Decorative elements that appear on top of user elements
- *
- * @param {Object} template - Template configuration object
- * @param {Object} template.background - Background layer config
- * @param {Array} template.overlay - Overlay elements array
- * @param {number} canvasSize - Size of the canvas (default: 300)
- */
-export function TemplateLayer({ template, canvasSize = 300 }) {
- if (!template) return null;
-
- const { background, overlay } = template;
-
- return (
-
- {/* Background Layer */}
- {background && (
-
- {background.type === 'color' ? (
-
- ) : background.type === 'image' ? (
-
- ) : null}
-
- )}
-
- {/* Overlay Layer */}
- {overlay && overlay.map((el, index) => {
- if (el.nonPrintable) return null; // Skip guides and watermarks
-
- const key = `overlay-${index}`;
-
- if (el.type === 'image') {
- return (
-
- );
- }
-
- if (el.type === 'text') {
- return (
-
- );
- }
-
- return null;
- })}
-
- );
-}
diff --git a/client/src/components/editor/PhotoPreEditor.jsx b/client/src/components/editor/PhotoPreEditor.jsx
deleted file mode 100644
index dfa9487..0000000
--- a/client/src/components/editor/PhotoPreEditor.jsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { useState, useEffect, useRef } from 'react';
-import FilerobotImageEditor from 'react-filerobot-image-editor';
-
-export function PhotoPreEditor({ imageSrc, onComplete, onClose, triggerElementRef }) {
- const [saving, setSaving] = useState(false);
- const modalContentRef = useRef(null);
- const previousFocusRef = useRef(null);
-
- // Focus management - trap focus inside modal and restore on close
- useEffect(() => {
- // Store the element that had focus before modal opened
- previousFocusRef.current = document.activeElement;
-
- // Focus the modal content when it opens
- const focusableElement = modalContentRef.current?.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
- focusableElement?.focus();
-
- // Focus trap handler
- const handleKeyDown = (e) => {
- if (e.key === 'Escape') {
- onClose();
- return;
- }
-
- if (e.key === 'Tab') {
- const modalContent = modalContentRef.current;
- if (!modalContent) return;
-
- const focusableElements = modalContent.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
- const firstElement = focusableElements[0];
- const lastElement = focusableElements[focusableElements.length - 1];
-
- if (e.shiftKey && document.activeElement === firstElement) {
- e.preventDefault();
- lastElement.focus();
- } else if (!e.shiftKey && document.activeElement === lastElement) {
- e.preventDefault();
- firstElement.focus();
- }
- }
- };
-
- document.addEventListener('keydown', handleKeyDown);
-
- return () => {
- document.removeEventListener('keydown', handleKeyDown);
- // Restore focus to the element that triggered the modal
- previousFocusRef.current?.focus();
- };
- }, [onClose]);
-
- const handleComplete = (editedImageObject, designState) => {
- setSaving(true);
-
- // Export the edited image
- editedImageObject.exportAsync({
- quality: 1,
- mimeType: 'image/png',
- }).then((blob) => {
- const url = URL.createObjectURL(blob);
- setSaving(false);
- onComplete(url);
- }).catch((error) => {
- console.error('Export failed:', error);
- setSaving(false);
- onClose();
- });
- };
-
- return (
-
-
-
-
-
- Photo Editor
-
-
- );
-}
diff --git a/client/src/components/panels/LayersPanel.jsx b/client/src/components/panels/LayersPanel.jsx
deleted file mode 100644
index ff3979c..0000000
--- a/client/src/components/panels/LayersPanel.jsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import { memo } from 'react';
-
-export const LayersPanel = memo(function LayersPanel({ elements, selectedId, onSelect, onDelete }) {
- const getIcon = (element) => {
- switch (element.type) {
- case 'image':
- return element.bgRemoved ? '🖼️' : '📷';
- case 'text':
- return '📝';
- case 'sticker':
- return '🎨';
- default:
- return '📁';
- }
- };
-
- const getName = (element) => {
- switch (element.type) {
- case 'image':
- return element.bgRemoved ? 'Image (BG ✓)' : 'Image';
- case 'text':
- return element.text?.substring(0, 20) || 'Text';
- case 'sticker':
- return 'Sticker';
- default:
- return 'Element';
- }
- };
-
- if (elements.length === 0) {
- return (
-
- No elements yet. Add images, text, or stickers to your design.
-
- );
- }
-
- return (
-
-
- Layers ({elements.length})
-
-
-
- {elements.map((element, index) => (
-
onSelect(element.id)}
- style={{
- display: 'flex',
- alignItems: 'center',
- gap: '0.5rem',
- padding: '0.5rem 0.75rem',
- background: selectedId === element.id ? 'var(--accent-bg)' : 'transparent',
- border: `1px solid ${selectedId === element.id ? 'var(--accent)' : 'var(--border)'}`,
- borderRadius: 'var(--radius-sm)',
- cursor: 'pointer',
- transition: 'all 0.15s ease',
- }}
- onMouseEnter={(e) => {
- if (selectedId !== element.id) {
- e.target.style.borderColor = 'var(--accent)';
- }
- }}
- onMouseLeave={(e) => {
- if (selectedId !== element.id) {
- e.target.style.borderColor = 'var(--border)';
- }
- }}
- >
- {getIcon(element)}
-
- {getName(element)}
-
-
-
- ))}
-
-
- );
-});
diff --git a/client/src/components/panels/PropertiesPanel.jsx b/client/src/components/panels/PropertiesPanel.jsx
deleted file mode 100644
index e436984..0000000
--- a/client/src/components/panels/PropertiesPanel.jsx
+++ /dev/null
@@ -1,314 +0,0 @@
-import { memo } from 'react';
-
-export const PropertiesPanel = memo(function PropertiesPanel({ element, onUpdate, onDelete, onEditPhoto }) {
- if (!element) {
- return (
-
-
-
- Properties
-
-
-
-
- Select an element to edit its properties
-
-
- );
- }
-
- const handlePositionChange = (axis, value) => {
- onUpdate({ [axis]: parseFloat(value) || 0 });
- };
-
- const handleSizeChange = (axis, value) => {
- const numValue = parseFloat(value) || 20;
- onUpdate({ [axis]: Math.max(20, numValue) });
- };
-
- const handleRotationChange = (value) => {
- const numValue = parseFloat(value) || 0;
- onUpdate({ rotation: Math.max(-180, Math.min(180, numValue)) });
- };
-
- return (
-
-
-
- Properties
-
-
-
-
- {/* Element type badge */}
-
- {element.type}
-
-
- {/* Position */}
-
-
-
-
-
- handlePositionChange('x', e.target.value)}
- style={{
- width: '100%',
- padding: '0.5rem',
- border: `1px solid var(--border)`,
- borderRadius: 'var(--radius-sm)',
- fontSize: '13px',
- }}
- />
-
-
-
- handlePositionChange('y', e.target.value)}
- style={{
- width: '100%',
- padding: '0.5rem',
- border: `1px solid var(--border)`,
- borderRadius: 'var(--radius-sm)',
- fontSize: '13px',
- }}
- />
-
-
-
-
- {/* Size (for images and stickers) */}
- {(element.type === 'image' || element.type === 'sticker') && (
-
-
-
-
-
- handleSizeChange('width', e.target.value)}
- style={{
- width: '100%',
- padding: '0.5rem',
- border: `1px solid var(--border)`,
- borderRadius: 'var(--radius-sm)',
- fontSize: '13px',
- }}
- />
-
-
-
- handleSizeChange('height', e.target.value)}
- style={{
- width: '100%',
- padding: '0.5rem',
- border: `1px solid var(--border)`,
- borderRadius: 'var(--radius-sm)',
- fontSize: '13px',
- }}
- />
-
-
-
- )}
-
- {/* Edit Photo button (for images only) */}
- {element.type === 'image' && onEditPhoto && (
-
-
-
- )}
-
- {/* Font size (for text) */}
- {element.type === 'text' && (
- <>
-
-
- onUpdate({ fontSize: parseInt(e.target.value, 10) })}
- style={{ width: '100%' }}
- />
-
-
-
-
- onUpdate({ fill: e.target.value })}
- style={{
- width: '100%',
- height: '36px',
- border: `1px solid var(--border)`,
- borderRadius: 'var(--radius-sm)',
- cursor: 'pointer',
- padding: '2px',
- }}
- />
-
- >
- )}
-
- {/* Rotation */}
-
-
- handleRotationChange(e.target.value)}
- style={{ width: '100%' }}
- />
-
-
- {/* Delete button */}
-
-
-
- );
-});
diff --git a/client/src/components/properties/PropertiesPanel.jsx b/client/src/components/properties/PropertiesPanel.jsx
deleted file mode 100644
index c9b46fb..0000000
--- a/client/src/components/properties/PropertiesPanel.jsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import { BackgroundRemovalButton } from '../sidebar/BackgroundRemovalButton';
-
-export function PropertiesPanel({ selectedElement, onUpdate, onDelete }) {
- if (!selectedElement) {
- return (
-
-
Properties
-
-
Select an element to edit its properties
-
-
- );
- }
-
- const handlePositionChange = (axis, value) => {
- onUpdate(selectedElement.id, { [axis]: Number(value) });
- };
-
- const handleSizeChange = (dimension, value) => {
- onUpdate(selectedElement.id, { [dimension]: Number(value) });
- };
-
- const handleRotationChange = (value) => {
- onUpdate(selectedElement.id, { rotation: Number(value) });
- };
-
- const getIcon = () => {
- if (selectedElement.type === 'image') return '🖼️';
- if (selectedElement.type === 'text') return 'T';
- if (selectedElement.type === 'sticker') return '😊';
- return '📦';
- };
-
- return (
-
-
Properties
-
- {getIcon()}
-
- {selectedElement.type === 'text'
- ? selectedElement.text?.substring(0, 20) || 'Text'
- : `${selectedElement.type}`}
-
-
-
-
-
-
-
-
-
- handleRotationChange(e.target.value)}
- className="rotation-slider"
- />
-
-
- {selectedElement.type === 'image' && (
-
- )}
-
-
-
- );
-}
diff --git a/client/src/components/properties/index.js b/client/src/components/properties/index.js
deleted file mode 100644
index 43c7b2c..0000000
--- a/client/src/components/properties/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { PropertiesPanel } from './PropertiesPanel';
diff --git a/client/src/components/sidebar/BackgroundRemovalButton.jsx b/client/src/components/sidebar/BackgroundRemovalButton.jsx
deleted file mode 100644
index 0f93cf7..0000000
--- a/client/src/components/sidebar/BackgroundRemovalButton.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useBackgroundRemoval } from '../../hooks/useBackgroundRemoval';
-
-export function BackgroundRemovalButton({ selectedElement, onUpdate }) {
- const { loading, progress, hasModel, loadModel, removeBackground } = useBackgroundRemoval();
-
- const handleRemoveBackground = async () => {
- if (!selectedElement || selectedElement.type !== 'image') return;
-
- if (!hasModel) {
- const loaded = await loadModel();
- if (!loaded) return;
- }
-
- const resultUrl = await removeBackground(selectedElement.src);
- if (resultUrl) {
- onUpdate(selectedElement.id, { src: resultUrl });
- }
- };
-
- if (!selectedElement || selectedElement.type !== 'image') {
- return null;
- }
-
- return (
-
-
- {!hasModel && (
-
- First use requires downloading ~170MB model. Subsequent uses are cached.
-
- )}
-
- );
-}
diff --git a/client/src/components/sidebar/Sidebar.jsx b/client/src/components/sidebar/Sidebar.jsx
deleted file mode 100644
index 99e5215..0000000
--- a/client/src/components/sidebar/Sidebar.jsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { useState } from 'react';
-import { UploadTab } from './UploadTab';
-import { StickersTab } from './StickersTab';
-import { TextTab } from './TextTab';
-import { TemplatesTab } from './TemplatesTab';
-
-const TABS = [
- { id: 'upload', label: 'Upload', icon: '📁' },
- { id: 'stickers', label: 'Stickers', icon: '🎨' },
- { id: 'text', label: 'Text', icon: '📝' },
- { id: 'templates', label: 'Templates', icon: '📋' },
-];
-
-export function Sidebar({ onAddImage, onAddSticker, onAddText, onAddTemplate, onSlotImageUpload }) {
- const [activeTab, setActiveTab] = useState('upload');
-
- const renderTabContent = () => {
- switch (activeTab) {
- case 'upload':
- return ;
- case 'stickers':
- return ;
- case 'text':
- return ;
- case 'templates':
- return ;
- default:
- return null;
- }
- };
-
- return (
-
- {/* Tab headers */}
-
- {TABS.map((tab) => (
-
- ))}
-
-
- {/* Tab content */}
-
- {renderTabContent()}
-
-
- );
-}
diff --git a/client/src/components/sidebar/StickersTab.jsx b/client/src/components/sidebar/StickersTab.jsx
deleted file mode 100644
index d87a6b4..0000000
--- a/client/src/components/sidebar/StickersTab.jsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import { useState } from 'react';
-import { STICKERS, STICKER_CATEGORIES } from '../../constants/stickers';
-
-export function StickersTab({ onAddSticker }) {
- const [activeCategory, setActiveCategory] = useState('all');
-
- const categories = ['all', ...STICKER_CATEGORIES];
-
- const filteredStickers = activeCategory === 'all'
- ? STICKERS
- : STICKERS.filter(s => s.category === activeCategory);
-
- const handleAddSticker = (emoji) => {
- // Create a canvas element with the emoji
- const canvas = document.createElement('canvas');
- const size = 100;
- canvas.width = size;
- canvas.height = size;
- const ctx = canvas.getContext('2d');
-
- ctx.font = `${size * 0.8}px Arial`;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(emoji, size / 2, size / 2);
-
- const dataUrl = canvas.toDataURL('image/png');
-
- onAddSticker({
- type: 'sticker',
- x: 125,
- y: 125,
- width: 80,
- height: 80,
- rotation: 0,
- src: dataUrl,
- emoji,
- });
- };
-
- return (
-
-
- Stickers
-
-
- {/* Category pills */}
-
- {categories.map((cat) => (
-
- ))}
-
-
- {/* Sticker grid */}
-
- {filteredStickers.map((sticker, index) => (
-
- ))}
-
-
- );
-}
diff --git a/client/src/components/sidebar/TemplatesTab.jsx b/client/src/components/sidebar/TemplatesTab.jsx
deleted file mode 100644
index 2ecf1eb..0000000
--- a/client/src/components/sidebar/TemplatesTab.jsx
+++ /dev/null
@@ -1,257 +0,0 @@
-import { useState } from 'react';
-import { TEMPLATES, TEMPLATE_CATEGORIES } from '../../constants/templates';
-
-// Helper to get emoji for category
-function getCategoryEmoji(category) {
- const emojis = {
- Sports: '⚽',
- Music: '🎸',
- Quotes: '💬',
- Animals: '🐱',
- Abstract: '🌈',
- Vintage: '🏅',
- Nature: '🏔️',
- Tech: '💻',
- };
- return emojis[category] || '🎨';
-}
-
-export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
- const [selectedTemplateId, setSelectedTemplateId] = useState(null);
- const [uploadSlotId, setUploadSlotId] = useState(null);
-
- const templates = [
- {
- id: 'freeform',
- name: 'Freeform',
- description: 'No template - design freely',
- thumbnail: '🎨',
- },
- ...TEMPLATES.map(t => ({
- id: t.id,
- name: t.name,
- description: t.description,
- thumbnail: getCategoryEmoji(t.category),
- hasSlots: !!t.slots,
- })),
- ];
-
- const handleSelectTemplate = (template) => {
- setSelectedTemplateId(template.id);
- onAddTemplate(template.id);
- };
-
- const handleSlotClick = (slotId) => {
- setUploadSlotId(slotId);
- // Trigger file input click
- document.getElementById('slot-file-input')?.click();
- };
-
- const handleFileChange = (e) => {
- const file = e.target.files?.[0];
- if (file && uploadSlotId) {
- const reader = new FileReader();
- reader.onload = (event) => {
- onSlotImageUpload?.(uploadSlotId, event.target.result);
- };
- reader.readAsDataURL(file);
- }
- e.target.value = '';
- setUploadSlotId(null);
- };
-
- // Get slots for selected template
- const selectedTemplate = TEMPLATES.find(t => t.id === selectedTemplateId);
- const slots = selectedTemplate?.slots || [];
- const templates = [
- {
- id: 'freeform',
- name: 'Freeform',
- description: 'No template - design freely',
- thumbnail: '🎨',
- },
- ...TEMPLATES.map(t => ({
- id: t.id,
- name: t.name,
- description: t.description,
- thumbnail: getCategoryEmoji(t.category),
- })),
- ];
-
- const handleSelectTemplate = (template) => {
- onAddTemplate(template.id);
- };
-
- // Hidden file input for slot image uploads
- const renderFileInput = () => (
-
- );
-
- // Render slot upload buttons for template with slots
- const renderSlotUploads = () => {
- if (!selectedTemplateId || selectedTemplateId === 'freeform' || slots.length === 0) {
- return null;
- }
-
- return (
-
-
- Template Slots
-
-
- {slots.map((slot) => (
-
- ))}
-
-
- );
- };
-
- return (
-
- {renderFileInput()}
-
-
- Templates
-
-
-
- Choose a template to get started or design freely.
-
-
-
- {templates.map((template) => (
-
- ))}
-
-
- {renderSlotUploads()}
-
- );
-}
diff --git a/client/src/components/sidebar/TextTab.jsx b/client/src/components/sidebar/TextTab.jsx
deleted file mode 100644
index b96baac..0000000
--- a/client/src/components/sidebar/TextTab.jsx
+++ /dev/null
@@ -1,199 +0,0 @@
-import { useState } from 'react';
-import { FONTS } from '../../constants/fonts';
-
-export function TextTab({ onAddText }) {
- const [text, setText] = useState('Your text here');
- const [fontFamily, setFontFamily] = useState('Roboto');
- const [fontSize, setFontSize] = useState(48);
- const [fill, setFill] = useState('#0f172a');
-
- const handleAddText = () => {
- onAddText({
- type: 'text',
- x: 150,
- y: 150,
- text,
- fontFamily,
- fontSize,
- fill,
- rotation: 0,
- });
- };
-
- return (
-
-
- Add Text
-
-
- {/* Text input */}
-
-
-
-
- {/* Font selector */}
-
-
-
-
-
- {/* Font size */}
-
-
- setFontSize(parseInt(e.target.value, 10))}
- style={{ width: '100%' }}
- />
-
-
- {/* Color picker */}
-
-
-
- setFill(e.target.value)}
- style={{
- width: '40px',
- height: '40px',
- border: `1px solid var(--border)`,
- borderRadius: 'var(--radius-sm)',
- cursor: 'pointer',
- padding: '2px',
- }}
- />
- setFill(e.target.value)}
- style={{
- flex: 1,
- padding: '0.75rem',
- border: `1px solid var(--border)`,
- borderRadius: 'var(--radius-md)',
- fontSize: '13px',
- fontFamily: 'var(--font-mono)',
- }}
- />
-
-
-
- {/* Preview */}
-
-
- {/* Add Text button */}
-
-
- );
-}
diff --git a/client/src/components/sidebar/UploadTab.jsx b/client/src/components/sidebar/UploadTab.jsx
deleted file mode 100644
index 1526708..0000000
--- a/client/src/components/sidebar/UploadTab.jsx
+++ /dev/null
@@ -1,150 +0,0 @@
-import { useRef, useState } from 'react';
-
-export function UploadTab({ onAddImage }) {
- const fileInputRef = useRef(null);
- const [isDragging, setIsDragging] = useState(false);
- const [isUploading, setIsUploading] = useState(false);
-
- const handleFiles = async (files) => {
- const file = files[0];
- if (!file) return;
-
- // Validate file type
- const validTypes = ['image/jpeg', 'image/png', 'image/webp'];
- if (!validTypes.includes(file.type)) {
- alert('Please upload a JPEG, PNG, or WebP image');
- return;
- }
-
- // Validate file size (20MB)
- if (file.size > 20 * 1024 * 1024) {
- alert('File size must be under 20MB');
- return;
- }
-
- setIsUploading(true);
-
- try {
- const formData = new FormData();
- formData.append('image', file);
-
- const response = await fetch('/api/upload', {
- method: 'POST',
- body: formData,
- });
-
- if (!response.ok) {
- throw new Error('Upload failed');
- }
-
- const data = await response.json();
-
- // Add the uploaded image to canvas (use preview for canvas)
- onAddImage({
- type: 'image',
- x: 75,
- y: 75,
- width: 150,
- height: 150,
- rotation: 0,
- src: data.preview.url,
- originalUrl: data.original.url,
- });
- } catch (error) {
- console.error('Upload error:', error);
- alert('Failed to upload image. Please try again.');
- } finally {
- setIsUploading(false);
- }
- };
-
- const handleDragOver = (e) => {
- e.preventDefault();
- setIsDragging(true);
- };
-
- const handleDragLeave = (e) => {
- e.preventDefault();
- setIsDragging(false);
- };
-
- const handleDrop = (e) => {
- e.preventDefault();
- setIsDragging(false);
- handleFiles(e.dataTransfer.files);
- };
-
- const handleClick = () => {
- fileInputRef.current?.click();
- };
-
- const handleFileChange = (e) => {
- handleFiles(e.target.files);
- };
-
- return (
-
-
- Upload Image
-
-
-
-
📁
-
- Click to upload or drag and drop
-
-
- JPEG, PNG, WebP (max 20MB)
-
-
-
-
-
- {isUploading && (
-
- Uploading...
-
- )}
-
-
- Tip: After uploading, you can remove the background from your image using the background removal tool.
-
-
- );
-}
diff --git a/client/src/hooks/index.js b/client/src/hooks/index.js
deleted file mode 100644
index 2c01557..0000000
--- a/client/src/hooks/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { useDesignEditor } from './useDesignEditor';
diff --git a/client/src/hooks/useBackgroundRemoval.js b/client/src/hooks/useBackgroundRemoval.js
deleted file mode 100644
index 5d2cd77..0000000
--- a/client/src/hooks/useBackgroundRemoval.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import { useState, useCallback } from 'react';
-import { env, AutoModel, AutoProcessor, RawImage } from '@xenova/transformers';
-
-// Use local models only
-env.allowLocalModels = true;
-env.useBrowserCache = true;
-
-export function useBackgroundRemoval() {
- const [loading, setLoading] = useState(false);
- const [progress, setProgress] = useState(0);
- const [model, setModel] = useState(null);
- const [processor, setProcessor] = useState(null);
-
- const loadModel = useCallback(async () => {
- if (model && processor) return true;
-
- setLoading(true);
- setProgress(0);
-
- try {
- const loadedModel = await AutoModel.from_pretrained('Xenova/rmbg-1.4', {
- progress_callback: (data) => {
- if (data.status === 'progress') {
- setProgress(Math.round(data.progress));
- }
- },
- local_model_path: '/models/rmbg-1.4',
- });
-
- const loadedProcessor = await AutoProcessor.from_pretrained('Xenova/rmbg-1.4', {
- local_model_path: '/models/rmbg-1.4',
- });
-
- setModel(loadedModel);
- setProcessor(loadedProcessor);
- setLoading(false);
- return true;
- } catch (error) {
- console.error('Failed to load background removal model:', error);
- setLoading(false);
- return false;
- }
- }, [model, processor]);
-
- const removeBackground = useCallback(async (imageSrc) => {
- if (!model || !processor) {
- const loaded = await loadModel();
- if (!loaded) return null;
- }
-
- setLoading(true);
-
- try {
- // Load the image
- const img = new Image();
- img.crossOrigin = 'anonymous';
- img.src = imageSrc;
- await new Promise((resolve, reject) => {
- img.onload = resolve;
- img.onerror = reject;
- });
-
- // Process image through the model
- const inputs = await processor(img);
- const { pixel_values } = inputs;
-
- // Run inference
- const { output } = await model({ pixel_values });
-
- // Get the mask
- const maskData = await RawImage.fromTensor(output[0].mul(255).to('uint8')).resize(
- img.width,
- img.height
- );
-
- // Create canvas to apply mask
- const canvas = document.createElement('canvas');
- canvas.width = img.width;
- canvas.height = img.height;
- const ctx = canvas.getContext('2d');
-
- // Draw original image
- ctx.drawImage(img, 0, 0);
-
- // Get image data
- const imageData = ctx.getImageData(0, 0, img.width, img.height);
- const data = imageData.data;
- const maskPixels = maskData.data;
-
- // Apply alpha mask
- for (let i = 0; i < maskPixels.length; i++) {
- const alpha = maskPixels[i];
- data[i * 4 + 3] = alpha; // Set alpha channel
- }
-
- ctx.putImageData(imageData, 0, 0);
-
- // Convert to blob URL
- const blob = await new Promise((resolve) => {
- canvas.toBlob(resolve, 'image/png');
- });
-
- const url = URL.createObjectURL(blob);
- setLoading(false);
- return url;
- } catch (error) {
- console.error('Background removal failed:', error);
- setLoading(false);
- return null;
- }
- }, [model, processor, loadModel]);
-
- return {
- loading,
- progress,
- hasModel: !!model,
- loadModel,
- removeBackground,
- };
-}
diff --git a/client/src/hooks/useDesignEditor.js b/client/src/hooks/useDesignEditor.js
deleted file mode 100644
index 17a62e6..0000000
--- a/client/src/hooks/useDesignEditor.js
+++ /dev/null
@@ -1,185 +0,0 @@
-import { useState, useCallback, useRef, useEffect } from 'react';
-
-const MAX_HISTORY = 50;
-const DEBOUNCE_DELAY_MS = 300;
-
-export function useDesignEditor() {
- const [elements, setElements] = useState([]);
- const [selectedId, setSelectedId] = useState(null);
-
- // History for undo/redo
- const historyRef = useRef([]);
- const historyIndexRef = useRef(-1);
-
- // Debounce timer for rapid changes (drag/transform)
- const historyTimerRef = useRef(null);
- const pendingChangesRef = useRef(null);
-
- const saveToHistory = useCallback((newElements) => {
- // Remove any future history if we're in the middle of the stack
- if (historyIndexRef.current < historyRef.current.length - 1) {
- historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1);
- }
-
- // Add new state to history
- historyRef.current.push(JSON.stringify(newElements));
-
- // Limit history size
- if (historyRef.current.length > MAX_HISTORY) {
- historyRef.current.shift();
- } else {
- historyIndexRef.current++;
- }
- }, []);
-
- // Flush pending changes to history
- const flushPendingChanges = useCallback(() => {
- if (pendingChangesRef.current) {
- saveToHistory(pendingChangesRef.current);
- pendingChangesRef.current = null;
- }
- if (historyTimerRef.current) {
- clearTimeout(historyTimerRef.current);
- historyTimerRef.current = null;
- }
- }, [saveToHistory]);
-
- // Cleanup timer on unmount
- useEffect(() => {
- return () => {
- if (historyTimerRef.current) {
- clearTimeout(historyTimerRef.current);
- }
- };
- }, []);
-
- const canUndo = historyIndexRef.current > 0;
- const canRedo = historyIndexRef.current < historyRef.current.length - 1;
-
- const addElement = useCallback((element) => {
- // Flush any pending debounced changes first
- flushPendingChanges();
-
- const newElement = {
- ...element,
- id: `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
- };
-
- setElements((prev) => {
- const newElements = [...prev, newElement];
- saveToHistory(newElements);
- return newElements;
- });
-
- setSelectedId(newElement.id);
- return newElement.id;
- }, [flushPendingChanges, saveToHistory]);
-
- const updateElement = useCallback((id, attrs) => {
- setElements((prev) => {
- const newElements = prev.map((el) => (el.id === id ? { ...el, ...attrs } : el));
-
- // Debounce history commits for rapid changes (drag/transform)
- // Store pending changes but don't commit yet
- pendingChangesRef.current = newElements;
-
- // Clear existing timer
- if (historyTimerRef.current) {
- clearTimeout(historyTimerRef.current);
- }
-
- // Set timer to commit changes after delay
- historyTimerRef.current = setTimeout(() => {
- flushPendingChanges();
- }, DEBOUNCE_DELAY_MS);
-
- return newElements;
- });
- }, [flushPendingChanges]);
-
- const deleteElement = useCallback((id) => {
- // Flush any pending debounced changes first
- flushPendingChanges();
-
- setElements((prev) => {
- const newElements = prev.filter((el) => el.id !== id);
- saveToHistory(newElements);
- return newElements;
- });
-
- if (selectedId === id) {
- setSelectedId(null);
- }
- }, [selectedId, flushPendingChanges, saveToHistory]);
-
- const selectElement = useCallback((id) => {
- setSelectedId(id);
- }, []);
-
- const deselectAll = useCallback(() => {
- setSelectedId(null);
- }, []);
-
- const reorderElement = useCallback((id, newOrder) => {
- // Flush any pending debounced changes first
- flushPendingChanges();
-
- setElements((prev) => {
- const index = prev.findIndex((el) => el.id === id);
- if (index === -1 || index === newOrder) return prev;
-
- const newElements = [...prev];
- const [removed] = newElements.splice(index, 1);
- newElements.splice(newOrder, 0, removed);
-
- saveToHistory(newElements);
- return newElements;
- });
- }, [flushPendingChanges, saveToHistory]);
-
- // Commit history immediately (called on dragEnd/transformEnd)
- const commitHistory = useCallback(() => {
- flushPendingChanges();
- }, [flushPendingChanges]);
-
- const undo = useCallback(() => {
- if (historyIndexRef.current > 0) {
- historyIndexRef.current--;
- const prevState = JSON.parse(historyRef.current[historyIndexRef.current]);
- setElements(prevState);
- setSelectedId(null);
- }
- }, []);
-
- const redo = useCallback(() => {
- if (historyIndexRef.current < historyRef.current.length - 1) {
- historyIndexRef.current++;
- const nextState = JSON.parse(historyRef.current[historyIndexRef.current]);
- setElements(nextState);
- setSelectedId(null);
- }
- }, []);
-
- // Initialize history with empty state
- const initializeHistory = useCallback(() => {
- historyRef.current = [JSON.stringify([])];
- historyIndexRef.current = 0;
- }, []);
-
- return {
- elements,
- selectedId,
- addElement,
- updateElement,
- deleteElement,
- selectElement,
- deselectAll,
- reorderElement,
- commitHistory, // Call this on dragEnd/transformEnd to commit debounced changes
- undo,
- redo,
- canUndo,
- canRedo,
- initializeHistory,
- };
-}
diff --git a/client/src/hooks/useTemplate.js b/client/src/hooks/useTemplate.js
deleted file mode 100644
index db63c9b..0000000
--- a/client/src/hooks/useTemplate.js
+++ /dev/null
@@ -1,192 +0,0 @@
-import { useState, useCallback, useRef } from 'react';
-
-/**
- * Calculate auto-crop for fitting an image into a slot
- * Implements object-fit: cover behavior
- *
- * @param {Object} imageSize - Original image dimensions {width, height}
- * @param {Object} slotSize - Slot dimensions {width, height}
- * @returns {Object} Crop coordinates {sx, sy, sWidth, sHeight}
- */
-export function calculateAutoCrop(imageSize, slotSize) {
- const imageRatio = imageSize.width / imageSize.height;
- const slotRatio = slotSize.width / slotSize.height;
-
- let sx, sy, sWidth, sHeight;
-
- if (imageRatio > slotRatio) {
- // Image is wider than slot - crop sides
- sHeight = imageSize.height;
- sWidth = imageSize.height * slotRatio;
- sx = (imageSize.width - sWidth) / 2;
- sy = 0;
- } else {
- // Image is taller than slot - crop top/bottom
- sWidth = imageSize.width;
- sHeight = imageSize.width / slotRatio;
- sx = 0;
- sy = (imageSize.height - sHeight) / 2;
- }
-
- return { sx, sy, sWidth, sHeight };
-}
-
-/**
- * Create a drag bound function that constrains movement to slot bounds
- *
- * @param {Object} slot - Slot configuration with bounds
- * @param {Object} elementSize - Element dimensions {width, height}
- * @returns {Function} Drag bound function for Konva
- */
-export function createDragBoundFunc(slot, elementSize) {
- const { bounds } = slot;
- const minX = bounds.x;
- const minY = bounds.y;
- const maxX = bounds.x + bounds.width - elementSize.width;
- const maxY = bounds.y + bounds.height - elementSize.height;
-
- return (oldBox, newBox) => {
- return {
- x: Math.max(minX, Math.min(newBox.x, maxX)),
- y: Math.max(minY, Math.min(newBox.y, maxY)),
- width: newBox.width,
- height: newBox.height,
- };
- };
-}
-
-/**
- * useTemplate - Hook for managing template state and slot operations
- *
- * @param {Array} templates - Available templates
- * @returns {Object} Template management functions and state
- */
-export function useTemplate(templates = []) {
- const [currentTemplateId, setCurrentTemplateId] = useState(null);
- const [assignedSlots, setAssignedSlots] = useState({});
- const templateRef = useRef(null);
-
- // Get current template object
- const currentTemplate = templates.find(t => t.id === currentTemplateId) || null;
-
- // Get slots for current template
- const getSlots = useCallback(() => {
- if (!currentTemplate || !currentTemplate.slots) return [];
- return currentTemplate.slots;
- }, [currentTemplate]);
-
- // Load a template
- const loadTemplate = useCallback((templateId) => {
- const template = templates.find(t => t.id === templateId);
- if (template) {
- setCurrentTemplateId(templateId);
- setAssignedSlots({});
- templateRef.current = template;
- return true;
- }
- return false;
- }, [templates]);
-
- // Clear template (freeform mode)
- const clearTemplate = useCallback(() => {
- setCurrentTemplateId(null);
- setAssignedSlots({});
- templateRef.current = null;
- }, []);
-
- // Assign image to slot
- const assignImageToSlot = useCallback((slotId, imageData) => {
- const slots = getSlots();
- const slot = slots.find(s => s.id === slotId);
-
- if (!slot) {
- console.error(`Slot ${slotId} not found`);
- return null;
- }
-
- // Load image to get dimensions
- const img = new Image();
- img.src = imageData;
-
- const elementData = {
- type: 'image',
- src: imageData,
- x: slot.bounds.x,
- y: slot.bounds.y,
- width: slot.bounds.width,
- height: slot.bounds.height,
- slotId,
- // Will be set after image loads
- crop: null,
- };
-
- // Calculate crop once image is loaded
- img.onload = () => {
- const crop = calculateAutoCrop(
- { width: img.width, height: img.height },
- { width: slot.bounds.width, height: slot.bounds.height }
- );
- elementData.crop = crop;
- };
-
- setAssignedSlots(prev => ({
- ...prev,
- [slotId]: elementData,
- }));
-
- return elementData;
- }, [getSlots]);
-
- // Get assigned slot data
- const getAssignedSlot = useCallback((slotId) => {
- return assignedSlots[slotId] || null;
- }, [assignedSlots]);
-
- // Remove image from slot
- const removeFromSlot = useCallback((slotId) => {
- setAssignedSlots(prev => {
- const updated = { ...prev };
- delete updated[slotId];
- return updated;
- });
- }, []);
-
- // Get drag bound function for a slot
- const getDragBoundFunc = useCallback((slotId, elementSize) => {
- const slot = getSlots().find(s => s.id === slotId);
- if (!slot) return null;
- return createDragBoundFunc(slot, elementSize);
- }, [getSlots]);
-
- // Check if slot is filled
- const isSlotFilled = useCallback((slotId) => {
- return !!assignedSlots[slotId];
- }, [assignedSlots]);
-
- // Get all assigned elements
- const getAssignedElements = useCallback(() => {
- return Object.values(assignedSlots);
- }, [assignedSlots]);
-
- return {
- // State
- currentTemplateId,
- currentTemplate,
- assignedSlots,
-
- // Template management
- loadTemplate,
- clearTemplate,
- getSlots,
-
- // Slot operations
- assignImageToSlot,
- getAssignedSlot,
- removeFromSlot,
- isSlotFilled,
- getAssignedElements,
-
- // Constraints
- getDragBoundFunc,
- };
-}
diff --git a/client/src/index.css b/client/src/index.css
deleted file mode 100644
index 46c2994..0000000
--- a/client/src/index.css
+++ /dev/null
@@ -1,1035 +0,0 @@
-:root {
- /* Colors */
- --accent: #38bdf8;
- --accent-hover: #0ea5e9;
- --accent-bg: rgba(56, 189, 248, 0.1);
-
- /* Neutrals */
- --bg-primary: #ffffff;
- --bg-secondary: #f8fafc;
- --bg-tertiary: #f1f5f9;
- --border: #e2e8f0;
- --border-focus: #38bdf8;
- --text-primary: #0f172a;
- --text-secondary: #475569;
- --text-muted: #94a3b8;
-
- /* Status colors */
- --success: #22c55e;
- --warning: #f59e0b;
- --error: #ef4444;
-
- /* Border radii */
- --radius-sm: 4px;
- --radius-md: 8px;
- --radius-lg: 12px;
- --radius-xl: 16px;
-
- /* Shadows */
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
-
- /* Typography */
- --font-body: 'DM Sans', system-ui, -apple-system, sans-serif;
- --font-mono: 'Space Mono', ui-monospace, Consolas, monospace;
-
- font-family: var(--font-body);
- line-height: 1.5;
- font-weight: 400;
- color: var(--text-primary);
- background-color: var(--bg-primary);
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-* {
- box-sizing: border-box;
-}
-
-body {
- margin: 0;
- min-height: 100vh;
-}
-
-#root {
- min-height: 100vh;
- display: flex;
- flex-direction: column;
-}
-
-/* Three-column layout */
-.editor-layout {
- display: flex;
- flex: 1;
- overflow: hidden;
-}
-
-.sidebar {
- width: 320px;
- background: var(--bg-secondary);
- border-right: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-.canvas-area {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background: var(--bg-tertiary);
- overflow: auto;
- padding: 2rem;
-}
-
-.properties-panel {
- width: 280px;
- background: var(--bg-secondary);
- border-left: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-button {
- font-family: inherit;
- cursor: pointer;
- outline: none;
-}
-
-button:focus-visible {
- box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--accent);
-}
-
-input, textarea, select {
- font-family: inherit;
-}
-
-/* App Layout - Three Column */
-.app-layout {
- display: flex;
- min-height: 100vh;
- background: var(--bg-secondary);
-}
-
-.sidebar-container {
- width: 280px;
- background: var(--bg-primary);
- border-right: 1px solid var(--border);
- flex-shrink: 0;
- overflow-y: auto;
-}
-
-.canvas-container {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 2rem;
- overflow-y: auto;
-}
-
-.properties-container {
- width: 260px;
- background: var(--bg-primary);
- border-left: 1px solid var(--border);
- flex-shrink: 0;
- overflow-y: auto;
-}
-
-.app-title {
- font-size: 1.5rem;
- font-weight: 600;
- margin: 0 0 0.25rem 0;
- color: var(--text-primary);
-}
-
-.app-subtitle {
- color: var(--text-secondary);
- margin: 0;
- font-size: 0.875rem;
-}
-
-.canvas-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 1rem;
- width: 100%;
- max-width: 400px;
-}
-
-.canvas-actions {
- display: flex;
- gap: 0.75rem;
- align-items: center;
-}
-
-.undo-redo-buttons {
- display: flex;
- gap: 0.5rem;
-}
-
-.icon-btn {
- padding: 0.5rem 0.75rem;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- font-size: 0.75rem;
- cursor: pointer;
- transition: all 0.2s;
- color: var(--text-secondary);
-}
-
-.icon-btn:hover:not(:disabled) {
- background: var(--accent);
- border-color: var(--accent);
- color: white;
-}
-
-.icon-btn.disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-.icon-btn:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-.export-btn {
- padding: 0.5rem 1rem;
- background: linear-gradient(135deg, #22c55e, #16a34a);
- color: white;
- border: none;
- border-radius: var(--radius-md);
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.export-btn:hover:not(:disabled) {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
-}
-
-.export-btn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-.export-error {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.75rem 1rem;
- background: #fef2f2;
- border: 1px solid #fecaca;
- border-radius: var(--radius-md);
- color: #dc2626;
- font-size: 0.875rem;
- margin-bottom: 1rem;
- width: 100%;
- max-width: 400px;
-}
-
-.export-error p {
- margin: 0;
-}
-
-.close-error {
- background: transparent;
- border: none;
- color: #dc2626;
- cursor: pointer;
- font-size: 1.25rem;
- padding: 0;
- line-height: 1;
-}
-
-.close-error:hover {
- color: #991b1b;
-}
-
-.canvas-wrapper {
- margin-bottom: 1rem;
-}
-
-.debug-info {
- padding: 1rem;
- background: var(--bg-tertiary);
- border-radius: var(--radius-md);
- font-size: 0.875rem;
- text-align: center;
-}
-
-.debug-info p {
- margin: 0.25rem 0;
-}
-
-.debug-info .tip {
- color: var(--text-muted);
- font-size: 12px;
-}
-
-/* Sidebar */
-.sidebar {
- display: flex;
- flex-direction: column;
- height: 100%;
-}
-
-.sidebar-tabs {
- display: flex;
- border-bottom: 1px solid var(--border);
- background: var(--bg-tertiary);
-}
-
-.sidebar-tab {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 0.75rem 0.5rem;
- background: transparent;
- border: none;
- border-bottom: 2px solid transparent;
- cursor: pointer;
- transition: all 0.2s;
- font-size: 0.75rem;
- color: var(--text-secondary);
-}
-
-.sidebar-tab:hover {
- background: var(--bg-primary);
-}
-
-.sidebar-tab.active {
- border-bottom-color: var(--accent);
- color: var(--text-primary);
-}
-
-.tab-icon {
- font-size: 1.25rem;
- margin-bottom: 0.25rem;
-}
-
-.tab-label {
- font-size: 0.625rem;
-}
-
-.sidebar-content {
- padding: 1rem;
- flex: 1;
-}
-
-/* Upload Tab */
-.upload-tab h3 {
- font-size: 0.875rem;
- margin: 0 0 1rem 0;
- color: var(--text-primary);
-}
-
-.upload-zone {
- border: 2px dashed var(--border);
- border-radius: var(--radius-md);
- padding: 2rem 1rem;
- text-align: center;
- cursor: pointer;
- transition: all 0.2s;
- background: var(--bg-secondary);
-}
-
-.upload-zone:hover {
- border-color: var(--accent);
- background: var(--accent-bg);
-}
-
-.upload-zone.dragging {
- border-color: var(--accent);
- background: var(--accent-bg);
-}
-
-.upload-zone.uploading {
- opacity: 0.7;
- pointer-events: none;
-}
-
-.upload-icon {
- font-size: 2.5rem;
- margin-bottom: 0.5rem;
-}
-
-.upload-zone p {
- margin: 0.25rem 0;
- font-size: 0.875rem;
- color: var(--text-secondary);
-}
-
-.upload-hint {
- font-size: 0.75rem !important;
- color: var(--text-muted) !important;
-}
-
-.uploading-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.5rem;
-}
-
-.spinner {
- width: 24px;
- height: 24px;
- border: 2px solid var(--border);
- border-top-color: var(--accent);
- border-radius: 50%;
- animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-/* Stickers Tab */
-.stickers-tab h3 {
- font-size: 0.875rem;
- margin: 0 0 1rem 0;
-}
-
-.category-pills {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin-bottom: 1rem;
-}
-
-.category-pill {
- padding: 0.25rem 0.75rem;
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- background: var(--bg-secondary);
- font-size: 0.75rem;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.category-pill:hover {
- border-color: var(--accent);
-}
-
-.category-pill.active {
- background: var(--accent);
- border-color: var(--accent);
- color: white;
-}
-
-.sticker-grid {
- display: grid;
- grid-template-columns: repeat(6, 1fr);
- gap: 0.5rem;
- max-height: 400px;
- overflow-y: auto;
-}
-
-.sticker-button {
- font-size: 1.5rem;
- padding: 0.5rem;
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- background: var(--bg-primary);
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.sticker-button:hover {
- transform: scale(1.1);
- border-color: var(--accent);
-}
-
-/* Text Tab */
-.text-tab h3 {
- font-size: 0.875rem;
- margin: 0 0 1rem 0;
-}
-
-.text-input-group {
- margin-bottom: 1rem;
-}
-
-.text-input-group label {
- display: block;
- font-size: 0.75rem;
- color: var(--text-secondary);
- margin-bottom: 0.25rem;
-}
-
-.text-input,
-.font-select {
- width: 100%;
- padding: 0.5rem;
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- font-size: 0.875rem;
-}
-
-.text-input:focus,
-.font-select:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.size-slider {
- width: 100%;
-}
-
-.color-picker-row {
- display: flex;
- gap: 0.5rem;
-}
-
-.color-picker {
- width: 40px;
- height: 36px;
- padding: 0;
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- cursor: pointer;
-}
-
-.color-input {
- flex: 1;
- padding: 0.5rem;
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
-}
-
-.add-text-btn {
- width: 100%;
- padding: 0.75rem;
- background: var(--accent);
- color: white;
- border: none;
- border-radius: var(--radius-md);
- font-weight: 500;
- cursor: pointer;
- transition: background 0.2s;
-}
-
-.add-text-btn:hover {
- background: var(--accent-hover);
-}
-
-.text-preview {
- margin-top: 1rem;
- padding: 1rem;
- background: var(--bg-secondary);
- border-radius: var(--radius-md);
- text-align: center;
-}
-
-/* Templates Tab */
-.templates-tab h3 {
- font-size: 0.875rem;
- margin: 0 0 1rem 0;
-}
-
-.templates-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 0.75rem;
- max-height: 400px;
- overflow-y: auto;
-}
-
-.template-card {
- display: flex;
- flex-direction: column;
- padding: 0.75rem;
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- background: var(--bg-primary);
- cursor: pointer;
- transition: all 0.2s;
- text-align: left;
-}
-
-.template-card:hover {
- border-color: var(--accent);
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
-}
-
-.template-preview {
- height: 60px;
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- margin-bottom: 0.5rem;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.25rem;
- flex-wrap: wrap;
- padding: 0.25rem;
-}
-
-.template-preview-element {
- font-weight: 500;
-}
-
-.template-info {
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
-}
-
-.template-name {
- font-size: 0.75rem;
- font-weight: 500;
- color: var(--text-primary);
-}
-
-.template-category {
- font-size: 0.625rem;
- color: var(--text-muted);
-}
-
-/* Properties Panel */
-.properties-panel {
- padding: 1rem;
-}
-
-.properties-panel h3 {
- font-size: 0.875rem;
- margin: 0 0 1rem 0;
- color: var(--text-primary);
-}
-
-.no-selection {
- padding: 2rem 1rem;
- text-align: center;
- color: var(--text-muted);
- font-size: 0.875rem;
-}
-
-.element-header {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.75rem;
- background: var(--bg-tertiary);
- border-radius: var(--radius-md);
- margin-bottom: 1rem;
-}
-
-.element-icon {
- font-size: 1.25rem;
-}
-
-.element-name {
- font-weight: 500;
- font-size: 0.875rem;
-}
-
-.property-group {
- margin-bottom: 1rem;
-}
-
-.property-group label {
- display: block;
- font-size: 0.75rem;
- color: var(--text-secondary);
- margin-bottom: 0.5rem;
-}
-
-.property-row {
- display: flex;
- gap: 0.5rem;
-}
-
-.property-input {
- flex: 1;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.property-label {
- font-size: 0.75rem;
- color: var(--text-muted);
- min-width: 12px;
-}
-
-.property-input input {
- flex: 1;
- padding: 0.5rem;
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- font-size: 0.875rem;
- text-align: center;
-}
-
-.property-input input:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.rotation-slider {
- width: 100%;
-}
-
-.delete-btn {
- width: 100%;
- padding: 0.75rem;
- background: transparent;
- color: var(--error);
- border: 1px solid var(--error);
- border-radius: var(--radius-md);
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.delete-btn:hover {
- background: var(--error);
- color: white;
-}
-
-.bg-removal-container {
- margin-top: 1rem;
- padding-top: 1rem;
- border-top: 1px solid var(--border);
-}
-
-.bg-removal-btn {
- width: 100%;
- padding: 0.75rem;
- background: linear-gradient(135deg, #8b5cf6, #ec4899);
- color: white;
- border: none;
- border-radius: var(--radius-md);
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.5rem;
-}
-
-.bg-removal-btn:hover:not(:disabled) {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
-}
-
-.bg-removal-btn:disabled {
- opacity: 0.7;
- cursor: not-allowed;
-}
-
-.bg-removal-hint {
- font-size: 0.7rem;
- color: var(--text-muted);
- margin: 0.5rem 0 0 0;
- line-height: 1.4;
-}
-
-.spinner-small {
- width: 16px;
- height: 16px;
- border: 2px solid rgba(255, 255, 255, 0.3);
- border-top-color: white;
- border-radius: 50%;
- animation: spin 1s linear infinite;
-}
-
-/* Filerobot Editor Overlay */
-.filerobot-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.8);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
-}
-
-.filerobot-container {
- width: 90%;
- height: 90%;
- background: #1e1e1e;
- border-radius: var(--radius-lg);
- overflow: hidden;
-}
-
-/* PWA Install Banner */
-.pwa-install-banner {
- position: fixed;
- bottom: 20px;
- left: 50%;
- transform: translateX(-50%);
- background: var(--bg-primary);
- border: 1px solid var(--accent);
- border-radius: var(--radius-lg);
- padding: 1rem 1.5rem;
- box-shadow: var(--shadow-lg);
- z-index: 1000;
- display: flex;
- align-items: center;
- gap: 1rem;
- animation: slideUp 0.3s ease-out;
-}
-
-@keyframes slideUp {
- from {
- opacity: 0;
- transform: translateX(-50%) translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateX(-50%) translateY(0);
- }
-}
-
-.pwa-install-banner p {
- margin: 0;
- font-size: 0.875rem;
- color: var(--text-primary);
-}
-
-.pwa-install-buttons {
- display: flex;
- gap: 0.5rem;
-}
-
-.install-btn {
- padding: 0.5rem 1rem;
- background: var(--accent);
- color: white;
- border: none;
- border-radius: var(--radius-sm);
- font-size: 0.75rem;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.2s;
-}
-
-.install-btn:hover {
- background: var(--accent-hover);
-}
-
-.dismiss-btn {
- padding: 0.5rem 1rem;
- background: var(--bg-tertiary);
- color: var(--text-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- font-size: 0.75rem;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.dismiss-btn:hover {
- background: var(--bg-secondary);
-}
-
-/* Responsive */
-@media (max-width: 900px) {
- .app-layout {
- flex-direction: column;
- }
-
- .sidebar-container,
- .properties-container {
- width: 100%;
- border: none;
- border-bottom: 1px solid var(--border);
- }
-
- .sidebar-tabs {
- overflow-x: auto;
- }
-
- .sticker-grid {
- grid-template-columns: repeat(8, 1fr);
- }
-}
-
-/* Tablet - Stack sidebar and properties panel */
-@media (max-width: 1200px) and (min-width: 768px) {
- .sidebar {
- width: 280px;
- }
-
- .properties-panel {
- width: 240px;
- }
-
- .canvas-area {
- padding: 1.5rem;
- }
-}
-
-/* Mobile - Full width layout */
-@media (max-width: 767px) {
- .editor-layout {
- flex-direction: column;
- }
-
- .sidebar {
- width: 100%;
- height: auto;
- max-height: 50vh;
- border-right: none;
- border-bottom: 1px solid var(--border);
- }
-
- .canvas-area {
- padding: 1rem;
- width: 100%;
- }
-
- .properties-panel {
- width: 100%;
- border-left: none;
- border-top: 1px solid var(--border);
- max-height: 40vh;
- }
-
- /* Adjust tab buttons for mobile */
- .sidebar-tabs button {
- padding: 10px 6px;
- font-size: 10px;
- }
-
- .tab-icon {
- font-size: 14px;
- }
-
- /* Smaller sticker grid on mobile */
- .sticker-grid {
- grid-template-columns: repeat(4, 1fr);
- }
-
- /* Templates grid on mobile */
- .templates-grid {
- grid-template-columns: 1fr;
- }
-
- /* Adjust canvas size display */
- .canvas-area h1 {
- font-size: 16px;
- }
-
- .canvas-area p {
- font-size: 11px;
- }
-
- /* Stack action buttons */
- .canvas-area > div:nth-child(2) {
- flex-wrap: wrap;
- }
-
- .canvas-area button {
- flex: 1;
- min-width: 80px;
- }
-
- /* PWA banners on mobile */
- .pwa-install-banner,
- .pwa-update-banner {
- left: 10px;
- right: 10px;
- transform: none;
- width: auto;
- flex-direction: column;
- text-align: center;
- }
-
- .pwa-install-buttons,
- .pwa-update-buttons {
- width: 100%;
- justify-content: center;
- }
-}
-
-/* Small mobile */
-@media (max-width: 480px) {
- .sidebar {
- max-height: 40vh;
- }
-
- .canvas-area {
- padding: 0.5rem;
- }
-
- /* Larger touch targets */
- button {
- min-height: 44px;
- }
-
- .sticker-grid {
- grid-template-columns: repeat(3, 1fr);
- }
-
- .category-pills {
- gap: 0.25rem;
- }
-
- .category-pill {
- padding: 0.25rem 0.5rem;
- font-size: 11px;
- }
-}
-
-/* Touch device optimizations */
-@media (hover: none) and (pointer: coarse) {
- .sticker-button:hover {
- transform: none;
- }
-
- .template-card:hover {
- transform: none;
- }
-
- /* Larger drop zones */
- .upload-zone {
- padding: 2.5rem 1rem;
- }
-}
-
-/* Offline indicator */
-.offline-indicator {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- background: linear-gradient(135deg, #f59e0b, #d97706);
- color: white;
- padding: 0.5rem 1rem;
- text-align: center;
- font-size: 12px;
- font-weight: 500;
- z-index: 9999;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.5rem;
- box-shadow: var(--shadow-md);
-}
-
-.offline-indicator.hidden {
- display: none;
-}
diff --git a/client/vite.config.js b/client/vite.config.js
deleted file mode 100644
index 51e54d1..0000000
--- a/client/vite.config.js
+++ /dev/null
@@ -1,160 +0,0 @@
-import { defineConfig } from 'vite';
-import react from '@vitejs/plugin-react';
-import { VitePWA } from 'vite-plugin-pwa';
-
-export default defineConfig({
- plugins: [
- react(),
- VitePWA({
- registerType: 'prompt',
- includeAssets: ['favicon.ico', 'pwa-192x192.svg', 'pwa-512x512.svg'],
- manifest: {
- name: 'Apparel Designer',
- short_name: 'ApparelDesigner',
- description: 'T-shirt customization editor',
- theme_color: '#38bdf8',
- background_color: '#ffffff',
- display: 'standalone',
- orientation: 'any',
- scope: '/',
- start_url: '/',
- icons: [
- {
- src: 'pwa-192x192.svg',
- sizes: '192x192',
- type: 'image/svg+xml',
- },
- {
- src: 'pwa-512x512.svg',
- sizes: '512x512',
- type: 'image/svg+xml',
- },
- {
- src: 'pwa-512x512.svg',
- sizes: '512x512',
- type: 'image/svg+xml',
- purpose: 'any maskable',
- },
- ],
- },
- workbox: {
- globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
- runtimeCaching: [
- {
- urlPattern: /^https:\/\/cdn\.huggingface\.co\/.*/i,
- handler: 'CacheFirst',
- options: {
- cacheName: 'transformers-models',
- expiration: {
- maxEntries: 10,
- maxAgeSeconds: 60 * 60 * 24 * 30,
- },
- cacheableResponse: {
- statuses: [0, 200],
- },
- },
- },
- // HuggingFace LFS (Large File Storage) for model weights
- {
- urlPattern: /^https:\/\/cdn-lfs\.huggingface\.co\/.*/i,
- handler: 'CacheFirst',
- options: {
- cacheName: 'transformers-lfs',
- expiration: {
- maxEntries: 10,
- maxAgeSeconds: 60 * 60 * 24 * 30,
- },
- cacheableResponse: {
- statuses: [0, 200],
- },
- },
- },
- {
- urlPattern: /^\/api\/uploads\/.*/i,
- handler: 'CacheFirst',
- options: {
- cacheName: 'uploaded-images',
- expiration: {
- maxEntries: 50,
- maxAgeSeconds: 60 * 60 * 24 * 7,
- },
- },
- },
- // Template data caching
- {
- urlPattern: /^\/api\/templates\/.*/i,
- handler: 'StaleWhileRevalidate',
- options: {
- cacheName: 'template-data',
- expiration: {
- maxEntries: 20,
- maxAgeSeconds: 60 * 60 * 24 * 7,
- },
- cacheableResponse: {
- statuses: [0, 200],
- },
- },
- },
- // API responses caching
- {
- urlPattern: /^\/api\/.*/i,
- handler: 'NetworkFirst',
- options: {
- cacheName: 'api-responses',
- expiration: {
- maxEntries: 50,
- maxAgeSeconds: 300,
- },
- cacheableResponse: {
- statuses: [0, 200],
- },
- networkTimeoutSeconds: 3,
- },
- },
- {
- urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
- handler: 'StaleWhileRevalidate',
- options: {
- cacheName: 'google-fonts',
- expiration: {
- maxEntries: 10,
- maxAgeSeconds: 60 * 60 * 24 * 365,
- },
- },
- },
- {
- urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
- handler: 'CacheFirst',
- options: {
- cacheName: 'gstatic-fonts',
- expiration: {
- maxEntries: 10,
- maxAgeSeconds: 60 * 60 * 24 * 365,
- },
- },
- },
- ],
- },
- }),
- ],
- server: {
- port: 3000,
- proxy: {
- '/api': {
- target: 'http://localhost:3001',
- changeOrigin: true,
- },
- '/uploads': {
- target: 'http://localhost:3001',
- changeOrigin: true,
- },
- '/exports': {
- target: 'http://localhost:3001',
- changeOrigin: true,
- },
- },
- },
- build: {
- outDir: 'dist',
- },
-});
diff --git a/docker-compose.yml b/docker-compose.yml
index 15f34ea..da4db68 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,26 +1,15 @@
-version: '3.8'
-
services:
apparel-designer:
- build:
- context: .
- dockerfile: Dockerfile
+ build: { context: ., dockerfile: Dockerfile }
container_name: apparel-designer
- ports:
- - "3001:3001"
+ ports: ["3001:3001"]
volumes:
- - uploads_data:/app/server/uploads
- - exports_data:/app/server/exports
+ - uploads_data:/app/uploads
+ - exports_data:/app/exports
environment:
- NODE_ENV=production
- PORT=3001
restart: unless-stopped
- healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/api/health"]
- interval: 30s
- timeout: 10s
- retries: 3
- start_period: 10s
volumes:
uploads_data:
diff --git a/client/eslint.config.js b/eslint.config.js
similarity index 68%
rename from client/eslint.config.js
rename to eslint.config.js
index 4fa125d..e3e879f 100644
--- a/client/eslint.config.js
+++ b/eslint.config.js
@@ -7,7 +7,7 @@ import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
- files: ['**/*.{js,jsx}'],
+ files: ['src/**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
@@ -23,7 +23,19 @@ export default defineConfig([
},
},
rules: {
- 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]', argsIgnorePattern: '^_' }],
+ },
+ },
+ {
+ files: ['server.js'],
+ extends: [js.configs.recommended],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.node,
+ sourceType: 'module',
+ },
+ rules: {
+ 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
},
])
diff --git a/server/exports/.gitkeep b/exports/.gitkeep
similarity index 100%
rename from server/exports/.gitkeep
rename to exports/.gitkeep
diff --git a/client/index.html b/index.html
similarity index 96%
rename from client/index.html
rename to index.html
index fb60102..bdeda60 100644
--- a/client/index.html
+++ b/index.html
@@ -5,11 +5,9 @@
Apparel Designer
-
-
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 80bed63..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,373 +0,0 @@
-{
- "name": "apparel-designer",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "apparel-designer",
- "version": "1.0.0",
- "hasInstallScript": true,
- "devDependencies": {
- "concurrently": "^8.2.0"
- }
- },
- "node_modules/@babel/runtime": {
- "version": "7.29.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
- "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/chalk/node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
- "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.1",
- "wrap-ansi": "^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/concurrently": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
- "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.1.2",
- "date-fns": "^2.30.0",
- "lodash": "^4.17.21",
- "rxjs": "^7.8.1",
- "shell-quote": "^1.8.1",
- "spawn-command": "0.0.2",
- "supports-color": "^8.1.1",
- "tree-kill": "^1.2.2",
- "yargs": "^17.7.2"
- },
- "bin": {
- "conc": "dist/bin/concurrently.js",
- "concurrently": "dist/bin/concurrently.js"
- },
- "engines": {
- "node": "^14.13.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
- }
- },
- "node_modules/date-fns": {
- "version": "2.30.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
- "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.21.0"
- },
- "engines": {
- "node": ">=0.11"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/date-fns"
- }
- },
- "node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "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/get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "6.* || 8.* || >= 10.*"
- }
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/lodash": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
- "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/rxjs": {
- "version": "7.8.2",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
- "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
- "node_modules/shell-quote": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
- "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/spawn-command": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
- "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
- "dev": true
- },
- "node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/tree-kill": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
- "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "tree-kill": "cli.js"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
- "license": "0BSD"
- },
- "node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/y18n": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
- "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/yargs": {
- "version": "17.7.2",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
- "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cliui": "^8.0.1",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
- "require-directory": "^2.1.1",
- "string-width": "^4.2.3",
- "y18n": "^5.0.5",
- "yargs-parser": "^21.1.1"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/yargs-parser": {
- "version": "21.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
- "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- }
- }
-}
diff --git a/package.json b/package.json
index 9a2e3d9..f5ca1d4 100644
--- a/package.json
+++ b/package.json
@@ -4,21 +4,27 @@
"description": "T-shirt customization editor with background removal, stickers, text, and export",
"private": true,
"type": "module",
- "main": "server/index.js",
"scripts": {
- "dev": "concurrently \"npm run dev:client\" \"npm run dev:server\"",
- "dev:client": "cd client && vite",
- "dev:server": "node --watch server/index.js",
- "build": "cd client && vite build",
- "start": "node server/index.js",
- "install:all": "npm install && cd client && npm install"
+ "dev": "concurrently \"vite\" \"DYLD_INSERT_LIBRARIES='' node --watch server.js\"",
+ "dev:win": "concurrently \"vite\" \"node --watch server.js\"",
+ "build": "vite build",
+ "start": "node server.js",
+ "lint": "eslint .",
+ "preview": "vite preview"
},
"dependencies": {
+ "@huggingface/transformers": "^3.4.0",
"canvas": "^2.11.2",
"cors": "^2.8.5",
"express": "^4.18.2",
+ "konva": "^10.0.0",
"multer": "^1.4.5-lts.1",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
+ "react-filerobot-image-editor": "^4.8.1",
+ "react-konva": "^19.2.3",
"sharp": "^0.33.2",
+ "use-image": "^1.1.1",
"uuid": "^9.0.1"
},
"devDependencies": {
@@ -33,14 +39,7 @@
"globals": "^17.5.0",
"vite": "^8.0.9",
"vite-plugin-pwa": "^0.20.5",
- "workbox-window": "^7.1.0",
- "react": "^19.2.5",
- "react-dom": "^19.2.5",
- "react-konva": "^18.2.10",
- "konva": "^9.3.18",
- "use-image": "^1.1.1",
- "@xenova/transformers": "^2.17.2",
- "react-filerobot-image-editor": "^4.8.1"
+ "workbox-window": "^7.1.0"
},
"engines": {
"node": ">=20.0.0"
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..841ad9a
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/icons.svg b/public/icons.svg
new file mode 100644
index 0000000..c68709b
--- /dev/null
+++ b/public/icons.svg
@@ -0,0 +1 @@
+
diff --git a/public/pwa-192x192.svg b/public/pwa-192x192.svg
new file mode 100644
index 0000000..5eccd31
--- /dev/null
+++ b/public/pwa-192x192.svg
@@ -0,0 +1 @@
+
diff --git a/public/pwa-512x512.svg b/public/pwa-512x512.svg
new file mode 100644
index 0000000..4317fea
--- /dev/null
+++ b/public/pwa-512x512.svg
@@ -0,0 +1 @@
+
diff --git a/server/index.js b/server.js
similarity index 69%
rename from server/index.js
rename to server.js
index 0a7e85a..aa32979 100644
--- a/server/index.js
+++ b/server.js
@@ -13,6 +13,7 @@ const __dirname = dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3001;
+const IS_PRODUCTION = process.env.NODE_ENV === 'production';
// Ensure upload and export directories exist
const uploadsDir = join(__dirname, 'uploads');
@@ -21,8 +22,11 @@ const exportsDir = join(__dirname, 'exports');
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
});
-// Middleware
-app.use(cors());
+// Middleware — restrict CORS in production
+const corsOptions = IS_PRODUCTION
+ ? { origin: process.env.CORS_ORIGIN || false }
+ : { origin: true };
+app.use(cors(corsOptions));
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
@@ -30,36 +34,32 @@ app.use(express.urlencoded({ extended: true, limit: '50mb' }));
app.use('/uploads', express.static(uploadsDir));
app.use('/exports', express.static(exportsDir));
-// Serve built client static files
-const clientDistDir = join(__dirname, 'dist');
-if (existsSync(clientDistDir)) {
- app.use(express.static(clientDistDir));
-
- // Serve index.html for all non-API routes (SPA routing)
- app.get('*', (req, res, next) => {
- if (!req.path.startsWith('/api') && !req.path.startsWith('/uploads') && !req.path.startsWith('/exports')) {
- res.sendFile(join(clientDistDir, 'index.html'));
- } else {
- next();
- }
- });
+// In production, serve the Vite-built client
+if (IS_PRODUCTION) {
+ const clientDist = join(__dirname, 'dist');
+ app.use(express.static(clientDist));
}
+// Map MIME types to file extensions
+const MIME_TO_EXT = {
+ 'image/jpeg': 'jpg',
+ 'image/png': 'png',
+ 'image/webp': 'webp',
+};
+
// Configure multer for image uploads
const storage = multer.diskStorage({
- destination: (req, file, cb) => {
+ destination: (_req, _file, cb) => {
cb(null, uploadsDir);
},
- filename: (req, file, cb) => {
- const ext = file.originalname.split('.').pop();
- const filename = `${uuidv4()}.${ext}`;
- cb(null, filename);
+ filename: (_req, file, cb) => {
+ const ext = MIME_TO_EXT[file.mimetype] || 'bin';
+ cb(null, `${uuidv4()}.${ext}`);
},
});
-const fileFilter = (req, file, cb) => {
- const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
- if (allowedTypes.includes(file.mimetype)) {
+const fileFilter = (_req, file, cb) => {
+ if (MIME_TO_EXT[file.mimetype]) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, and WebP are allowed.'), false);
@@ -69,53 +69,45 @@ const fileFilter = (req, file, cb) => {
const upload = multer({
storage,
fileFilter,
- limits: {
- fileSize: 20 * 1024 * 1024, // 20MB
- },
+ limits: { fileSize: 20 * 1024 * 1024 },
});
-// Health check endpoint
-app.get('/api/health', (req, res) => {
+// ── API Routes ──────────────────────────────────────────────────────────────
+
+// Health check
+app.get('/api/health', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
-// Upload endpoint
+// Upload
app.post('/api/upload', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
- const originalPath = req.file.path;
const originalUrl = `/uploads/${req.file.filename}`;
// Create preview by resizing to max 1000px
- const previewFilename = req.file.filename.replace(/\.[^.]+$/, '.png');
- const previewPath = join(uploadsDir, 'preview', previewFilename);
-
- // Ensure preview directory exists
+ const previewFilename = `${uuidv4()}.png`;
const previewDir = join(uploadsDir, 'preview');
if (!existsSync(previewDir)) mkdirSync(previewDir, { recursive: true });
- await sharp(originalPath)
+ await sharp(req.file.path)
.resize({ width: 1000, height: 1000, fit: 'inside' })
.png()
- .toFile(previewPath);
-
- const previewUrl = `/uploads/preview/${previewFilename}`;
+ .toFile(join(previewDir, previewFilename));
res.json({
success: true,
original: {
- path: originalPath,
url: originalUrl,
filename: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype,
},
preview: {
- path: previewPath,
- url: previewUrl,
+ url: `/uploads/preview/${previewFilename}`,
filename: previewFilename,
},
});
@@ -125,18 +117,7 @@ app.post('/api/upload', upload.single('image'), async (req, res) => {
}
});
-// Error handling for multer
-app.use((err, req, res, next) => {
- if (err instanceof multer.MulterError) {
- if (err.code === 'LIMIT_FILE_SIZE') {
- return res.status(400).json({ error: 'File too large. Maximum size is 20MB.' });
- }
- return res.status(400).json({ error: err.message });
- }
- next(err);
-});
-
-// High-resolution export endpoint (300x300px -> 4500x4500px @ 300 DPI)
+// High-resolution export (300×300 → 4500×4500 @ 300 DPI)
const EXPORT_SCALE = 15;
const EXPORT_SIZE = 4500;
@@ -151,8 +132,8 @@ app.post('/api/export', async (req, res) => {
const canvas = createCanvas(EXPORT_SIZE, EXPORT_SIZE);
const ctx = canvas.getContext('2d');
- // Render template background layer first (if template active)
- if (template && template.background) {
+ // Template background
+ if (template?.background) {
const bg = template.background;
if (bg.type === 'color') {
ctx.fillStyle = bg.color;
@@ -171,12 +152,11 @@ app.post('/api/export', async (req, res) => {
}
}
} else {
- // Default white background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, EXPORT_SIZE, EXPORT_SIZE);
}
- // Helper function to render a single element
+ // Render a single element
const renderElement = async (el) => {
ctx.save();
@@ -186,7 +166,7 @@ app.post('/api/export', async (req, res) => {
const centerY = y + ((el.height || el.fontSize || 100) * EXPORT_SCALE) / 2;
ctx.translate(centerX, centerY);
- ctx.rotate((el.rotation || 0) * Math.PI / 180);
+ ctx.rotate(((el.rotation || 0) * Math.PI) / 180);
ctx.translate(-centerX, -centerY);
if (el.type === 'image' && el.src) {
@@ -198,14 +178,8 @@ app.post('/api/export', async (req, res) => {
const width = (el.width || 100) * EXPORT_SCALE;
const height = (el.height || 100) * EXPORT_SCALE;
- // Apply crop if slot crop region specified
if (el.crop) {
- const { sx, sy, sWidth, sHeight } = el.crop;
- ctx.drawImage(
- img,
- sx, sy, sWidth, sHeight, // source crop
- x, y, width, height // destination
- );
+ ctx.drawImage(img, el.crop.sx, el.crop.sy, el.crop.sWidth, el.crop.sHeight, x, y, width, height);
} else {
ctx.drawImage(img, x, y, width, height);
}
@@ -213,7 +187,7 @@ app.post('/api/export', async (req, res) => {
console.error('Failed to load image for export:', imgError);
}
} else if (el.type === 'text') {
- const fontSize = (el.fontSize || 32) * EXPORT_SCALE / 32;
+ const fontSize = ((el.fontSize || 32) * EXPORT_SCALE) / 32;
ctx.font = `${fontSize}px "${el.fontFamily || 'Arial'}"`;
ctx.fillStyle = el.fill || '#000000';
ctx.textAlign = 'center';
@@ -226,13 +200,12 @@ app.post('/api/export', async (req, res) => {
// Render user elements
for (const el of elements) {
- // Skip non-printable elements (guides, watermarks, template-only layers)
if (el.nonPrintable) continue;
await renderElement(el);
}
- // Render template overlay layer last (if template active)
- if (template && template.overlay) {
+ // Render template overlay
+ if (template?.overlay) {
for (const overlayEl of template.overlay) {
if (overlayEl.nonPrintable) continue;
await renderElement(overlayEl);
@@ -242,14 +215,12 @@ app.post('/api/export', async (req, res) => {
// Save to file
const exportFilename = `${designName.replace(/[^a-z0-9]/gi, '_')}_${uuidv4()}.png`;
const exportPath = join(exportsDir, exportFilename);
- const buffer = canvas.toBuffer('image/png');
- writeFileSync(exportPath, buffer);
+ writeFileSync(exportPath, canvas.toBuffer('image/png'));
res.json({
success: true,
export: {
url: `/exports/${exportFilename}`,
- path: exportPath,
filename: exportFilename,
width: EXPORT_SIZE,
height: EXPORT_SIZE,
@@ -263,8 +234,40 @@ app.post('/api/export', async (req, res) => {
}
});
+// Download
+app.get('/api/download/:filename', (req, res) => {
+ const filePath = join(exportsDir, req.params.filename);
+ if (!existsSync(filePath)) {
+ return res.status(404).json({ error: 'File not found' });
+ }
+ res.download(filePath);
+});
+
+// API 404 catch-all (before SPA catch-all)
+app.all('/api/*', (_req, res) => {
+ res.status(404).json({ error: 'API route not found' });
+});
+
+// In production, SPA catch-all
+if (IS_PRODUCTION) {
+ app.get('*', (_req, res) => {
+ res.sendFile(join(__dirname, 'dist', 'index.html'));
+ });
+}
+
+// Error handling
+app.use((err, _req, res, _next) => {
+ if (err instanceof multer.MulterError) {
+ if (err.code === 'LIMIT_FILE_SIZE') {
+ return res.status(400).json({ error: 'File too large. Maximum size is 20MB.' });
+ }
+ return res.status(400).json({ error: err.message });
+ }
+ console.error('Unhandled error:', err);
+ res.status(500).json({ error: 'Internal server error', details: err.message });
+});
+
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
- console.log(`Health check: http://localhost:${PORT}/api/health`);
- console.log(`Upload endpoint: POST http://localhost:${PORT}/api/upload`);
+ console.log(`Mode: ${IS_PRODUCTION ? 'production' : 'development'}`);
});
diff --git a/server/package-lock.json b/server/package-lock.json
deleted file mode 100644
index 632a047..0000000
--- a/server/package-lock.json
+++ /dev/null
@@ -1,1525 +0,0 @@
-{
- "name": "apparel-designer-server",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "apparel-designer-server",
- "version": "1.0.0",
- "dependencies": {
- "cors": "^2.8.5",
- "express": "^4.18.2",
- "multer": "^1.4.5-lts.1",
- "sharp": "^0.33.2",
- "uuid": "^9.0.1"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
- "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@img/sharp-darwin-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
- "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-darwin-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
- "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
- "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
- "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
- "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
- "cpu": [
- "arm"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
- "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
- "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
- "cpu": [
- "s390x"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
- "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
- "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
- "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-linux-arm": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
- "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
- "cpu": [
- "arm"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.0.5"
- }
- },
- "node_modules/@img/sharp-linux-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
- "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-linux-s390x": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
- "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
- "cpu": [
- "s390x"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.0.4"
- }
- },
- "node_modules/@img/sharp-linux-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
- "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
- "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
- "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-wasm32": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
- "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
- "cpu": [
- "wasm32"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/runtime": "^1.2.0"
- },
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-ia32": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
- "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
- "cpu": [
- "ia32"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
- "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/append-field": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
- "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
- "license": "MIT"
- },
- "node_modules/array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
- "license": "MIT"
- },
- "node_modules/body-parser": {
- "version": "1.20.4",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
- "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "content-type": "~1.0.5",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "~1.2.0",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.4.24",
- "on-finished": "~2.4.1",
- "qs": "~6.14.0",
- "raw-body": "~2.5.3",
- "type-is": "~1.6.18",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "license": "MIT"
- },
- "node_modules/busboy": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
- "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
- "dependencies": {
- "streamsearch": "^1.1.0"
- },
- "engines": {
- "node": ">=10.16.0"
- }
- },
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/color": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
- "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1",
- "color-string": "^1.9.0"
- },
- "engines": {
- "node": ">=12.5.0"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "license": "MIT"
- },
- "node_modules/color-string": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
- "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "^1.0.0",
- "simple-swizzle": "^0.2.2"
- }
- },
- "node_modules/concat-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
- "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
- "engines": [
- "node >= 0.8"
- ],
- "license": "MIT",
- "dependencies": {
- "buffer-from": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^2.2.2",
- "typedarray": "^0.0.6"
- }
- },
- "node_modules/content-disposition": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
- "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "5.2.1"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
- "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
- "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
- "license": "MIT"
- },
- "node_modules/core-util-is": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "license": "MIT"
- },
- "node_modules/cors": {
- "version": "2.8.6",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
- "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
- "license": "MIT",
- "dependencies": {
- "object-assign": "^4",
- "vary": "^1"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/destroy": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
- "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "license": "MIT"
- },
- "node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/express": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
- "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
- "license": "MIT",
- "dependencies": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "~1.20.3",
- "content-disposition": "~0.5.4",
- "content-type": "~1.0.4",
- "cookie": "~0.7.1",
- "cookie-signature": "~1.0.6",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "~1.3.1",
- "fresh": "~0.5.2",
- "http-errors": "~2.0.0",
- "merge-descriptors": "1.0.3",
- "methods": "~1.1.2",
- "on-finished": "~2.4.1",
- "parseurl": "~1.3.3",
- "path-to-regexp": "~0.1.12",
- "proxy-addr": "~2.0.7",
- "qs": "~6.14.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "~0.19.0",
- "serve-static": "~1.16.2",
- "setprototypeof": "1.2.0",
- "statuses": "~2.0.1",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/finalhandler": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
- "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "on-finished": "~2.4.1",
- "parseurl": "~1.3.3",
- "statuses": "~2.0.2",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "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==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
- "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC"
- },
- "node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/is-arrayish": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
- "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
- "license": "MIT"
- },
- "node_modules/isarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
- "license": "MIT"
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
- "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "license": "MIT",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/mkdirp": {
- "version": "0.5.6",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
- "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
- "license": "MIT",
- "dependencies": {
- "minimist": "^1.2.6"
- },
- "bin": {
- "mkdirp": "bin/cmd.js"
- }
- },
- "node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/multer": {
- "version": "1.4.5-lts.2",
- "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
- "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
- "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
- "license": "MIT",
- "dependencies": {
- "append-field": "^1.0.0",
- "busboy": "^1.0.0",
- "concat-stream": "^1.5.2",
- "mkdirp": "^0.5.4",
- "object-assign": "^4.1.1",
- "type-is": "^1.6.4",
- "xtend": "^4.0.0"
- },
- "engines": {
- "node": ">= 6.0.0"
- }
- },
- "node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "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==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "0.1.13",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
- "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
- "license": "MIT"
- },
- "node_modules/process-nextick-args": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "license": "MIT"
- },
- "node_modules/proxy-addr": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
- "license": "MIT",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/qs": {
- "version": "6.14.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
- "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "side-channel": "^1.1.0"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/raw-body": {
- "version": "2.5.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
- "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.4.24",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/readable-stream": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
- "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
- "license": "MIT",
- "dependencies": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "node_modules/readable-stream/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "license": "MIT"
- },
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "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/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "license": "MIT"
- },
- "node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/send": {
- "version": "0.19.2",
- "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
- "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "~0.5.2",
- "http-errors": "~2.0.1",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "~2.4.1",
- "range-parser": "~1.2.1",
- "statuses": "~2.0.2"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/send/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==",
- "license": "MIT"
- },
- "node_modules/serve-static": {
- "version": "1.16.3",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
- "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
- "license": "MIT",
- "dependencies": {
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "~0.19.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "license": "ISC"
- },
- "node_modules/sharp": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
- "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
- "hasInstallScript": true,
- "license": "Apache-2.0",
- "dependencies": {
- "color": "^4.2.3",
- "detect-libc": "^2.0.3",
- "semver": "^7.6.3"
- },
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-darwin-arm64": "0.33.5",
- "@img/sharp-darwin-x64": "0.33.5",
- "@img/sharp-libvips-darwin-arm64": "1.0.4",
- "@img/sharp-libvips-darwin-x64": "1.0.4",
- "@img/sharp-libvips-linux-arm": "1.0.5",
- "@img/sharp-libvips-linux-arm64": "1.0.4",
- "@img/sharp-libvips-linux-s390x": "1.0.4",
- "@img/sharp-libvips-linux-x64": "1.0.4",
- "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
- "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
- "@img/sharp-linux-arm": "0.33.5",
- "@img/sharp-linux-arm64": "0.33.5",
- "@img/sharp-linux-s390x": "0.33.5",
- "@img/sharp-linux-x64": "0.33.5",
- "@img/sharp-linuxmusl-arm64": "0.33.5",
- "@img/sharp-linuxmusl-x64": "0.33.5",
- "@img/sharp-wasm32": "0.33.5",
- "@img/sharp-win32-ia32": "0.33.5",
- "@img/sharp-win32-x64": "0.33.5"
- }
- },
- "node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-list": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
- "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/simple-swizzle": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
- "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
- "license": "MIT",
- "dependencies": {
- "is-arrayish": "^0.3.1"
- }
- },
- "node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/streamsearch": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
- "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "~5.1.0"
- }
- },
- "node_modules/string_decoder/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "license": "MIT"
- },
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD",
- "optional": true
- },
- "node_modules/type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "license": "MIT",
- "dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/typedarray": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
- "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
- "license": "MIT"
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "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==",
- "license": "MIT"
- },
- "node_modules/utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/uuid": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
- "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
- "funding": [
- "https://github.com/sponsors/broofa",
- "https://github.com/sponsors/ctavan"
- ],
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4"
- }
- }
- }
-}
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..7d04987
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1 @@
+/* App-level styles — project styles go here */
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 0000000..fc8f605
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,138 @@
+import { useEffect, useState } from 'react';
+import { DesignCanvas } from './components/canvas/DesignCanvas';
+import { Sidebar } from './components/sidebar/Sidebar';
+import { LayersPanel } from './components/panels/LayersPanel';
+import { PropertiesPanel } from './components/panels/PropertiesPanel';
+import { PWAInstall } from './components/PWAInstall';
+import { OfflineIndicator } from './components/OfflineIndicator';
+import { PhotoPreEditor } from './components/editor/PhotoPreEditor';
+import { useDesignEditor } from './hooks/useDesignEditor';
+import { useExport } from './hooks/useExport';
+import { useTemplate } from './hooks/useTemplate';
+import { TEMPLATES } from './constants/templates';
+
+function App() {
+ const [editingElement, setEditingElement] = useState(null);
+
+ const {
+ elements, selectedId, addElement, updateElement, deleteElement,
+ selectElement, deselectAll, commitHistory, undo, redo, canUndo, canRedo, initializeHistory,
+ } = useDesignEditor();
+
+ const { exporting, progress, exportDesign, error, clearExport } = useExport();
+
+ const {
+ currentTemplate, currentTemplateId, assignedSlots,
+ loadTemplate, clearTemplate, getSlots, assignImageToSlot, getDragBoundFunc, isSlotFilled,
+ } = useTemplate(TEMPLATES);
+
+ const selectedElement = elements.find((el) => el.id === selectedId);
+
+ useEffect(() => { initializeHistory(); }, [initializeHistory]);
+
+ // Keyboard shortcuts
+ useEffect(() => {
+ const handleKeyDown = (e) => {
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
+ e.preventDefault();
+ if (canUndo) undo();
+ return;
+ }
+ if ((e.ctrlKey || e.metaKey) && ((e.key === 'z' && e.shiftKey) || e.key === 'y')) {
+ e.preventDefault();
+ if (canRedo) redo();
+ return;
+ }
+ if ((e.key === 'Delete' || e.key === 'Backspace') && selectedId) {
+ deleteElement(selectedId);
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [selectedId, deleteElement, undo, redo, canUndo, canRedo]);
+
+ const handleAddTemplate = (templateId) => {
+ if (templateId === 'freeform') { clearTemplate(); return; }
+ const success = loadTemplate(templateId);
+ if (success) {
+ const template = TEMPLATES.find(t => t.id === templateId);
+ if (template?.elements) {
+ template.elements.forEach((el, index) => {
+ setTimeout(() => addElement({ ...el }), index * 50);
+ });
+ }
+ }
+ };
+
+ const handleSlotImageUpload = (slotId, imageData) => {
+ const elementData = assignImageToSlot(slotId, imageData);
+ if (elementData) addElement(elementData);
+ };
+
+ return (
+
+
+
+
+
addElement(data)}
+ onAddSticker={(data) => addElement(data)}
+ onAddText={(data) => addElement(data)}
+ onAddTemplate={handleAddTemplate}
+ onSlotImageUpload={handleSlotImageUpload}
+ />
+
+
+
+
Apparel Designer
+
T-shirt customization editor
+
+
+
+
+
+
+
+
+ {error && (
+
+
⚠️ Export failed: {error}
+
+
+ )}
+
+
updateElement(id, attrs)} onCommit={commitHistory}
+ currentTemplate={currentTemplate} assignedSlots={assignedSlots} getDragBoundFunc={getDragBoundFunc}
+ />
+
+
+
+
+
+
+ updateElement(selectedId, attrs)}
+ onDelete={deleteElement}
+ onEditPhoto={(el) => setEditingElement(el)}
+ />
+
+ {editingElement && (
+ { updateElement(editingElement.id, { src: url }); setEditingElement(null); }}
+ onClose={() => setEditingElement(null)}
+ />
+ )}
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/OfflineIndicator.jsx b/src/components/OfflineIndicator.jsx
similarity index 58%
rename from client/src/components/OfflineIndicator.jsx
rename to src/components/OfflineIndicator.jsx
index 842b43c..8e6f437 100644
--- a/client/src/components/OfflineIndicator.jsx
+++ b/src/components/OfflineIndicator.jsx
@@ -6,22 +6,11 @@ export function OfflineIndicator() {
useEffect(() => {
const handleOnline = () => setIsOffline(false);
const handleOffline = () => setIsOffline(true);
-
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
-
- return () => {
- window.removeEventListener('online', handleOnline);
- window.removeEventListener('offline', handleOffline);
- };
+ return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); };
}, []);
if (!isOffline) return null;
-
- return (
-
- ⚠️
- You're offline - changes are saved locally
-
- );
+ return ⚠️You're offline — changes are saved locally
;
}
diff --git a/src/components/PWAInstall.jsx b/src/components/PWAInstall.jsx
new file mode 100644
index 0000000..e2ec866
--- /dev/null
+++ b/src/components/PWAInstall.jsx
@@ -0,0 +1,48 @@
+import { useState, useEffect } from 'react';
+
+export function PWAInstall() {
+ const [deferredPrompt, setDeferredPrompt] = useState(null);
+ const [showInstall, setShowInstall] = useState(false);
+ const [updateAvailable, setUpdateAvailable] = useState(false);
+ const [newWorker, setNewWorker] = useState(null);
+
+ useEffect(() => {
+ const handleBeforeInstallPrompt = (e) => { e.preventDefault(); setDeferredPrompt(e); setShowInstall(true); };
+ const handleSWUpdated = (event) => { setNewWorker(event.detail); setUpdateAvailable(true); };
+ window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
+ window.addEventListener('swUpdated', handleSWUpdated);
+ return () => { window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); window.removeEventListener('swUpdated', handleSWUpdated); };
+ }, []);
+
+ const handleInstall = async () => {
+ if (!deferredPrompt) return;
+ deferredPrompt.prompt();
+ const { outcome } = await deferredPrompt.userChoice;
+ if (outcome === 'accepted') { setShowInstall(false); setDeferredPrompt(null); }
+ };
+
+ const handleUpdate = () => { if (newWorker) { newWorker.postMessage({ type: 'SKIP_WAITING' }); window.location.reload(); } };
+
+ if (!showInstall && !updateAvailable) return null;
+
+ return (
+ <>
+ {showInstall && (
+
+
Install Apparel Designer for offline access!
+
+
+
+
+
+ )}
+ {updateAvailable && (
+
+ 🔄 New version available!
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/canvas/DesignCanvas.jsx b/src/components/canvas/DesignCanvas.jsx
new file mode 100644
index 0000000..524068c
--- /dev/null
+++ b/src/components/canvas/DesignCanvas.jsx
@@ -0,0 +1,66 @@
+import { Stage, Layer } from 'react-konva';
+import { TShirtSVG } from './TShirtSVG';
+import { ImageElement } from './ImageElement';
+import { TextElement } from './TextElement';
+import { TemplateLayer } from './TemplateLayer';
+import { SlotPlaceholder, SlotBoundsGuide } from './SlotPlaceholder';
+import { memo } from 'react';
+
+const CANVAS_SIZE = 300;
+
+export const DesignCanvas = memo(function DesignCanvas({
+ elements, selectedId, onSelect, onDeselect, onUpdate, onCommit,
+ currentTemplate, assignedSlots, getDragBoundFunc,
+}) {
+ const slots = currentTemplate?.slots || [];
+
+ return (
+
+
+
+
+ {currentTemplate && }
+
+
+ {slots.map((slot) => )}
+
+
+ {elements.map((el) => {
+ if (el.type === 'image') {
+ return (
+ onSelect(el.id)} onUpdate={(attrs) => onUpdate(el.id, attrs)} onCommit={onCommit}
+ dragBoundFunc={el.slotId ? getDragBoundFunc?.(el.slotId, { width: el.width, height: el.height }) : null}
+ />
+ );
+ }
+ if (el.type === 'text') {
+ return (
+ onSelect(el.id)} onUpdate={(attrs) => onUpdate(el.id, attrs)} onCommit={onCommit}
+ />
+ );
+ }
+ return null;
+ })}
+
+
+ {slots.map((slot) => )}
+
+
+
+ Design Area: 15" × 15" • Export: 4500 × 4500px @ 300 DPI
+
+
+ );
+});
diff --git a/client/src/components/canvas/ImageElement.jsx b/src/components/canvas/ImageElement.jsx
similarity index 63%
rename from client/src/components/canvas/ImageElement.jsx
rename to src/components/canvas/ImageElement.jsx
index 5971bb3..312074d 100644
--- a/client/src/components/canvas/ImageElement.jsx
+++ b/src/components/canvas/ImageElement.jsx
@@ -1,30 +1,30 @@
-import { useEffect, useState, memo } from 'react';
+import { useEffect, useRef, memo } from 'react';
import { Image, Transformer } from 'react-konva';
import useImage from 'use-image';
-function URLImage({ src, ...props }) {
+function URLImage({ src, innerRef, ...props }) {
const [img] = useImage(src, 'anonymous');
- return ;
+ return ;
}
export const ImageElement = memo(function ImageElement({
id,
- x,
- y,
- width,
- height,
- rotation,
+ x = 0,
+ y = 0,
+ width = 100,
+ height = 100,
+ rotation = 0,
src,
isSelected,
onSelect,
onUpdate,
onCommit,
}) {
- const shapeRef = null;
- const trRef = null;
+ const shapeRef = useRef(null);
+ const trRef = useRef(null);
useEffect(() => {
- if (isSelected && trRef.current) {
+ if (isSelected && trRef.current && shapeRef.current) {
trRef.current.nodes([shapeRef.current]);
trRef.current.getLayer().batchDraw();
}
@@ -33,7 +33,7 @@ export const ImageElement = memo(function ImageElement({
return (
<>
{
- onUpdate({
- x: e.target.x(),
- y: e.target.y(),
- });
+ onUpdate({ x: e.target.x(), y: e.target.y() });
onCommit?.();
}}
- onTransformEnd={(e) => {
+ onTransformEnd={() => {
const node = shapeRef.current;
+ if (!node) return;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
node.scaleX(1);
@@ -65,22 +63,12 @@ export const ImageElement = memo(function ImageElement({
});
onCommit?.();
}}
- boundBoxFunc={(oldBox, newBox) => {
- // Minimum size constraint
- if (newBox.width < 20 || newBox.height < 20) {
- return oldBox;
- }
- return newBox;
- }}
/>
{isSelected && (
{
- // Limit resize to minimum size
- if (newBox.width < 20 || newBox.height < 20) {
- return oldBox;
- }
+ if (newBox.width < 20 || newBox.height < 20) return oldBox;
return newBox;
}}
anchorSize={8}
@@ -92,10 +80,4 @@ export const ImageElement = memo(function ImageElement({
)}
>
);
-}
-
-ImageElement.defaultProps = {
- width: 100,
- height: 100,
- rotation: 0,
});
diff --git a/src/components/canvas/SlotPlaceholder.jsx b/src/components/canvas/SlotPlaceholder.jsx
new file mode 100644
index 0000000..fac0f10
--- /dev/null
+++ b/src/components/canvas/SlotPlaceholder.jsx
@@ -0,0 +1,33 @@
+import { Group, Rect, Text, Line } from 'react-konva';
+
+export function SlotPlaceholder({ slot, isEmpty = true }) {
+ const { bounds, label } = slot;
+ const { x, y, width, height } = bounds;
+ if (!isEmpty) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function SlotBoundsGuide({ slot }) {
+ const { bounds, id } = slot;
+ return (
+
+
+
+ );
+}
diff --git a/src/components/canvas/TShirtSVG.jsx b/src/components/canvas/TShirtSVG.jsx
new file mode 100644
index 0000000..a8d5c2b
--- /dev/null
+++ b/src/components/canvas/TShirtSVG.jsx
@@ -0,0 +1,14 @@
+export function TShirtSVG({ size = 300 }) {
+ const padding = size * 0.1;
+ const innerSize = size - padding * 2;
+
+ return (
+
+ );
+}
diff --git a/src/components/canvas/TemplateLayer.jsx b/src/components/canvas/TemplateLayer.jsx
new file mode 100644
index 0000000..6c0da59
--- /dev/null
+++ b/src/components/canvas/TemplateLayer.jsx
@@ -0,0 +1,37 @@
+import { Group, Image as KonvaImage, Rect, Text as KonvaText } from 'react-konva';
+import useImage from 'use-image';
+
+function TemplateImage({ src, x, y, width, height, opacity = 1, listening = false }) {
+ const [img] = useImage(src, 'anonymous');
+ return ;
+}
+
+function TemplateText({ text, x, y, fontSize, fontFamily, fill, rotation = 0 }) {
+ return ;
+}
+
+export function TemplateLayer({ template, canvasSize = 300 }) {
+ if (!template) return null;
+ const { background, overlay } = template;
+
+ return (
+
+ {background && (
+
+ {background.type === 'color' ? (
+
+ ) : background.type === 'image' ? (
+
+ ) : null}
+
+ )}
+ {overlay && overlay.map((el, index) => {
+ if (el.nonPrintable) return null;
+ const key = `overlay-${index}`;
+ if (el.type === 'image') return ;
+ if (el.type === 'text') return ;
+ return null;
+ })}
+
+ );
+}
diff --git a/client/src/components/canvas/TextElement.jsx b/src/components/canvas/TextElement.jsx
similarity index 62%
rename from client/src/components/canvas/TextElement.jsx
rename to src/components/canvas/TextElement.jsx
index 9cd3cdf..67d98e7 100644
--- a/client/src/components/canvas/TextElement.jsx
+++ b/src/components/canvas/TextElement.jsx
@@ -1,38 +1,25 @@
-import { useEffect, memo } from 'react';
+import { useEffect, useRef, memo } from 'react';
import { Text, Transformer } from 'react-konva';
export const TextElement = memo(function TextElement({
id,
- x,
- y,
- text,
- fontSize,
- fontFamily,
- fill,
- rotation,
+ x = 0,
+ y = 0,
+ text = '',
+ fontSize = 24,
+ fontFamily = 'DM Sans',
+ fill = '#0f172a',
+ rotation = 0,
isSelected,
onSelect,
onUpdate,
onCommit,
-}, prevProps) {
- // Custom comparison for memo
- if (!prevProps) return true;
- return (
- prevProps.x === x &&
- prevProps.y === y &&
- prevProps.text === text &&
- prevProps.fontSize === fontSize &&
- prevProps.fontFamily === fontFamily &&
- prevProps.fill === fill &&
- prevProps.rotation === rotation &&
- prevProps.isSelected === isSelected
- );
-});
- const textRef = null;
- const trRef = null;
+}) {
+ const textRef = useRef(null);
+ const trRef = useRef(null);
useEffect(() => {
- if (isSelected && trRef.current) {
+ if (isSelected && trRef.current && textRef.current) {
trRef.current.nodes([textRef.current]);
trRef.current.getLayer().batchDraw();
}
@@ -53,14 +40,12 @@ export const TextElement = memo(function TextElement({
onClick={onSelect}
onTap={onSelect}
onDragEnd={(e) => {
- onUpdate({
- x: e.target.x(),
- y: e.target.y(),
- });
+ onUpdate({ x: e.target.x(), y: e.target.y() });
onCommit?.();
}}
- onTransformEnd={(e) => {
+ onTransformEnd={() => {
const node = textRef.current;
+ if (!node) return;
const scaleX = node.scaleX();
node.scaleX(1);
node.scaleY(1);
@@ -86,11 +71,4 @@ export const TextElement = memo(function TextElement({
)}
>
);
-}
-
-TextElement.defaultProps = {
- fontSize: 24,
- fontFamily: 'DM Sans',
- fill: '#0f172a',
- rotation: 0,
});
diff --git a/client/src/components/canvas/index.js b/src/components/canvas/index.js
similarity index 100%
rename from client/src/components/canvas/index.js
rename to src/components/canvas/index.js
diff --git a/src/components/editor/PhotoPreEditor.jsx b/src/components/editor/PhotoPreEditor.jsx
new file mode 100644
index 0000000..a444356
--- /dev/null
+++ b/src/components/editor/PhotoPreEditor.jsx
@@ -0,0 +1,43 @@
+import { useState, useEffect, useRef } from 'react';
+import FilerobotImageEditor from 'react-filerobot-image-editor';
+
+export function PhotoPreEditor({ imageSrc, onComplete, onClose }) {
+ const [saving, setSaving] = useState(false);
+ const modalContentRef = useRef(null);
+ const previousFocusRef = useRef(null);
+
+ useEffect(() => {
+ previousFocusRef.current = document.activeElement;
+ const handleKeyDown = (e) => { if (e.key === 'Escape') onClose(); };
+ document.addEventListener('keydown', handleKeyDown);
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ previousFocusRef.current?.focus();
+ };
+ }, [onClose]);
+
+ const handleComplete = (editedImageObject) => {
+ setSaving(true);
+ editedImageObject.exportAsync({ quality: 1, mimeType: 'image/png' })
+ .then((blob) => { setSaving(false); onComplete(URL.createObjectURL(blob)); })
+ .catch((error) => { console.error('Export failed:', error); setSaving(false); onClose(); });
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/editor/index.js b/src/components/editor/index.js
similarity index 100%
rename from client/src/components/editor/index.js
rename to src/components/editor/index.js
diff --git a/src/components/panels/LayersPanel.jsx b/src/components/panels/LayersPanel.jsx
new file mode 100644
index 0000000..751b954
--- /dev/null
+++ b/src/components/panels/LayersPanel.jsx
@@ -0,0 +1,36 @@
+import { memo } from 'react';
+
+export const LayersPanel = memo(function LayersPanel({ elements, selectedId, onSelect, onDelete }) {
+ const getIcon = (el) => el.type === 'image' ? (el.bgRemoved ? '🖼️' : '📷') : el.type === 'text' ? '📝' : '🎨';
+ const getName = (el) => el.type === 'image' ? (el.bgRemoved ? 'Image (BG ✓)' : 'Image') : el.type === 'text' ? (el.text?.substring(0, 20) || 'Text') : 'Sticker';
+
+ if (elements.length === 0) {
+ return No elements yet. Add images, text, or stickers to your design.
;
+ }
+
+ return (
+
+
Layers ({elements.length})
+
+ {elements.map((element) => (
+
onSelect(element.id)}
+ style={{
+ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0.75rem',
+ background: selectedId === element.id ? 'var(--accent-bg)' : 'transparent',
+ border: `1px solid ${selectedId === element.id ? 'var(--accent)' : 'var(--border)'}`,
+ borderRadius: 'var(--radius-sm)', cursor: 'pointer',
+ }}>
+ {getIcon(element)}
+
+ {getName(element)}
+
+
+
+ ))}
+
+
+ );
+});
diff --git a/src/components/panels/PropertiesPanel.jsx b/src/components/panels/PropertiesPanel.jsx
new file mode 100644
index 0000000..519bc5c
--- /dev/null
+++ b/src/components/panels/PropertiesPanel.jsx
@@ -0,0 +1,113 @@
+import { memo } from 'react';
+import { BackgroundRemovalButton } from '../sidebar/BackgroundRemovalButton';
+
+export const PropertiesPanel = memo(function PropertiesPanel({ element, onUpdate, onDelete, onEditPhoto }) {
+ if (!element) {
+ return (
+
+
+
Properties
+
+
+ Select an element to edit its properties
+
+
+ );
+ }
+
+ const handlePositionChange = (axis, value) => onUpdate({ [axis]: parseFloat(value) || 0 });
+ const handleSizeChange = (axis, value) => onUpdate({ [axis]: Math.max(20, parseFloat(value) || 20) });
+ const handleRotationChange = (value) => onUpdate({ rotation: Math.max(-180, Math.min(180, parseFloat(value) || 0)) });
+
+ const inputStyle = { width: '100%', padding: '0.5rem', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', fontSize: '13px' };
+ const labelStyle = { display: 'block', fontSize: '11px', fontWeight: '600', color: 'var(--text-secondary)', marginBottom: '0.5rem', textTransform: 'uppercase' };
+
+ return (
+
+
+
Properties
+
+
+
+ {/* Element type badge */}
+
+ {element.type}
+
+
+ {/* Position */}
+
+
+ {/* Size (for images and stickers) */}
+ {(element.type === 'image' || element.type === 'sticker') && (
+
+ )}
+
+ {/* Edit Photo button */}
+ {element.type === 'image' && onEditPhoto && (
+
+
+
+ )}
+
+ {/* Text-specific controls */}
+ {element.type === 'text' && (
+ <>
+
+
+ onUpdate({ fontSize: parseInt(e.target.value, 10) })} style={{ width: '100%' }} />
+
+
+
+ onUpdate({ fill: e.target.value })} style={{ width: '100%', height: '36px', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer', padding: '2px' }} />
+
+ >
+ )}
+
+ {/* Rotation */}
+
+
+ handleRotationChange(e.target.value)} style={{ width: '100%' }} />
+
+
+ {/* Background Removal (for images) */}
+ {element.type === 'image' && (
+
onUpdate(attrs)}
+ />
+ )}
+
+ {/* Delete */}
+
+
+
+ );
+});
diff --git a/client/src/components/panels/index.js b/src/components/panels/index.js
similarity index 100%
rename from client/src/components/panels/index.js
rename to src/components/panels/index.js
diff --git a/src/components/sidebar/BackgroundRemovalButton.jsx b/src/components/sidebar/BackgroundRemovalButton.jsx
new file mode 100644
index 0000000..3f26a84
--- /dev/null
+++ b/src/components/sidebar/BackgroundRemovalButton.jsx
@@ -0,0 +1,23 @@
+import { useBackgroundRemoval } from '../../hooks/useBackgroundRemoval';
+
+export function BackgroundRemovalButton({ selectedElement, onUpdate }) {
+ const { loading, progress, hasModel, loadModel, removeBackground } = useBackgroundRemoval();
+
+ const handleRemoveBackground = async () => {
+ if (!selectedElement || selectedElement.type !== 'image') return;
+ if (!hasModel) { const loaded = await loadModel(); if (!loaded) return; }
+ const resultUrl = await removeBackground(selectedElement.src);
+ if (resultUrl) onUpdate(selectedElement.id, { src: resultUrl, bgRemoved: true });
+ };
+
+ if (!selectedElement || selectedElement.type !== 'image') return null;
+
+ return (
+
+
+ {!hasModel &&
First use requires downloading ~86MB model. Subsequent uses are cached.
}
+
+ );
+}
diff --git a/src/components/sidebar/Sidebar.jsx b/src/components/sidebar/Sidebar.jsx
new file mode 100644
index 0000000..b605c0b
--- /dev/null
+++ b/src/components/sidebar/Sidebar.jsx
@@ -0,0 +1,46 @@
+import { useState } from 'react';
+import { UploadTab } from './UploadTab';
+import { StickersTab } from './StickersTab';
+import { TextTab } from './TextTab';
+import { TemplatesTab } from './TemplatesTab';
+
+const TABS = [
+ { id: 'upload', label: 'Upload', icon: '📁' },
+ { id: 'stickers', label: 'Stickers', icon: '🎨' },
+ { id: 'text', label: 'Text', icon: '📝' },
+ { id: 'templates', label: 'Templates', icon: '📋' },
+];
+
+export function Sidebar({ onAddImage, onAddSticker, onAddText, onAddTemplate, onSlotImageUpload }) {
+ const [activeTab, setActiveTab] = useState('upload');
+
+ const renderTabContent = () => {
+ switch (activeTab) {
+ case 'upload': return ;
+ case 'stickers': return ;
+ case 'text': return ;
+ case 'templates': return ;
+ default: return null;
+ }
+ };
+
+ return (
+
+
+ {TABS.map((tab) => (
+
+ ))}
+
+
{renderTabContent()}
+
+ );
+}
diff --git a/src/components/sidebar/StickersTab.jsx b/src/components/sidebar/StickersTab.jsx
new file mode 100644
index 0000000..45c8775
--- /dev/null
+++ b/src/components/sidebar/StickersTab.jsx
@@ -0,0 +1,57 @@
+import { useState } from 'react';
+import { STICKERS, STICKER_CATEGORIES } from '../../constants/stickers';
+
+export function StickersTab({ onAddSticker }) {
+ const [activeCategory, setActiveCategory] = useState('all');
+
+ const filteredStickers = activeCategory === 'all'
+ ? STICKERS
+ : STICKERS.filter(s => s.category === activeCategory);
+
+ const handleAddSticker = (emoji) => {
+ const canvas = document.createElement('canvas');
+ const size = 100;
+ canvas.width = size;
+ canvas.height = size;
+ const ctx = canvas.getContext('2d');
+ ctx.font = `${size * 0.8}px Arial`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(emoji, size / 2, size / 2);
+
+ onAddSticker({
+ type: 'image',
+ x: 125, y: 125, width: 80, height: 80, rotation: 0,
+ src: canvas.toDataURL('image/png'),
+ emoji,
+ });
+ };
+
+ return (
+
+
Stickers
+
+ {STICKER_CATEGORIES.map((cat) => (
+
+ ))}
+
+
+ {filteredStickers.map((sticker, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/sidebar/TemplatesTab.jsx b/src/components/sidebar/TemplatesTab.jsx
new file mode 100644
index 0000000..17e0b77
--- /dev/null
+++ b/src/components/sidebar/TemplatesTab.jsx
@@ -0,0 +1,118 @@
+import { useState } from 'react';
+import { TEMPLATES, TEMPLATE_CATEGORIES } from '../../constants/templates';
+
+function getCategoryEmoji(category) {
+ const emojis = {
+ Sports: '⚽', Music: '🎸', Quotes: '💬', Animals: '🐱',
+ Abstract: '🌈', Vintage: '🏅', Nature: '🏔️', Tech: '💻',
+ };
+ return emojis[category] || '🎨';
+}
+
+export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
+ const [selectedTemplateId, setSelectedTemplateId] = useState(null);
+ const [uploadSlotId, setUploadSlotId] = useState(null);
+
+ const templates = [
+ { id: 'freeform', name: 'Freeform', description: 'No template - design freely', thumbnail: '🎨' },
+ ...TEMPLATES.map(t => ({
+ id: t.id,
+ name: t.name,
+ description: t.description,
+ thumbnail: getCategoryEmoji(t.category),
+ hasSlots: !!t.slots,
+ })),
+ ];
+
+ const handleSelectTemplate = (template) => {
+ setSelectedTemplateId(template.id);
+ onAddTemplate(template.id);
+ };
+
+ const handleSlotClick = (slotId) => {
+ setUploadSlotId(slotId);
+ document.getElementById('slot-file-input')?.click();
+ };
+
+ const handleFileChange = (e) => {
+ const file = e.target.files?.[0];
+ if (file && uploadSlotId) {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ onSlotImageUpload?.(uploadSlotId, event.target.result);
+ };
+ reader.readAsDataURL(file);
+ }
+ e.target.value = '';
+ setUploadSlotId(null);
+ };
+
+ const selectedTemplate = TEMPLATES.find(t => t.id === selectedTemplateId);
+ const slots = selectedTemplate?.slots || [];
+
+ return (
+
+
+
+
Templates
+
+
+ Choose a template to get started or design freely.
+
+
+
+ {templates.map((template) => (
+
+ ))}
+
+
+ {selectedTemplateId && selectedTemplateId !== 'freeform' && slots.length > 0 && (
+
+
Template Slots
+
+ {slots.map((slot) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/sidebar/TextTab.jsx b/src/components/sidebar/TextTab.jsx
new file mode 100644
index 0000000..743dcd6
--- /dev/null
+++ b/src/components/sidebar/TextTab.jsx
@@ -0,0 +1,55 @@
+import { useState } from 'react';
+import { FONTS } from '../../constants/fonts';
+
+export function TextTab({ onAddText }) {
+ const [text, setText] = useState('Your text here');
+ const [fontFamily, setFontFamily] = useState('Roboto');
+ const [fontSize, setFontSize] = useState(48);
+ const [fill, setFill] = useState('#0f172a');
+
+ const handleAddText = () => {
+ onAddText({ type: 'text', x: 150, y: 150, text, fontFamily, fontSize, fill, rotation: 0 });
+ };
+
+ const labelStyle = { display: 'block', fontSize: '11px', fontWeight: '600', color: 'var(--text-secondary)', marginBottom: '0.5rem', textTransform: 'uppercase' };
+ const inputStyle = { width: '100%', padding: '0.75rem', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', fontSize: '14px', fontFamily: 'var(--font-body)' };
+
+ return (
+
+
Add Text
+
+
+
+
+
+
+
+
+
+
+
+
+ setFontSize(parseInt(e.target.value, 10))} style={{ width: '100%' }} />
+
+
+
+
+
+ setFill(e.target.value)} style={{ width: '40px', height: '40px', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer', padding: '2px' }} />
+ setFill(e.target.value)} style={{ flex: 1, padding: '0.75rem', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', fontSize: '13px', fontFamily: 'var(--font-mono)' }} />
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/sidebar/UploadTab.jsx b/src/components/sidebar/UploadTab.jsx
new file mode 100644
index 0000000..176801a
--- /dev/null
+++ b/src/components/sidebar/UploadTab.jsx
@@ -0,0 +1,47 @@
+import { useRef, useState } from 'react';
+
+export function UploadTab({ onAddImage }) {
+ const fileInputRef = useRef(null);
+ const [isDragging, setIsDragging] = useState(false);
+ const [isUploading, setIsUploading] = useState(false);
+
+ const handleFiles = async (files) => {
+ const file = files[0];
+ if (!file) return;
+ const validTypes = ['image/jpeg', 'image/png', 'image/webp'];
+ if (!validTypes.includes(file.type)) { alert('Please upload a JPEG, PNG, or WebP image'); return; }
+ if (file.size > 20 * 1024 * 1024) { alert('File size must be under 20MB'); return; }
+
+ setIsUploading(true);
+ try {
+ const formData = new FormData();
+ formData.append('image', file);
+ const response = await fetch('/api/upload', { method: 'POST', body: formData });
+ if (!response.ok) throw new Error('Upload failed');
+ const data = await response.json();
+ onAddImage({ type: 'image', x: 75, y: 75, width: 150, height: 150, rotation: 0, src: data.preview.url, originalUrl: data.original.url });
+ } catch (error) {
+ console.error('Upload error:', error);
+ alert('Failed to upload image. Please try again.');
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ return (
+
+
Upload Image
+
fileInputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }} onDragLeave={(e) => { e.preventDefault(); setIsDragging(false); }} onDrop={(e) => { e.preventDefault(); setIsDragging(false); handleFiles(e.dataTransfer.files); }}
+ style={{ border: `2px dashed ${isDragging ? 'var(--accent)' : 'var(--border)'}`, borderRadius: 'var(--radius-md)', padding: '2rem 1rem', textAlign: 'center', cursor: 'pointer', background: isDragging ? 'var(--accent-bg)' : 'var(--bg-primary)', marginBottom: '1rem' }}>
+
📁
+
Click to upload or drag and drop
+
JPEG, PNG, WebP (max 20MB)
+
+
handleFiles(e.target.files)} style={{ display: 'none' }} />
+ {isUploading &&
Uploading...
}
+
+ Tip: After uploading, you can remove the background using the background removal tool in the properties panel.
+
+
+ );
+}
diff --git a/client/src/components/sidebar/index.js b/src/components/sidebar/index.js
similarity index 100%
rename from client/src/components/sidebar/index.js
rename to src/components/sidebar/index.js
diff --git a/client/src/constants/fonts.js b/src/constants/fonts.js
similarity index 100%
rename from client/src/constants/fonts.js
rename to src/constants/fonts.js
diff --git a/client/src/constants/stickers.js b/src/constants/stickers.js
similarity index 100%
rename from client/src/constants/stickers.js
rename to src/constants/stickers.js
diff --git a/client/src/constants/templates.js b/src/constants/templates.js
similarity index 100%
rename from client/src/constants/templates.js
rename to src/constants/templates.js
diff --git a/src/hooks/index.js b/src/hooks/index.js
new file mode 100644
index 0000000..b614fa3
--- /dev/null
+++ b/src/hooks/index.js
@@ -0,0 +1,4 @@
+export { useDesignEditor } from './useDesignEditor';
+export { useBackgroundRemoval } from './useBackgroundRemoval';
+export { useExport } from './useExport';
+export { useTemplate } from './useTemplate';
diff --git a/src/hooks/useBackgroundRemoval.js b/src/hooks/useBackgroundRemoval.js
new file mode 100644
index 0000000..77fa233
--- /dev/null
+++ b/src/hooks/useBackgroundRemoval.js
@@ -0,0 +1,90 @@
+import { useState, useCallback, useRef } from 'react';
+import { AutoModel, AutoProcessor, RawImage } from '@huggingface/transformers';
+
+export function useBackgroundRemoval() {
+ const [loading, setLoading] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const modelRef = useRef(null);
+ const processorRef = useRef(null);
+
+ const loadModel = useCallback(async () => {
+ if (modelRef.current && processorRef.current) return true;
+
+ setLoading(true);
+ setProgress(0);
+
+ try {
+ modelRef.current = await AutoModel.from_pretrained('briaai/RMBG-1.4', {
+ dtype: 'q8',
+ device: navigator.gpu ? 'webgpu' : 'wasm',
+ progress_callback: (p) => {
+ if (p.progress != null) setProgress(Math.round(p.progress * 50));
+ },
+ });
+
+ processorRef.current = await AutoProcessor.from_pretrained('briaai/RMBG-1.4');
+
+ setProgress(50);
+ setLoading(false);
+ return true;
+ } catch (error) {
+ console.error('Failed to load background removal model:', error);
+ setLoading(false);
+ return false;
+ }
+ }, []);
+
+ const removeBackground = useCallback(async (imageSrc) => {
+ if (!modelRef.current || !processorRef.current) {
+ const loaded = await loadModel();
+ if (!loaded) return null;
+ }
+
+ setLoading(true);
+ setProgress(50);
+
+ try {
+ const image = await RawImage.fromURL(imageSrc);
+ const { pixel_values } = await processorRef.current(image);
+
+ setProgress(70);
+
+ const { output } = await modelRef.current({ input: pixel_values });
+
+ setProgress(90);
+
+ const mask = await RawImage.fromTensor(
+ output[0].mul(255).to('uint8')
+ ).resize(image.width, image.height);
+
+ const canvas = document.createElement('canvas');
+ canvas.width = image.width;
+ canvas.height = image.height;
+ const ctx = canvas.getContext('2d');
+
+ ctx.drawImage(await image.toCanvas(), 0, 0);
+
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ for (let i = 0; i < mask.data.length; i++) {
+ imageData.data[i * 4 + 3] = mask.data[i];
+ }
+ ctx.putImageData(imageData, 0, 0);
+
+ setProgress(100);
+ setLoading(false);
+ return canvas.toDataURL('image/png');
+ } catch (error) {
+ console.error('Background removal failed:', error);
+ setLoading(false);
+ return null;
+ }
+ }, [loadModel]);
+
+ return {
+ loading,
+ progress,
+ hasModel: !!(modelRef.current),
+ loadModel,
+ removeBackground,
+ };
+}
diff --git a/src/hooks/useDesignEditor.js b/src/hooks/useDesignEditor.js
new file mode 100644
index 0000000..4b25990
--- /dev/null
+++ b/src/hooks/useDesignEditor.js
@@ -0,0 +1,72 @@
+import { useState, useCallback, useRef, useEffect } from 'react';
+
+const MAX_HISTORY = 50;
+const DEBOUNCE_DELAY_MS = 300;
+
+export function useDesignEditor() {
+ const [elements, setElements] = useState([]);
+ const [selectedId, setSelectedId] = useState(null);
+ const historyRef = useRef([]);
+ const historyIndexRef = useRef(-1);
+ const historyTimerRef = useRef(null);
+ const pendingChangesRef = useRef(null);
+
+ const saveToHistory = useCallback((newElements) => {
+ if (historyIndexRef.current < historyRef.current.length - 1) {
+ historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1);
+ }
+ historyRef.current.push(JSON.stringify(newElements));
+ if (historyRef.current.length > MAX_HISTORY) { historyRef.current.shift(); }
+ else { historyIndexRef.current++; }
+ }, []);
+
+ const flushPendingChanges = useCallback(() => {
+ if (pendingChangesRef.current) { saveToHistory(pendingChangesRef.current); pendingChangesRef.current = null; }
+ if (historyTimerRef.current) { clearTimeout(historyTimerRef.current); historyTimerRef.current = null; }
+ }, [saveToHistory]);
+
+ useEffect(() => { return () => { if (historyTimerRef.current) clearTimeout(historyTimerRef.current); }; }, []);
+
+ const canUndo = historyIndexRef.current > 0;
+ const canRedo = historyIndexRef.current < historyRef.current.length - 1;
+
+ const addElement = useCallback((element) => {
+ flushPendingChanges();
+ const newElement = { ...element, id: `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` };
+ setElements((prev) => { const newElements = [...prev, newElement]; saveToHistory(newElements); return newElements; });
+ setSelectedId(newElement.id);
+ return newElement.id;
+ }, [flushPendingChanges, saveToHistory]);
+
+ const updateElement = useCallback((id, attrs) => {
+ setElements((prev) => {
+ const newElements = prev.map((el) => (el.id === id ? { ...el, ...attrs } : el));
+ pendingChangesRef.current = newElements;
+ if (historyTimerRef.current) clearTimeout(historyTimerRef.current);
+ historyTimerRef.current = setTimeout(() => { flushPendingChanges(); }, DEBOUNCE_DELAY_MS);
+ return newElements;
+ });
+ }, [flushPendingChanges]);
+
+ const deleteElement = useCallback((id) => {
+ flushPendingChanges();
+ setElements((prev) => { const newElements = prev.filter((el) => el.id !== id); saveToHistory(newElements); return newElements; });
+ if (selectedId === id) setSelectedId(null);
+ }, [selectedId, flushPendingChanges, saveToHistory]);
+
+ const selectElement = useCallback((id) => setSelectedId(id), []);
+ const deselectAll = useCallback(() => setSelectedId(null), []);
+ const commitHistory = useCallback(() => flushPendingChanges(), [flushPendingChanges]);
+
+ const undo = useCallback(() => {
+ if (historyIndexRef.current > 0) { historyIndexRef.current--; setElements(JSON.parse(historyRef.current[historyIndexRef.current])); setSelectedId(null); }
+ }, []);
+
+ const redo = useCallback(() => {
+ if (historyIndexRef.current < historyRef.current.length - 1) { historyIndexRef.current++; setElements(JSON.parse(historyRef.current[historyIndexRef.current])); setSelectedId(null); }
+ }, []);
+
+ const initializeHistory = useCallback(() => { historyRef.current = [JSON.stringify([])]; historyIndexRef.current = 0; }, []);
+
+ return { elements, selectedId, addElement, updateElement, deleteElement, selectElement, deselectAll, commitHistory, undo, redo, canUndo, canRedo, initializeHistory };
+}
diff --git a/client/src/hooks/useExport.js b/src/hooks/useExport.js
similarity index 59%
rename from client/src/hooks/useExport.js
rename to src/hooks/useExport.js
index 6b5f459..bc85073 100644
--- a/client/src/hooks/useExport.js
+++ b/src/hooks/useExport.js
@@ -7,37 +7,25 @@ export function useExport() {
const [error, setError] = useState(null);
const exportDesign = useCallback(async (elements, designName = 'design', template = null) => {
- setExporting(true);
- setProgress(0);
- setError(null);
- setExportUrl(null);
+ setExporting(true); setProgress(0); setError(null); setExportUrl(null);
try {
- // Simulate progress during export
- const progressInterval = setInterval(() => {
- setProgress((prev) => Math.min(prev + 10, 90));
- }, 200);
+ const progressInterval = setInterval(() => { setProgress((prev) => Math.min(prev + 10, 90)); }, 200);
const response = await fetch('/api/export', {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
+ headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ elements, designName, template }),
});
clearInterval(progressInterval);
setProgress(100);
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error || 'Export failed');
- }
+ if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Export failed'); }
const data = await response.json();
setExportUrl(data.export.url);
- // Trigger download
const link = document.createElement('a');
link.href = data.export.url;
link.download = data.export.filename;
@@ -53,17 +41,7 @@ export function useExport() {
}
}, []);
- const clearExport = useCallback(() => {
- setExportUrl(null);
- setError(null);
- }, []);
+ const clearExport = useCallback(() => { setExportUrl(null); setError(null); }, []);
- return {
- exporting,
- progress,
- exportUrl,
- error,
- exportDesign,
- clearExport,
- };
+ return { exporting, progress, exportUrl, error, exportDesign, clearExport };
}
diff --git a/src/hooks/useTemplate.js b/src/hooks/useTemplate.js
new file mode 100644
index 0000000..b4efce2
--- /dev/null
+++ b/src/hooks/useTemplate.js
@@ -0,0 +1,57 @@
+import { useState, useCallback, useRef } from 'react';
+
+export function calculateAutoCrop(imageSize, slotSize) {
+ const imageRatio = imageSize.width / imageSize.height;
+ const slotRatio = slotSize.width / slotSize.height;
+ let sx, sy, sWidth, sHeight;
+ if (imageRatio > slotRatio) { sHeight = imageSize.height; sWidth = imageSize.height * slotRatio; sx = (imageSize.width - sWidth) / 2; sy = 0; }
+ else { sWidth = imageSize.width; sHeight = imageSize.width / slotRatio; sx = 0; sy = (imageSize.height - sHeight) / 2; }
+ return { sx, sy, sWidth, sHeight };
+}
+
+export function createDragBoundFunc(slot, elementSize) {
+ const { bounds } = slot;
+ const minX = bounds.x, minY = bounds.y;
+ const maxX = bounds.x + bounds.width - elementSize.width;
+ const maxY = bounds.y + bounds.height - elementSize.height;
+ return (_oldBox, newBox) => ({ x: Math.max(minX, Math.min(newBox.x, maxX)), y: Math.max(minY, Math.min(newBox.y, maxY)), width: newBox.width, height: newBox.height });
+}
+
+export function useTemplate(templates = []) {
+ const [currentTemplateId, setCurrentTemplateId] = useState(null);
+ const [assignedSlots, setAssignedSlots] = useState({});
+ const templateRef = useRef(null);
+
+ const currentTemplate = templates.find(t => t.id === currentTemplateId) || null;
+ const getSlots = useCallback(() => currentTemplate?.slots || [], [currentTemplate]);
+
+ const loadTemplate = useCallback((templateId) => {
+ const template = templates.find(t => t.id === templateId);
+ if (template) { setCurrentTemplateId(templateId); setAssignedSlots({}); templateRef.current = template; return true; }
+ return false;
+ }, [templates]);
+
+ const clearTemplate = useCallback(() => { setCurrentTemplateId(null); setAssignedSlots({}); templateRef.current = null; }, []);
+
+ const assignImageToSlot = useCallback((slotId, imageData) => {
+ const slots = getSlots();
+ const slot = slots.find(s => s.id === slotId);
+ if (!slot) return null;
+ const elementData = { type: 'image', src: imageData, x: slot.bounds.x, y: slot.bounds.y, width: slot.bounds.width, height: slot.bounds.height, slotId, crop: null };
+ const img = new Image();
+ img.src = imageData;
+ img.onload = () => { elementData.crop = calculateAutoCrop({ width: img.width, height: img.height }, { width: slot.bounds.width, height: slot.bounds.height }); };
+ setAssignedSlots(prev => ({ ...prev, [slotId]: elementData }));
+ return elementData;
+ }, [getSlots]);
+
+ const getDragBoundFunc = useCallback((slotId, elementSize) => {
+ const slot = getSlots().find(s => s.id === slotId);
+ if (!slot) return null;
+ return createDragBoundFunc(slot, elementSize);
+ }, [getSlots]);
+
+ const isSlotFilled = useCallback((slotId) => !!assignedSlots[slotId], [assignedSlots]);
+
+ return { currentTemplateId, currentTemplate, assignedSlots, loadTemplate, clearTemplate, getSlots, assignImageToSlot, getDragBoundFunc, isSlotFilled };
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..53ad5db
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,144 @@
+:root {
+ --accent: #38bdf8;
+ --accent-hover: #0ea5e9;
+ --accent-bg: rgba(56, 189, 248, 0.1);
+ --bg-primary: #ffffff;
+ --bg-secondary: #f8fafc;
+ --bg-tertiary: #f1f5f9;
+ --border: #e2e8f0;
+ --border-focus: #38bdf8;
+ --text-primary: #0f172a;
+ --text-secondary: #475569;
+ --text-muted: #94a3b8;
+ --success: #22c55e;
+ --warning: #f59e0b;
+ --error: #ef4444;
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-xl: 16px;
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ --font-body: 'DM Sans', system-ui, -apple-system, sans-serif;
+ --font-mono: 'Space Mono', ui-monospace, Consolas, monospace;
+ font-family: var(--font-body);
+ line-height: 1.5;
+ font-weight: 400;
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* { box-sizing: border-box; }
+body { margin: 0; min-height: 100vh; }
+#root { min-height: 100vh; display: flex; flex-direction: column; }
+button { font-family: inherit; cursor: pointer; outline: none; }
+button:focus-visible { box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--accent); }
+input, textarea, select { font-family: inherit; }
+
+.editor-layout { display: flex; flex: 1; overflow: hidden; }
+
+.sidebar {
+ width: 320px; background: var(--bg-secondary); border-right: 1px solid var(--border);
+ display: flex; flex-direction: column; overflow: hidden;
+}
+
+.canvas-area {
+ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
+ background: var(--bg-tertiary); overflow: auto; padding: 2rem;
+}
+
+.properties-panel {
+ width: 280px; background: var(--bg-secondary); border-left: 1px solid var(--border);
+ display: flex; flex-direction: column; overflow: hidden;
+}
+
+.icon-btn {
+ padding: 0.5rem 0.75rem; background: var(--bg-primary); border: 1px solid var(--border);
+ border-radius: var(--radius-sm); font-size: 0.75rem; cursor: pointer; transition: all 0.2s; color: var(--text-secondary);
+}
+.icon-btn:hover:not(:disabled) { background: var(--accent); border-color: var(--accent); color: white; }
+.icon-btn:disabled { opacity: 0.4; cursor: not-allowed; }
+
+.export-btn {
+ padding: 0.5rem 1rem; background: linear-gradient(135deg, #22c55e, #16a34a); color: white;
+ border: none; border-radius: var(--radius-md); font-size: 0.875rem; font-weight: 500;
+ cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 0.5rem;
+}
+.export-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: var(--shadow-md); }
+.export-btn:disabled { opacity: 0.6; cursor: not-allowed; }
+
+.export-error {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 0.75rem 1rem; background: #fef2f2; border: 1px solid #fecaca;
+ border-radius: var(--radius-md); color: #dc2626; font-size: 0.875rem;
+ margin-bottom: 1rem; width: 100%; max-width: 400px;
+}
+.export-error p { margin: 0; }
+.close-error { background: transparent; border: none; color: #dc2626; cursor: pointer; font-size: 1.25rem; padding: 0; line-height: 1; }
+
+.bg-removal-container { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
+.bg-removal-btn {
+ width: 100%; padding: 0.75rem; background: linear-gradient(135deg, #8b5cf6, #ec4899); color: white;
+ border: none; border-radius: var(--radius-md); font-weight: 500; cursor: pointer; transition: all 0.2s;
+ display: flex; align-items: center; justify-content: center; gap: 0.5rem;
+}
+.bg-removal-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: var(--shadow-md); }
+.bg-removal-btn:disabled { opacity: 0.7; cursor: not-allowed; }
+.bg-removal-hint { font-size: 0.7rem; color: var(--text-muted); margin: 0.5rem 0 0 0; line-height: 1.4; }
+
+.spinner-small {
+ width: 16px; height: 16px; border: 2px solid rgba(255, 255, 255, 0.3);
+ border-top-color: white; border-radius: 50%; animation: spin 1s linear infinite;
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+
+.filerobot-overlay {
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0, 0, 0, 0.8); display: flex; align-items: center;
+ justify-content: center; z-index: 1000;
+}
+.filerobot-container { width: 90%; height: 90%; background: #1e1e1e; border-radius: var(--radius-lg); overflow: hidden; }
+
+.pwa-install-banner {
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
+ background: var(--bg-primary); border: 1px solid var(--accent); border-radius: var(--radius-lg);
+ padding: 1rem 1.5rem; box-shadow: var(--shadow-lg); z-index: 1000;
+ display: flex; align-items: center; gap: 1rem; animation: slideUp 0.3s ease-out;
+}
+@keyframes slideUp { from { opacity: 0; transform: translateX(-50%) translateY(20px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
+.pwa-install-banner p { margin: 0; font-size: 0.875rem; color: var(--text-primary); }
+.install-btn { padding: 0.5rem 1rem; background: var(--accent); color: white; border: none; border-radius: var(--radius-sm); font-size: 0.75rem; font-weight: 500; cursor: pointer; }
+.dismiss-btn { padding: 0.5rem 1rem; background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 0.75rem; cursor: pointer; }
+
+.offline-indicator {
+ position: fixed; top: 0; left: 0; right: 0;
+ background: linear-gradient(135deg, #f59e0b, #d97706); color: white;
+ padding: 0.5rem 1rem; text-align: center; font-size: 12px; font-weight: 500;
+ z-index: 9999; display: flex; align-items: center; justify-content: center; gap: 0.5rem;
+ box-shadow: var(--shadow-md);
+}
+
+@media (max-width: 1200px) and (min-width: 768px) {
+ .sidebar { width: 280px; }
+ .properties-panel { width: 240px; }
+ .canvas-area { padding: 1.5rem; }
+}
+
+@media (max-width: 767px) {
+ .editor-layout { flex-direction: column; }
+ .sidebar { width: 100%; height: auto; max-height: 50vh; border-right: none; border-bottom: 1px solid var(--border); }
+ .canvas-area { padding: 1rem; width: 100%; }
+ .properties-panel { width: 100%; border-left: none; border-top: 1px solid var(--border); max-height: 40vh; }
+ .pwa-install-banner { left: 10px; right: 10px; transform: none; width: auto; flex-direction: column; text-align: center; }
+}
+
+@media (max-width: 480px) {
+ .sidebar { max-height: 40vh; }
+ .canvas-area { padding: 0.5rem; }
+ button { min-height: 44px; }
+}
diff --git a/client/src/main.jsx b/src/main.jsx
similarity index 83%
rename from client/src/main.jsx
rename to src/main.jsx
index ed456e3..57f1989 100644
--- a/client/src/main.jsx
+++ b/src/main.jsx
@@ -3,17 +3,13 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App'
-// Service worker update handling
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.ready.then((registration) => {
- // Listen for updates from the service worker
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
-
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
- // Dispatch custom event for PWAInstall component
window.dispatchEvent(new CustomEvent('swUpdated', { detail: newWorker }));
}
});
diff --git a/server/uploads/.gitkeep b/uploads/.gitkeep
similarity index 100%
rename from server/uploads/.gitkeep
rename to uploads/.gitkeep
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..0ba02be
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,95 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { VitePWA } from 'vite-plugin-pwa';
+
+export default defineConfig({
+ plugins: [
+ react(),
+ VitePWA({
+ registerType: 'prompt',
+ includeAssets: ['favicon.svg', 'pwa-192x192.svg', 'pwa-512x512.svg'],
+ manifest: {
+ name: 'Apparel Designer',
+ short_name: 'ApparelDesigner',
+ description: 'T-shirt customization editor',
+ theme_color: '#38bdf8',
+ background_color: '#ffffff',
+ display: 'standalone',
+ orientation: 'any',
+ scope: '/',
+ start_url: '/',
+ icons: [
+ { src: 'pwa-192x192.svg', sizes: '192x192', type: 'image/svg+xml' },
+ { src: 'pwa-512x512.svg', sizes: '512x512', type: 'image/svg+xml' },
+ { src: 'pwa-512x512.svg', sizes: '512x512', type: 'image/svg+xml', purpose: 'any maskable' },
+ ],
+ },
+ workbox: {
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
+ runtimeCaching: [
+ {
+ urlPattern: /^https:\/\/cdn\.huggingface\.co\/.*/i,
+ handler: 'CacheFirst',
+ options: {
+ cacheName: 'transformers-models',
+ expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 30 },
+ cacheableResponse: { statuses: [0, 200] },
+ },
+ },
+ {
+ urlPattern: /^https:\/\/cdn-lfs\.huggingface\.co\/.*/i,
+ handler: 'CacheFirst',
+ options: {
+ cacheName: 'transformers-lfs',
+ expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 30 },
+ cacheableResponse: { statuses: [0, 200] },
+ },
+ },
+ {
+ urlPattern: /^\/uploads\/.*/i,
+ handler: 'CacheFirst',
+ options: {
+ cacheName: 'uploaded-images',
+ expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 },
+ },
+ },
+ {
+ urlPattern: /^\/api\/.*/i,
+ handler: 'NetworkFirst',
+ options: {
+ cacheName: 'api-responses',
+ expiration: { maxEntries: 50, maxAgeSeconds: 300 },
+ cacheableResponse: { statuses: [0, 200] },
+ networkTimeoutSeconds: 3,
+ },
+ },
+ {
+ urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
+ handler: 'StaleWhileRevalidate',
+ options: {
+ cacheName: 'google-fonts',
+ expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 },
+ },
+ },
+ {
+ urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
+ handler: 'CacheFirst',
+ options: {
+ cacheName: 'gstatic-fonts',
+ expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 },
+ },
+ },
+ ],
+ },
+ }),
+ ],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': { target: 'http://localhost:3001', changeOrigin: true },
+ '/uploads': { target: 'http://localhost:3001', changeOrigin: true },
+ '/exports': { target: 'http://localhost:3001', changeOrigin: true },
+ },
+ },
+ build: { outDir: 'dist' },
+});