diff --git a/.gitignore b/.gitignore index 47777d9..5bdf59a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Stryker Mutation testing .stryker-tmp/ +mutation-testing/reports/mutation-report.json +mutation-testing/reports/html/ # Logs logs @@ -25,6 +27,8 @@ lib-cov # Coverage directory used by tools like istanbul coverage *.lcov +playwright-report/ +test-results/ # nyc test coverage .nyc_output diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..a0627b1 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "semi": true +} diff --git a/AGENTS.md b/AGENTS.md index 8fe7c9d..47723e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,106 +1,49 @@ -# AGENTS.md — TemplateJs -Canonical instructions for coding agents. Human-facing docs are in `README.md`. +# Rules -## Quick facts -- Minimal single-page web app (static HTML + modular JS) +## General +- Minimal local first web app (static HTML + modular JS) - Entrypoint: `index.html` (+ static pages in `pages/`) -- Source in `src/` with colocated tests -- Deploy via GitHub Pages from `main` (static assets) -- Preferred dev env: Codespaces (optional) - -## Runbook -- Before commit: `npm run check:all` then `npm test` -- Before PR/release: `npm run validate:all` (tests + checks + mutation) -- Always run commands in a real terminal and include actual output in notes/PRs. +- Source composed of small, focused modules in `src/` (`components/`, `utils/`, ..) with colocated tests +- Frequently during development and before each commit: run `npm test` +- `README.md` typically contains big picture dev. spec and context. It should be kept up to date whenever the code is ready for a PR +- Static app => serve `index.html` with simple static server (e.g., VS Code Live Server) +- Only change code directly related to the current task; keep diffs small +- Preserve existing comments & docs; add concise, long-lived comments where useful and avoid narrating changes via comments +- When external documentation is needed and you lack a browsing/online search tool, ask the user to run an online search for you (e.g., "Please search for \"x\" and paste back the findings") -## Build/Serve -- Static app; serve `index.html` with a simple static server (e.g., VS Code Live Server) +## Dependencies & no-build approach to use +This project follows a no-build, static workflow: +- Use native ES modules and ` + + + + + + +
+
+

MoodCanvas

+ IEC Demo +
+ + +
+
+
+ + + + + + + + + +
+ +
+

1) Main Target Photo

+ +
+ +
+ + +
+ +
+

2) Reference images (optional, up to 2; first = main style)

+
+
+
Reference #1 (main style)
+ +
+
+
+
Reference #2 (accent)
+ +
+
+
+
+ +
+ + Upload the main photo to enable. +
+
+ + +
+
+ + + + + + + + + + + +``` diff --git a/README.md b/README.md index b8cb22b..87355b2 100644 --- a/README.md +++ b/README.md @@ -10,37 +10,37 @@ A user uploads **one photo** (JPG/PNG, camera capture supported) of an **empty o **Goals (MVP)** -* Single-photo based advisory for **empty rooms**; secondary cases (near-empty) should still work. -* “Chat-style, linear” UX where each step appends a card; **strict backtrack**: changing an earlier answer removes subsequent cards. -* **Inspiration Pulse** inside the analysis: top usage candidates + style fit. -* **10-style gallery** (1 image per style) with **pool=5** parallel generation; **order by fit score** desc, no “recommended” badge. -* Post-selection: **A/B mini-variants** for up to 3 styles (**Smart Mixed** axes from analysis) after a confirmation step. -* Final: **3 hero renders (Smart Mixed)** after a confirmation step. -* **Mini shopping list** = **3 impact items** from analysis + **2 function staples**, **no prices**. -* **Strict JSON** analysis via Gemini (response schema enforced). -* **Dark, relaxing UI** (Plum–Peach palette) with **Tailwind Play CDN**; **system sans** fonts. +- Single-photo based advisory for **empty rooms**; secondary cases (near-empty) should still work. +- “Chat-style, linear” UX where each step appends a card; **strict backtrack**: changing an earlier answer removes subsequent cards. +- **Inspiration Pulse** inside the analysis: top usage candidates + style fit. +- **10-style gallery** (1 image per style) with **pool=5** parallel generation; **order by fit score** desc, no “recommended” badge. +- Post-selection: **A/B mini-variants** for up to 3 styles (**Smart Mixed** axes from analysis) after a confirmation step. +- Final: **3 hero renders (Smart Mixed)** after a confirmation step. +- **Mini shopping list** = **3 impact items** from analysis + **2 function staples**, **no prices**. +- **Strict JSON** analysis via Gemini (response schema enforced). +- **Dark, relaxing UI** (Plum–Peach palette) with **Tailwind Play CDN**; **system sans** fonts. **Non-goals (MVP)** -* No user accounts; no server storage; no exports (ZIP/PDF/HTML) yet. -* No multi-language UI (English only); internal units in **meters** only. -* No accessibility hard targets (postponed). -* No PWA/offline installation; **GitHub Pages** static hosting only. -* No seed control/deduplication for renders; no price or shop links in the mini list. +- No user accounts; no server storage; no exports (ZIP/PDF/HTML) yet. +- No multi-language UI (English only); internal units in **meters** only. +- No accessibility hard targets (postponed). +- No PWA/offline installation; **GitHub Pages** static hosting only. +- No seed control/deduplication for renders; no price or shop links in the mini list. # Primary use cases (supported room functions) -* Bedroom, Home-Office, Kids Room, Guest Room, Living Room, Dining Room, Hobby/Studio, Fitness/Yoga, Library/Reading, Music/Recording, Walk-in Closet, Storage/Utility, plus free-text “Other”. +- Bedroom, Home-Office, Kids Room, Guest Room, Living Room, Dining Room, Hobby/Studio, Fitness/Yoga, Library/Reading, Music/Recording, Walk-in Closet, Storage/Utility, plus free-text “Other”. # Architecture choices (and rationale) -* **Client-only web app**: privacy-forward, easy hosting (GitHub Pages), zero backend complexity. -* **BYOK (Gemini)**: user supplies API key; no server secrets. Key stored **only in localStorage**. -* **Models**: `gemini-2.5-flash` for **analysis**; `gemini-2.5-flash-image` for **renders**. Balanced cost/latency. -* **Strict JSON** mode for analysis: `responseMimeType:"application/json"` + `responseSchema` → stable, machine-consumable output. -* **Storage**: **IndexedDB** via tiny `idb` lib; **Hybrid model** (projects, events, media, artifacts). Local only. -* **Styling**: Tailwind compiled locally (CLI) into static CSS; **Plum–Peach** dark palette; system sans. -* **Hosting**: **GitHub Pages**; **CSP meta** (loose MVP) embedded in `index.html`. +- **Client-only web app**: privacy-forward, easy hosting (GitHub Pages), zero backend complexity. +- **BYOK (Gemini)**: user supplies API key; no server secrets. Key stored **only in localStorage**. +- **Models**: `gemini-2.5-flash` for **analysis**; `gemini-2.5-flash-image` for **renders**. Balanced cost/latency. +- **Strict JSON** mode for analysis: `responseMimeType:"application/json"` + `responseSchema` → stable, machine-consumable output. +- **Storage**: **IndexedDB** via tiny `idb` lib; **Hybrid model** (projects, events, media, artifacts). Local only. +- **Styling**: Tailwind compiled locally (CLI) into static CSS; **Plum–Peach** dark palette; system sans. +- **Hosting**: **GitHub Pages**; **CSP meta** (loose MVP) embedded in `index.html`. ## Build step: Tailwind CSS @@ -48,20 +48,18 @@ Run `npm run build:css` after editing HTML/JS classes to regenerate `styles/app. # Security & privacy -* **Key handling**: +- **Key handling**: + - BYOK banner (“Get your key… Google AI Studio. The key is stored only in your browser”). + - Store in **localStorage**; **Settings modal** offers “Remove key from this device”. + - No validation calls; optional superficial prefix hint (keys often start `AIza…`). + - Send key in header `x-goog-api-key` (not query). - * BYOK banner (“Get your key… Google AI Studio. The key is stored only in your browser”). - * Store in **localStorage**; **Settings modal** offers “Remove key from this device”. - * No validation calls; optional superficial prefix hint (keys often start `AIza…`). - * Send key in header `x-goog-api-key` (not query). +- **Images & metadata**: + - Allow **JPG/PNG**. Accept EXIF orientation; **do not strip metadata** for MVP; send “as-is”. + - Resize **before upload**: long side **≤ 2048 px** (q≈0.9). + - No additional consent gate (info lives in Help/README). -* **Images & metadata**: - - * Allow **JPG/PNG**. Accept EXIF orientation; **do not strip metadata** for MVP; send “as-is”. - * Resize **before upload**: long side **≤ 2048 px** (q≈0.9). - * No additional consent gate (info lives in Help/README). - -* **CSP (loose MVP)**: +- **CSP (loose MVP)**: ``` default-src 'self'; @@ -76,170 +74,158 @@ Run `npm run build:css` after editing HTML/JS classes to regenerate `styles/app. Stores (via `idb`): -* **projects** `{ id, name, createdAt, updatedAt, settings { units:"m", theme:"plum-peach" }, caps { perProjectMB:150 } }` -* **events** (append-only timeline) `{ id, projectId, type, payload, createdAt }` +- **projects** `{ id, name, createdAt, updatedAt, settings { units:"m", theme:"plum-peach" }, caps { perProjectMB:150 } }` +- **events** (append-only timeline) `{ id, projectId, type, payload, createdAt }` Types: `upload_image`, `analysis_done`, `gallery_generated`, `ab_generated`, `hero_generated`, `selection_changed`, `warning`, `error`, etc. -* **media** (blobs & thumbs) `{ id, projectId, kind:"input|render|thumb", bytes|blob, mime, width, height, relatedId, createdAt }` +- **media** (blobs & thumbs) `{ id, projectId, kind:"input|render|thumb", bytes|blob, mime, width, height, relatedId, createdAt }` Import: store **original** + **512px thumb**. Renders: store full + thumb. -* **artifacts** (JSON/text) `{ id, projectId, kind:"analysis|prompts|palette|quickwins|shoppinglist", json, createdAt }` +- **artifacts** (JSON/text) `{ id, projectId, kind:"analysis|prompts|palette|quickwins|shoppinglist", json, createdAt }` **Storage caps & cleanup** -* Output JPEG q≈0.85; thumbs 384px. -* **Per-project cap 150 MB**, **global cap 600 MB**. -* **LRU auto-delete** oldest **full-res renders** when over cap; thumbs remain; 30s undo toast. +- Output JPEG q≈0.85; thumbs 384px. +- **Per-project cap 150 MB**, **global cap 600 MB**. +- **LRU auto-delete** oldest **full-res renders** when over cap; thumbs remain; 30s undo toast. # UX flow (chat-like, linear; strict backtrack) Cards appear top→down; editing an earlier card **removes all later cards**. 1. **UploadCard** - - * `` - * Shows selected image preview. - * Minimal import normalization: **fix EXIF orientation**, store original + 512 thumb. + - `` + - Shows selected image preview. + - Minimal import normalization: **fix EXIF orientation**, store original + 512 thumb. 2. **KeyBanner** (only if no key is present) - - * Text: “Get your key… Google AI Studio. The key is stored only in your browser.” (link). - * Paste field to save key. No validation. + - Text: “Get your key… Google AI Studio. The key is stored only in your browser.” (link). + - Paste field to save key. No validation. 3. **Inspiration Pulse + Function/Scope Quick Pick** - - * One **Single-Analysis** call (strict JSON) returns `usage_candidates`. - * Show top 3 as an “Inspiration” bubble. - * User explicitly selects **intended use** and **scope (1–4)** (short picker). - * **Low scale confidence** → small non-blocking warning banner. + - One **Single-Analysis** call (strict JSON) returns `usage_candidates`. + - Show top 3 as an “Inspiration” bubble. + - User explicitly selects **intended use** and **scope (1–4)** (short picker). + - **Low scale confidence** → small non-blocking warning banner. 4. **AnalysisCard** - - * Renders JSON summary: `photo_findings`, `palette_60_30_10`, `quick_wins` (top 5), `styles_top10` order and scores. + - Renders JSON summary: `photo_findings`, `palette_60_30_10`, `quick_wins` (top 5), `styles_top10` order and scores. 5. **StyleGalleryCard** - - * Generates **10 full renders** (one per style) **in parallel** with **pool=5**; default size **1536×1024**. - * User can adjust **count (6/8/10)** and **size (1024/1536)** before running. - * Tiles **ordered by fit_score desc**; **corner chip** shows style name. - * Per-tile **soft fail** → small error chip + **Retry**. + - Generates **10 full renders** (one per style) **in parallel** with **pool=5**; default size **1536×1024**. + - User can adjust **count (6/8/10)** and **size (1024/1536)** before running. + - Tiles **ordered by fit_score desc**; **corner chip** shows style name. + - Per-tile **soft fail** → small error chip + **Retry**. 6. **SelectionCard** - - * Select **up to 3 favorites**. + - Select **up to 3 favorites**. 7. **A/B Mini-Variants** - - * Confirmation card shows “N favorites × 2 images”. - * Generate per favorite 2 images using **smart_mixed_axes** from analysis; pool=5. + - Confirmation card shows “N favorites × 2 images”. + - Generate per favorite 2 images using **smart_mixed_axes** from analysis; pool=5. 8. **HeroRenderCard** (final) - - * Confirmation card (“3 images @ 1536×1024”). - * Generate **3 hero renders (Smart Mixed)**. - * Derive **60-30-10 palette**, **5 Quick Wins**, and **mini shopping list** here. + - Confirmation card (“3 images @ 1536×1024”). + - Generate **3 hero renders (Smart Mixed)**. + - Derive **60-30-10 palette**, **5 Quick Wins**, and **mini shopping list** here. 9. **QuickWins & Mini List** - - * Show **5 actionable items** (concise rules, e.g., distances in meters). - * **Mini list (5 items)** = 3 highest-impact + 2 function staples; **no prices**. + - Show **5 actionable items** (concise rules, e.g., distances in meters). + - **Mini list (5 items)** = 3 highest-impact + 2 function staples; **no prices**. 10. **Settings modal** -* “Remove key from this device”. +- “Remove key from this device”. **Image viewer**: tapping any image opens it in a **new tab** (full size). # Rendering models & call strategy -* **Analysis**: `POST v1beta/models/gemini-2.5-flash:generateContent` +- **Analysis**: `POST v1beta/models/gemini-2.5-flash:generateContent` + - `contents`: user text prompt (spec below) + **inlineData** image (base64, ≤2048 px long side). + - `generationConfig`: + - `responseMimeType: "application/json"` + - `responseSchema: ANALYSIS_SCHEMA` (see “Strict JSON schema”). - * `contents`: user text prompt (spec below) + **inlineData** image (base64, ≤2048 px long side). - * `generationConfig`: - - * `responseMimeType: "application/json"` - * `responseSchema: ANALYSIS_SCHEMA` (see “Strict JSON schema”). -* **Renders (gallery, A/B, hero)**: `gemini-2.5-flash-image` (image-to-image) - - * Inputs: original image + per-style/variant **prompt** from `render_gallery[]` (or templates below). - * Concurrency: **5 at a time**. - * Errors: per-tile retry button; analysis errors show a single toast with **Retry**. +- **Renders (gallery, A/B, hero)**: `gemini-2.5-flash-image` (image-to-image) + - Inputs: original image + per-style/variant **prompt** from `render_gallery[]` (or templates below). + - Concurrency: **5 at a time**. + - Errors: per-tile retry button; analysis errors show a single toast with **Retry**. **Timeouts & retries (Safe Defaults)** -* Analysis timeout **45s**; Render timeout **120s**. -* Retry up to **2×** on **429 / 5xx / network** with 500ms/1500ms backoff. -* **AbortController** cancels in-flight calls if a prior step changes. +- Analysis timeout **45s**; Render timeout **120s**. +- Retry up to **2×** on **429 / 5xx / network** with 500ms/1500ms backoff. +- **AbortController** cancels in-flight calls if a prior step changes. # Prompts (finalized for MVP) **Single-Analysis (user content)** Use the version shared earlier (“ROLE: Interior design analyst for empty rooms…”) including context, inputs, and output rules. -* Language: **English**; units **meters**. -* Geometry: treat camera pose + envelope as fixed (unless scope=4). -* Return **only JSON** (no prose) because strict mode is used. +- Language: **English**; units **meters**. +- Geometry: treat camera pose + envelope as fixed (unless scope=4). +- Return **only JSON** (no prose) because strict mode is used. **Render templates** -* 10 styles: **Scandi, Japandi, Modern Minimal, Contemporary Cozy, Mid-Century, Industrial Soft, Boho, Rustic, Mediterranean, Art-Deco**. -* Base constraints for image-to-image: keep camera pose and room envelope; respect **intervention_scope**; reflect **palette_60_30_10** subtly; avoid logos/text; photorealistic lighting. -* A/B Mini: vary along `smart_mixed_axes.axisA` vs `axisB` as specified. -* Hero (3): Smart Mixed (A, B, and best-of-both). +- 10 styles: **Scandi, Japandi, Modern Minimal, Contemporary Cozy, Mid-Century, Industrial Soft, Boho, Rustic, Mediterranean, Art-Deco**. +- Base constraints for image-to-image: keep camera pose and room envelope; keep doors, windows, and entries exactly where they appear in the source photo; respect **intervention_scope**; reflect **palette_60_30_10** subtly; avoid logos/text; photorealistic lighting. +- A/B Mini: vary along `smart_mixed_axes.axisA` vs `axisB` as specified. +- Hero (3): Smart Mixed (A, B, and best-of-both). # Strict JSON schema (analysis) The approved **ANALYSIS_SCHEMA** (JSON Schema 2020-12) is part of the app and sent to Gemini in `responseSchema`. -* Required sections: `usage_candidates`, `photo_findings`, `palette_60_30_10`, `constraints`, `quick_wins`, `styles_top10`, `smart_mixed_axes`, `negative_prompts`, `safety_checks`, `render_gallery`. -* Enumerations and exact counts are now handled at the prompt/UX level (schema only asserts basic shapes) to avoid overwhelming Gemini with state explosion. -* Numeric ranges and string patterns (confidence 0-1, hex codes, etc.) are enforced via prompt instructions and client-side validation instead of schema constraints. -* `scale_guesses` allows `null` for width/depth/height with explicit `confidence` via `nullable: true` on numeric fields (no range bounds in-schema). +- Required sections: `usage_candidates`, `photo_findings`, `palette_60_30_10`, `constraints`, `quick_wins`, `styles_top10`, `smart_mixed_axes`, `negative_prompts`, `safety_checks`, `render_gallery`. +- Enumerations and exact counts are now handled at the prompt/UX level (schema only asserts basic shapes) to avoid overwhelming Gemini with state explosion. +- Numeric ranges and string patterns (confidence 0-1, hex codes, etc.) are enforced via prompt instructions and client-side validation instead of schema constraints. +- `scale_guesses` allows `null` for width/depth/height with explicit `confidence` via `nullable: true` on numeric fields (no range bounds in-schema). # UI & styling -* **Theme**: Dark **Plum–Peach** +- **Theme**: Dark **Plum–Peach** + - `bg #392338`, `surface #3F2840`, `surface2 #462E49`, `text #EDEDED`, `textMuted #CFC7D2`, `accent #FFCFA4`, `accent2 #FF947F`, `cta #C1264E`. - * `bg #392338`, `surface #3F2840`, `surface2 #462E49`, `text #EDEDED`, `textMuted #CFC7D2`, `accent #FFCFA4`, `accent2 #FF947F`, `cta #C1264E`. -* **Tailwind**: Play CDN; inline `tailwind.config` extends the above tokens; border radius `xl2`. -* **Components**: Header, HomeGrid, KeyBanner, ChatTimeline (cards listed above), Modals (Settings, Error). +- **Tailwind**: Play CDN; inline `tailwind.config` extends the above tokens; border radius `xl2`. +- **Components**: Header, HomeGrid, KeyBanner, ChatTimeline (cards listed above), Modals (Settings, Error). # Error handling & edge cases -* **No key**: show BYOK banner with AI Studio link; block calls; everything else visible. -* **Low scale confidence**: show non-blocking warning; proceed. -* **Analysis invalid**: strict JSON mode prevents schema drift; if API error, show retry toast. -* **Gallery tile fails**: show per-tile error chip + Retry; other tiles continue. -* **Rate limit spikes**: rely on per-tile behavior + two backoff retries; no global pause in MVP. -* **Network loss**: calls fail; show retry. -* **Storage cap exceeded**: auto-purge oldest full-res renders (thumbs remain) + 30s undo. -* **Backtrack**: editing any earlier card **removes** later cards and cancels in-flight requests. +- **No key**: show BYOK banner with AI Studio link; block calls; everything else visible. +- **Low scale confidence**: show non-blocking warning; proceed. +- **Analysis invalid**: strict JSON mode prevents schema drift; if API error, show retry toast. +- **Gallery tile fails**: show per-tile error chip + Retry; other tiles continue. +- **Rate limit spikes**: rely on per-tile behavior + two backoff retries; no global pause in MVP. +- **Network loss**: calls fail; show retry. +- **Storage cap exceeded**: auto-purge oldest full-res renders (thumbs remain) + 30s undo. +- **Backtrack**: editing any earlier card **removes** later cards and cancels in-flight requests. # Browser support -* **Mobile-priority**: Chrome (Android) and Safari (iOS) prioritized; desktop browsers “best effort” (latest Chrome/Edge/Firefox/Safari). -* Camera capture relies on `` (browser support varies; gracefully falls back to picker). +- **Mobile-priority**: Chrome (Android) and Safari (iOS) prioritized; desktop browsers “best effort” (latest Chrome/Edge/Firefox/Safari). +- Camera capture relies on `` (browser support varies; gracefully falls back to picker). # Testing plan (high level, MVP) -* **Analysis Strict-JSON Harness** (in-browser): - - * Given a room photo and a valid hard-coded key, when calling analysis, then the response **parses** and **validates** against `ANALYSIS_SCHEMA` with Ajv → **PASS**. - * Negative tests: extra fields → FAIL; wrong enum → FAIL; non-10 `styles_top10` length → FAIL. - * Edge: `scale_guesses` `null` values + low confidence should **PASS**. +- **Analysis Strict-JSON Harness** (in-browser): + - Given a room photo and a valid hard-coded key, when calling analysis, then the response **parses** and **validates** against `ANALYSIS_SCHEMA` with Ajv → **PASS**. + - Negative tests: extra fields → FAIL; wrong enum → FAIL; non-10 `styles_top10` length → FAIL. + - Edge: `scale_guesses` `null` values + low confidence should **PASS**. -* **Prompt sanity**: snapshots of rendered prompts for each style to avoid accidental drift in future edits. +- **Prompt sanity**: snapshots of rendered prompts for each style to avoid accidental drift in future edits. -* **Render pipeline smoke**: with mocks (if key absent) ensure UI handles tiles loading, success, and per-tile failure states. +- **Render pipeline smoke**: with mocks (if key absent) ensure UI handles tiles loading, success, and per-tile failure states. # Deployment -* **GitHub Pages**, single `index.html` entry (root), relative asset paths. -* Add `.nojekyll` to avoid Jekyll processing. -* Keep CSP meta in `index.html`. +- **GitHub Pages**, single `index.html` entry (root), relative asset paths. +- Add `.nojekyll` to avoid Jekyll processing. +- Keep CSP meta in `index.html`. -# Iteration development process rules +# Iteration development process rules 1. **Do exactly one prioritized task per iteration.** Before/after: run relevant checks (build/lint/tests or in-browser harness). 2. **When adding/updating tests**, include a brief “why this test matters” note to guide future changes. 3. **Before adding functionality**, search the codebase (ripgrep) to confirm it’s missing; if present, prefer **refactor** over re-implementation. 4. **After each iteration**, add a concise update to `docs/implementation-progress.md` (what changed, decisions, follow-ups). -5. **Prefer CI-friendly, non-interactive commands/reporters** where possible so runs can be automated later. \ No newline at end of file +5. **Prefer CI-friendly, non-interactive commands/reporters** where possible so runs can be automated later. diff --git a/config/.dependency-cruiser.js b/config/.dependency-cruiser.js index 9dc11d8..0cf2089 100644 --- a/config/.dependency-cruiser.js +++ b/config/.dependency-cruiser.js @@ -3,18 +3,24 @@ module.exports = { options: { doNotFollow: { path: 'node_modules' }, includeOnly: 'src', - reporterOptions: { dot: { collapsePattern: 'node_modules/[^/]*' } } + exclude: { path: '((^|\\/)\\.stryker-tmp/)|((^|\\/)playwright-report/)' }, + reporterOptions: { dot: { collapsePattern: 'node_modules/[^/]*' } }, }, forbidden: [ // No circular dependencies - { name: 'no-circular', severity: 'error', from: {}, to: { circular: true } }, - + { + name: 'no-circular', + severity: 'error', + from: {}, + to: { circular: true }, + }, + // Enforce a simple layered architecture: utils can't import from components { - name: 'no-utils-importing-from-components', - severity: 'error', - from: { path: '^src/utils' }, - to: { path: '^src/components' } - } - ] + name: 'no-utils-importing-from-components', + severity: 'error', + from: { path: '^src/utils' }, + to: { path: '^src/components' }, + }, + ], }; diff --git a/config/.jscpd.json b/config/.jscpd.json index 5ced7f4..b280b9d 100644 --- a/config/.jscpd.json +++ b/config/.jscpd.json @@ -3,7 +3,14 @@ "minTokens": 50, "reporters": ["consoleFull"], "files": ["src/**/*.js"], - "ignore": ["**/node_modules/**", "**/dist/**", "**/coverage/**", "**/*.test.js", "**/*.property.test.js"], - "exclude": ["**/node_modules/**", "**/dist/**", "**/coverage/**", "**/*.test.js", "**/*.property.test.js"], + "ignore": [ + "**/node_modules/**", + "**/dist/**", + "**/coverage/**", + "**/.stryker-tmp/**", + "**/playwright-report/**", + "**/*.test.js", + "**/*.property.test.js" + ], "gitignore": true } diff --git a/config/babel.config.js b/config/babel.config.js index 8283743..c74fb53 100644 --- a/config/babel.config.js +++ b/config/babel.config.js @@ -1,3 +1,3 @@ module.exports = { - presets: [['@babel/preset-env', {targets: {node: 'current'}}]], + presets: [['@babel/preset-env', { targets: { node: 'current' } }]], }; diff --git a/config/eslint.config.js b/config/eslint.config.js index 044e09a..f644ec9 100644 --- a/config/eslint.config.js +++ b/config/eslint.config.js @@ -2,9 +2,24 @@ const globals = require('globals'); const js = require('@eslint/js'); module.exports = [ + { + ignores: [ + // Config lives in config/, so eslint resolves globs relative to this file. + // Include both parent-relative and repo-root patterns to keep generated artifacts out + // regardless of where eslint is launched from. + '../coverage/**', + 'coverage/**', + '../reports/**', + 'reports/**', + '../node_modules/**', + '../playwright-report/**', + 'playwright-report/**', + '../test-results/**', + 'test-results/**', + ], + }, js.configs.recommended, { - ignores: ['coverage/**', 'reports/**', 'node_modules/**'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', @@ -18,7 +33,7 @@ module.exports = [ 'no-eval': 'error', 'no-implied-eval': 'error', 'no-extend-native': 'error', - 'complexity': ['warn', 10], + complexity: ['warn', 10], 'max-depth': ['warn', 4], }, }, diff --git a/config/jest.config.js b/config/jest.config.js index 6e35efe..da810f6 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -1,18 +1,24 @@ // Set rootDir to the repository root so Jest can find tests and source files // Since this config file is in ./config, we need to go up one level to reach the repo root +const path = require('node:path'); + module.exports = { rootDir: '../', - testPathIgnorePatterns: ['/node_modules/', '/.stryker-tmp/'], + testPathIgnorePatterns: [ + '/node_modules/', + '/.stryker-tmp/', + '/playwright-ui-tests/', + ], collectCoverage: true, // Include all source files so newly added modules without tests are reported with 0% coverage // This ensures gaps (e.g. currently untested audio.js) are visible and will impact thresholds collectCoverageFrom: [ - 'src/**/*.js', // all JS sources - '!src/**/*.test.js', // exclude standard unit tests + 'src/**/*.js', // all JS sources + '!src/**/*.test.js', // exclude standard unit tests '!src/**/*.property.test.js', // exclude property-based tests - '!src/**/__mocks__/**' // exclude any mocks if added later + '!src/**/__mocks__/**', // exclude any mocks if added later ], - coverageReporters: ["json", "lcov", "text", "clover"], + coverageReporters: ['json', 'lcov', 'text', 'clover'], coverageThreshold: { global: { branches: 85, @@ -21,5 +27,12 @@ module.exports = { statements: 90, }, }, + // Transpile ESM test files (and the modules they import) so Jest's CommonJS runtime can execute them: + transform: { + '^.+\\.js$': [ + 'babel-jest', + { configFile: path.join(__dirname, 'babel.config.js') }, + ], + }, testEnvironment: 'jsdom', }; diff --git a/docs/autonomous-agents-setup.md b/docs/autonomous-agents-setup.md index 1786940..9f86843 100644 --- a/docs/autonomous-agents-setup.md +++ b/docs/autonomous-agents-setup.md @@ -5,40 +5,48 @@ This repository is configured to work with autonomous AI agents that can automat ## Available Agents ### 1. Claude Code Agent + **Trigger:** Comment `@claude` on any issue or PR **Capabilities:** Full feature implementation, bug fixes, code refactoring **Workflow:** `.github/workflows/claude-agent.yml` **Setup Requirements:** + 1. Install Claude Code GitHub App: https://github.com/apps/claude-code 2. Add `ANTHROPIC_API_KEY` to repository secrets 3. Ensure repository permissions allow Actions to create PRs **Usage Example:** + ``` @claude please implement the user authentication feature described in this issue ``` -### 2. Cursor CLI Agent +### 2. Cursor CLI Agent + **Trigger:** Comment `/cursor start` on any issue **Capabilities:** Feature implementation with direct CLI access **Workflow:** `.github/workflows/cursor-agent.yml` **Setup Requirements:** + 1. Add `CURSOR_API_KEY` to repository secrets 2. Ensure repository has write permissions for Actions **Usage Example:** + ``` /cursor start ``` ### 3. Quality Assurance Agent + **Trigger:** Automatic (daily at 2 AM UTC) or manual dispatch **Capabilities:** Comprehensive quality checks, security audits, automated issue creation **Workflow:** `.github/workflows/qa-agent.yml` **Features:** + - Daily quality scans - Automatic issue creation for problems - Security vulnerability detection @@ -47,7 +55,9 @@ This repository is configured to work with autonomous AI agents that can automat ## Repository Configuration ### Agent Guidelines (`AGENTS.md`) + Canonical agent instructions live in the repository root `AGENTS.md`. It includes: + - Development standards and quality requirements - Testing strategies and coverage requirements - File structure conventions @@ -55,7 +65,9 @@ Canonical agent instructions live in the repository root `AGENTS.md`. It include - Success criteria and validation steps ### Cursor Rules (`.cursor/rules`) + Provides Cursor CLI agents with: + - Project overview and principles - Development workflow guidelines - Code standards and testing requirements @@ -64,16 +76,19 @@ Provides Cursor CLI agents with: ## Issue Templates ### Feature Request Template + - **File:** `.github/ISSUE_TEMPLATE/feature_request.md` - **Purpose:** Structured feature requests with clear requirements - **Agent-friendly:** Includes implementation guidance and testing strategy -### Bug Report Template +### Bug Report Template + - **File:** `.github/ISSUE_TEMPLATE/bug_report.md` - **Purpose:** Detailed bug reports with reproduction steps - **Agent-friendly:** Includes investigation notes and fix requirements ### Agent Task Template + - **File:** `.github/ISSUE_TEMPLATE/agent_task.md` - **Purpose:** Tasks specifically designed for autonomous agents - **Features:** Clear objectives, technical constraints, success criteria @@ -81,6 +96,7 @@ Provides Cursor CLI agents with: ## Quality gates and validation To avoid duplication, refer to the canonical guidance in AGENTS.md for: + - Required tests and quality checks - Exact validation commands to run locally and before PRs @@ -89,18 +105,23 @@ Agents should follow AGENTS.md as the single source of truth and must pass all c ## Security and Permissions ### Repository Secrets Required + - `ANTHROPIC_API_KEY` - For Claude Code agent - `CURSOR_API_KEY` - For Cursor CLI agent - `GITHUB_TOKEN` - Automatically provided (for PR/issue management) ### Workflow Permissions + All agent workflows are configured with minimal required permissions: + - `contents: write` - For code changes - `pull-requests: write` - For PR creation - `issues: write` - For status updates ### Branch Protection + Recommended settings: + - Require PR reviews before merging - Require status checks to pass - Restrict pushes to main branch @@ -109,6 +130,7 @@ Recommended settings: ## Usage Patterns ### 1. Feature Development + 1. Create issue using Feature Request template 2. Comment `@claude` or `/cursor start` to assign to agent 3. Agent creates branch, implements feature, writes tests @@ -116,13 +138,15 @@ Recommended settings: 5. Human review and merge ### 2. Bug Fixes -1. Create issue using Bug Report template + +1. Create issue using Bug Report template 2. Include reproduction steps and error details 3. Assign to agent with trigger comment 4. Agent investigates, fixes, and adds regression tests 5. Human verification and merge ### 3. Quality Maintenance + 1. QA Agent runs automatically every night 2. Creates issues for any problems found 3. Can assign these issues to implementation agents @@ -131,16 +155,19 @@ Recommended settings: ## Monitoring and Observability ### Workflow Status + - All workflows post status comments on issues - Success/failure notifications include next steps - Logs available in Actions tab for debugging ### Quality Metrics + - Test coverage reports uploaded as artifacts - Mutation testing reports generated - Quality check results tracked over time ### Cost Management + - Workflow timeouts prevent runaway costs - Concurrency limits prevent parallel execution conflicts - Turn limits cap agent iterations @@ -148,18 +175,21 @@ Recommended settings: ## Troubleshooting ### Agent Not Responding + 1. Check repository secrets are configured 2. Verify workflow permissions 3. Review workflow logs for errors 4. Ensure trigger phrases are exact ### Quality Gate Failures + 1. Review specific failure in CI logs 2. Run tests locally: `npm test` 3. Check quality issues: `npm run check:all` 4. Fix issues incrementally ### Permission Issues + 1. Verify repository settings allow Actions to create PRs 2. Check branch protection rules 3. Ensure secrets are accessible to workflows @@ -167,18 +197,21 @@ Recommended settings: ## Best Practices ### For Maintainers + - Use descriptive issue titles and clear requirements - Review agent-created PRs thoroughly - Maintain branch protection and review requirements - Monitor agent activity and costs ### For Contributors + - Use issue templates for consistent agent input - Provide clear acceptance criteria - Include test scenarios in requirements - Review agent implementations before merging ### For Agent Optimization + - Keep requirements specific and actionable - Include relevant context and constraints - Specify test requirements explicitly @@ -187,16 +220,21 @@ Recommended settings: ## Advanced Configuration ### Custom Agent Triggers + Modify workflow files to add custom trigger phrases: + ```yaml if: contains(github.event.comment.body, 'your-custom-trigger') ``` ### Environment-Specific Settings + Configure different behaviors for different environments by modifying workflow conditions and environment variables. ### Integration with External Tools + Agents can be extended to integrate with: + - External APIs for testing - Deployment systems - Monitoring tools diff --git a/docs/code-quality-specification.md b/docs/code-quality-specification.md index 43eaa7f..db175fd 100644 --- a/docs/code-quality-specification.md +++ b/docs/code-quality-specification.md @@ -6,28 +6,29 @@ Objective: ensure that any change (human- or AI-authored) satisfies general corr ## 1) Scope & Goals -* Prevent “cheat” implementations (lookup tables, case-by-case conditionals, trivial heuristics) from passing tests. -* Enforce **property-based** and **metamorphic** validation, **differential checks**, and **fuzzing**. -* Require **mutation-score gates** and **branch coverage thresholds**. -* Enforce **code reuse**, **layered architecture**, and avoid **copy-paste** and **cycles**. -* Provide reproducible local dev workflow and CI that runs with multiple randomized seeds. +- Prevent “cheat” implementations (lookup tables, case-by-case conditionals, trivial heuristics) from passing tests. +- Enforce **property-based** and **metamorphic** validation, **differential checks**, and **fuzzing**. +- Require **mutation-score gates** and **branch coverage thresholds**. +- Enforce **code reuse**, **layered architecture**, and avoid **copy-paste** and **cycles**. +- Provide reproducible local dev workflow and CI that runs with multiple randomized seeds. --- ## 2) Definitions -* **Core function**: any non-trivial pure/mostly-pure function in `src/` responsible for algorithmic behavior. -* **Metamorphic relation**: predictable output relation under a controlled input transformation (e.g., scale/permute/translate). +- **Core function**: any non-trivial pure/mostly-pure function in `src/` responsible for algorithmic behavior. +- **Metamorphic relation**: predictable output relation under a controlled input transformation (e.g., scale/permute/translate). --- ## 3) Tooling (reference only) -Core tools: Jest, fast-check, Stryker, ESLint, jscpd, dependency-cruiser, madge. Use the scripts defined in `package.json` and configurations under `config/`. For exact commands and validation flow, follow `../AGENTS.md`. +Core tools: Jest, fast-check, Stryker, ESLint, jscpd, dependency-cruiser, madge. Use the scripts defined in `package.json`, the tool configs under `config/`, and the Stryker config in `mutation-testing/stryker.conf.json`. For exact commands and validation flow, follow `../AGENTS.md`. --- ## 4) Test layout (co-located) + Place tests next to code: `*.test.js` for unit tests and `*.property.test.js` for property/metamorphic tests under `src/**`. Keep tests deterministic and fast. --- @@ -36,37 +37,36 @@ Place tests next to code: `*.test.js` for unit tests and `*.property.test.js` fo ### 5.1 General Implementation Rules -* Implement **general algorithms**; avoid large `switch/case` or literal maps keyed by example inputs. -* Prefer **composition** and reuse of existing utilities. -* Avoid new external deps unless justified (see §7.3 dependency guard). +- Implement **general algorithms**; avoid large `switch/case` or literal maps keyed by example inputs. +- Prefer **composition** and reuse of existing utilities. +- Avoid new external deps unless justified (see §7.3 dependency guard). ### 5.2 Property‑Based Testing (PBT) -* For each **core function**, add at least one property test using `fast-check`. -* Minimum **200 runs** per property in CI; locally may be reduced for speed. -* Typical properties to consider (choose those fitting the domain): - - * **Idempotence**: \`f(f(x)) === f(x)\` - * **Inverse**: \`decode(encode(x)) === x\` - * **Monotonicity/Ordering**: \`a ≤ b ⇒ f(a) ≤ f(b)\` - * **Algebraic laws**: associativity, commutativity, identity, inverse - * **Invariance**: permutation/translation/scale invariance where expected +- For each **core function**, add at least one property test using `fast-check`. +- Minimum **200 runs** per property in CI; locally may be reduced for speed. +- Typical properties to consider (choose those fitting the domain): + - **Idempotence**: \`f(f(x)) === f(x)\` + - **Inverse**: \`decode(encode(x)) === x\` + - **Monotonicity/Ordering**: \`a ≤ b ⇒ f(a) ≤ f(b)\` + - **Algebraic laws**: associativity, commutativity, identity, inverse + - **Invariance**: permutation/translation/scale invariance where expected ### 5.3 Metamorphic Testing -* At least **one metamorphic relation per core function**. -* Example patterns: - - * **Scale invariance**: scaling inputs changes magnitude but preserves direction - * **Permutation invariance**: reordering inputs yields identical reduced result - * **Additive translation**: adding a constant offsets outputs predictably +- At least **one metamorphic relation per core function**. +- Example patterns: + - **Scale invariance**: scaling inputs changes magnitude but preserves direction + - **Permutation invariance**: reordering inputs yields identical reduced result + - **Additive translation**: adding a constant offsets outputs predictably ### 5.5 Fuzz & Edge Cases -* Generate extremes: empty arrays, duplicates, long strings, Unicode, NaN/Infinity, boundary numerics. -* Seed handling: use env var **`FAST_CHECK_SEED`** to replay locally; CI overrides with multiple seeds. +- Generate extremes: empty arrays, duplicates, long strings, Unicode, NaN/Infinity, boundary numerics. +- Seed handling: use env var **`FAST_CHECK_SEED`** to replay locally; CI overrides with multiple seeds. ### 5.6 Performance Guards (optional) + For hot paths, add a simple benchmark and watch for regressions. Keep this lightweight. --- @@ -74,12 +74,15 @@ For hot paths, add a simple benchmark and watch for regressions. Keep this light ## 6) Test quality gates ### 6.1 Mutation Testing Gate + Minimum mutation score: >= 50%. Run Stryker in validation; fail if below. ### 6.2 Coverage Gate + Branch coverage >= 90% (and lines/functions/statements similarly high). Enforced via Jest config. ### 6.3 Property presence & test shape + At least one property test per core function using `.property.test.js`. --- @@ -87,23 +90,31 @@ At least one property test per core function using `.property.test.js`. ## 7) Architectural & reuse rules ### 7.1 Duplication guard (jscpd) + Threshold ≤ 1% duplicated lines. Prefer reuse and refactor when exceeded. ### 7.2 Module boundaries (dependency-cruiser) + Keep layers clean, avoid deep imports, and forbid cycles. Rules live in `config/.dependency-cruiser.js`. ### 7.3 Dependency hygiene + Avoid adding external deps unless necessary and justified. ### 7.4 Cycle detection (madge) + Detect and fail on cycles. --- ## 8) ESLint & static rules (anti-cheat & hygiene) + Ban `eval`, `new Function`, and extending built-ins; flag overly large literal maps and `switch/case` used as lookup tables; enforce boundaries via dependency-cruiser. + ## 9) Config and commands (canonical) + Avoid duplicating scripts/config here. Use: + - Commands and validation flow: `../AGENTS.md` - Scripts: `package.json` - Tool configs: `config/` directory @@ -111,21 +122,25 @@ Avoid duplicating scripts/config here. Use: --- ## 10) CI/CD + CI pipelines should run tests, mutation, and quality checks using the scripts above. Keep CI definitions DRY and aligned with `AGENTS.md`. --- ## 11) Example property test + Keep examples minimal; see tests under `src/**` for patterns. Prefer domain-relevant properties and metamorphic relations. --- ## 12) Acceptance criteria (PR gate) + Merge only when: tests (incl. property/metamorphic) pass, mutation score ≥ 50%, coverage thresholds met, duplication ≤ 1%, boundaries and cycles pass, lint passes, and any new deps are justified. --- ## 13) Developer checklist (pre-PR) + - General solution; no lookup tables or case-by-case logic - Property-based test(s) added/updated (≥ 200 runs) and at least one metamorphic relation - Coverage meets thresholds; mutation score meets threshold @@ -135,4 +150,5 @@ Merge only when: tests (incl. property/metamorphic) pass, mutation score ≥ 50% --- ### Notes + Thresholds can be ratcheted up as the codebase improves. Keep scripts/config single-sourced under `config/` and `package.json`. diff --git a/docs/implementation-progress.md b/docs/implementation-progress.md index a2f98bb..1e367cb 100644 --- a/docs/implementation-progress.md +++ b/docs/implementation-progress.md @@ -1,12 +1,14 @@ # Implementation progress — MoodCanvas ## 2024-11-24 + - Bootstrapped the client-only MoodCanvas application shell with Tailwind Play CDN and dark Plum–Peach theme. - Implemented local BYOK handling, IndexedDB project store, and sequential chat-like timeline for the room design workflow. - Added prompt builders, Gemini client integration scaffolding, and unit tests covering prompt generation utilities. - Persisted project media/artifacts locally and wired gallery, A/B, and hero render cards with concurrency scaffolding. ## 2025-10-02 + - Fixed Gemini client fetch invocation to run within the browser global context, resolving the "Illegal invocation" runtime error when running analysis. - Added a unit test ensuring the client binds the fetch implementation correctly to prevent regressions. - Sanitized the analysis response schema to match Gemini's structured output format and added regression tests to block unsupported keywords. @@ -15,11 +17,61 @@ - Further simplified the structured-output schema (removed enums and nested `required` lists) to avoid Gemini's "too many states" errors. ## 2025-10-02 (palette selection) + - Added a selectable 60-30-10 palette chooser that surfaces five options and gates the style gallery until a palette is confirmed. - Extended the analysis schema and gallery prompt builder to accept palette overrides, including new tests for the override behavior. - Implemented palette synthesis helpers to generate fallback palette variations when the analysis response does not provide five distinct options. ## 2025-10-02 (palette diversity upgrade) + - Reworked palette option synthesis to operate in HSL space and inject stronger hue/saturation shifts plus complementary fallbacks so the five palettes feel distinct. - Extracted the palette helpers into their own utility module and updated the app shell to consume the shared logic. - Added extensive unit coverage for edge cases (invalid hex values, provided-only palettes, grayscale inputs) to keep the global coverage gate above 90%. + +## 2025-10-04 (Iteration loop overhaul) + +- Replaced the analysis-first workflow with the Iteration 2 spec: single photo ingest, optional drag-sort references, silent room brief regeneration, and a 12-tile iterative gallery powered by a diversity deck. +- Added a sticky generate bar with guidance input that only appears once favorites are queued, plus modal compare-and-replace finalization using hi-res re-renders and build notes. +- Implemented Gemini iteration utilities (room brief, variant, finalization parsers) with resilient text auto-repair and a pool-based runner, backed by unit tests covering parser edge cases and prompt construction. +- Updated the silent room-brief prompt to match the Iteration 2 spec block format, including explicit input listings and constraint hints. +- Converted the initial "Start" control into a dual-purpose restart button that clears generated iterations per spec, and we now reset queued favorites as soon as a new round begins so the next selection cycle starts cleanly. + +## 2025-10-12 (gallery layout tweak) + +- Expanded the iteration timeline container to the full viewport width, forced iteration tiles into a single-column grid, and switched the preview frames to a 3:2 aspect ratio so each render scales tall enough to fill the available space on every screen size. + +## 2025-10-12 (tile details toggle) + +- Replaced the unused tile "Details" button with a click-to-expand description that shows only a short preview by default and reveals the full copy plus user notes on demand, reducing visual noise while keeping the annotation flow intact. + +## 2025-10-12 (room structure guardrails) + +- Clarified iteration, gallery, and finalization prompts so Gemini keeps walls, openings, and ceiling heights fixed to the source photo. +- Documented that reference images serve style cues only and must not influence room geometry. +- Added unit test expectations around the new guardrail language to prevent regressions. + +## 2025-10-12 (first-run focus) + +- Moved the first-iteration launch control into its own post-brief section so the "Start generating" button lives directly beneath the Silent Room Brief card. +- Hid the generate-next-iteration bar until the first gallery exists and stopped mounting the compare-final modal until a hi-res candidate is available, keeping the pre-run UI minimal. +- Added layout/unit tests that lock in the new control order and lazy modal creation to catch regressions. + +## 2025-10-12 (silent brief editing) + +- Swapped the static Silent Room Brief copy for editable inputs so the summary paragraph and constraint bullets can be tuned before launching a run. +- Added guardrails for constraint rows (add/remove up to five) and wired state updates on input events so iteration prompts reuse the edited text. +- Extended the layout spec to assert the editable controls render with the generated brief content. + +## 2025-10-12 (door & window guardrails) + +- Reinforced iteration, gallery, and finalization prompts so doors, windows, and room entries stay fixed to the source photo. +- Updated prompt unit tests to assert the new guardrail language for future regressions. + +## 2025-10-13 (iteration prompt simplification) + +- Simplified the iteration prompt wording to mirror the Iteration 2 spec and cut redundant architectural guardrails. +- Adjusted iteration prompt unit tests to check the leaner phrasing while keeping geometry preservation coverage. + +``` + +``` diff --git a/docs/template-development-specification.md b/docs/template-development-specification.md index b7184b7..e3adedc 100644 --- a/docs/template-development-specification.md +++ b/docs/template-development-specification.md @@ -3,12 +3,14 @@ Purpose: Practical, minimal guidance for building and iterating on this no-build, static SPA with modern JS and AI-assisted workflows. ## 1) Core principles + - Requirements first: define user stories and acceptance criteria. - Small, focused iterations: one feature/bugfix per commit. - AI-assisted coding: write a short spec, then implement with tests. - Keep it simple: static HTML + modular JS; no bundlers. ## 2) Project structure + ``` / ├── index.html # Entry HTML @@ -21,31 +23,37 @@ Purpose: Practical, minimal guidance for building and iterating on this no-build ├── config/ # Jest/ESLint/etc. configs └── package.json # Scripts and dev tooling ``` + Naming: camelCase files; `*.test.js` for unit tests; `*.property.test.js` for fast-check tests. ## 3) Development practices + - Single responsibility modules with explicit exports; minimize dependencies. - Co-locate tests with code; keep tests deterministic and fast. - Separate concerns: structure in HTML, behavior in `src/`, data in `assets/`. - Prefer plain ES modules and browser APIs; avoid framework lock-in. ## 4) Workflow (high level) -1) Write/confirm a short requirement. 2) Implement incrementally. 3) Add/update tests alongside code. 4) Run quality checks. 5) Commit and push. + +1. Write/confirm a short requirement. 2) Implement incrementally. 3) Add/update tests alongside code. 4) Run quality checks. 5) Commit and push. Commands, quality gates, and validation are defined centrally in AGENTS.md. Use those as the single source of truth. ## 5) Architecture guidelines + - Components: small, testable, and composable; keep data flow explicit. - Errors: validate inputs, fail fast with helpful messages, and degrade gracefully. - Boundaries: keep components/utils decoupled; avoid circular deps. ## 6) Customization + - Safe: update `index.html`, add modules under `src/components` and `src/utils`, expand `pages/`, and add data in `src/assets`. - Config: adjust files under `config/` only when necessary; prefer minimal changes. ## 7) References (canonical) + - AGENTS and quality gates: ../AGENTS.md - Code quality details: ./code-quality-specification.md - Project overview & Pages: ../README.md -Use this concise spec for day-to-day work. For exact commands, standards, and validation steps, follow AGENTS.md. \ No newline at end of file +Use this concise spec for day-to-day work. For exact commands, standards, and validation steps, follow AGENTS.md. diff --git a/docs/tests-and-lint-as-precommit-hook.md b/docs/tests-and-lint-as-precommit-hook.md index 8b229af..32a4a48 100644 --- a/docs/tests-and-lint-as-precommit-hook.md +++ b/docs/tests-and-lint-as-precommit-hook.md @@ -69,5 +69,5 @@ git commit --allow-empty -m "test hook" ## Additional Notes -* Developers can bypass local hooks with `--no-verify`, so keep CI with required checks as the final gate. -* For faster commits, consider moving long-running suites to a `pre-push` hook or to CI. +- Developers can bypass local hooks with `--no-verify`, so keep CI with required checks as the final gate. +- For faster commits, consider moving long-running suites to a `pre-push` hook or to CI. diff --git a/docs/ui-test-best-practices.md b/docs/ui-test-best-practices.md index 4538329..92f6d08 100644 --- a/docs/ui-test-best-practices.md +++ b/docs/ui-test-best-practices.md @@ -1,5 +1,3 @@ -**Primary Assistant:** - # Playwright + GitHub Copilot (incl. Codespaces & CI) — Developer Report (npm + JavaScript) This report consolidates setup and best-practices for running **Playwright** with **GitHub Copilot** locally in VS Code, inside **GitHub Codespaces**, and in **GitHub Actions**—with specific safeguards to **avoid any blocking/interactive artifacts** (e.g., “Serving HTML report… Press Ctrl+C to quit.”). @@ -8,13 +6,13 @@ This report consolidates setup and best-practices for running **Playwright** wit ## 0) Key non-blocking principles (applies everywhere) -* **Never call** `npx playwright show-report` in automated flows (CI, agent runs, LLM tools); it starts a local server and blocks the process. Instead, **upload the HTML folder as an artifact** or consume **JSON/JUnit** reports. The HTML report auto-open behavior can be disabled via config or env var (see §2 and §4). ([playwright.dev][1]) -* Force HTML report behavior to **not open** in any environment by setting either: +- **Never call** `npx playwright show-report` in automated flows (CI, agent runs, LLM tools); it starts a local server and blocks the process. Instead, **upload the HTML folder as an artifact** or consume **JSON/JUnit** reports. The HTML report auto-open behavior can be disabled via config or env var (see §2 and §4). ([playwright.dev][1]) +- Force HTML report behavior to **not open** in any environment by setting either: + - `reporter: [['html', { open: 'never' }]]` in `playwright-ui-tests/playwright.config.js`, or + - `PLAYWRIGHT_HTML_OPEN=never` in the environment. ([playwright.dev][1]) - * `reporter: [['html', { open: 'never' }]]` in `playwright.config.js`, or - * `PLAYWRIGHT_HTML_OPEN=never` in the environment. ([playwright.dev][1]) -* Prefer non-interactive reporters in pipelines/agent runs: `github`, `json`, `junit`, or `dot/line`. ([playwright.dev][1]) -* Avoid `--ui` in automation. **UI Mode** is for humans; it runs a dev server. If you must use it in Codespaces for manual debugging, bind host/port and close it yourself. ([playwright.dev][2]) +- Prefer non-interactive reporters in pipelines/agent runs: `github`, `json`, `junit`, or `dot/line`. ([playwright.dev][1]) +- Avoid `--ui` in automation. **UI Mode** is for humans; it runs a dev server. If you must use it in Codespaces for manual debugging, bind host/port and close it yourself. ([playwright.dev][2]) --- @@ -35,8 +33,8 @@ npx playwright install "scripts": { "test": "playwright test", "test:headed": "playwright test --headed", - "test:ui": "playwright test --ui", // manual only - "show-report": "playwright show-report", // manual only + "test:ui": "playwright test --ui", // manual only + "show-report": "playwright show-report", // manual only "show-report:last": "playwright show-report playwright-report" } } @@ -44,6 +42,8 @@ npx playwright install ### `playwright.config.js` (JavaScript, safe defaults) +> In this repository, the config file lives at `playwright-ui-tests/playwright.config.js`; adjust paths if you copy these defaults. + ```js // playwright.config.js const { defineConfig, devices } = require('@playwright/test'); @@ -51,29 +51,33 @@ const { defineConfig, devices } = require('@playwright/test'); module.exports = defineConfig({ testDir: './tests', fullyParallel: true, - forbidOnly: !!process.env.CI, // fail build if test.only is committed + forbidOnly: !!process.env.CI, // fail build if test.only is committed retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: process.env.CI - ? [['github'], ['json', { outputFile: 'test-results.json' }], ['junit', { outputFile: 'junit.xml' }]] - : [['list'], ['html', { open: 'never' }]], // never auto-open HTML + ? [ + ['github'], + ['json', { outputFile: 'test-results.json' }], + ['junit', { outputFile: 'junit.xml' }], + ] + : [['list'], ['html', { open: 'never' }]], // never auto-open HTML use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', - video: 'retain-on-failure' + video: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, - { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, - { name: 'webkit', use: { ...devices['Desktop Safari'] } } + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } }, ], webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, - timeout: 120000 - } + timeout: 120000, + }, }); ``` @@ -85,9 +89,9 @@ Rationale: `forbidOnly`, CI-only retries, the `github` reporter for annotations, ## 2) HTML/JSON/JUnit reports without blocking -* The **HTML reporter** writes a static folder (`playwright-report/`). By default, Playwright may **auto-open** on failure; disable it via `open: 'never'` or `PLAYWRIGHT_HTML_OPEN=never`. **Do not** run `show-report` in automation; upload the folder as an artifact. ([playwright.dev][1]) -* For machine consumption, use **JSON** (`--reporter=json`) or **JUnit** (`--reporter=junit`) and write files via config or `PLAYWRIGHT_JSON_OUTPUT_NAME` / `PLAYWRIGHT_JUNIT_OUTPUT_NAME`. ([playwright.dev][1]) -* The “Serving HTML report… Press Ctrl+C to quit.” message is emitted by the **report server** (e.g., after `show-report`); avoid invoking it in CI/agent contexts. Community threads confirm this is what causes blocking in pipelines. ([Stack Overflow][5]) +- The **HTML reporter** writes a static folder (`playwright-report/`). By default, Playwright may **auto-open** on failure; disable it via `open: 'never'` or `PLAYWRIGHT_HTML_OPEN=never`. **Do not** run `show-report` in automation; upload the folder as an artifact. ([playwright.dev][1]) +- For machine consumption, use **JSON** (`--reporter=json`) or **JUnit** (`--reporter=junit`) and write files via config or `PLAYWRIGHT_JSON_OUTPUT_NAME` / `PLAYWRIGHT_JUNIT_OUTPUT_NAME`. ([playwright.dev][1]) +- The “Serving HTML report… Press Ctrl+C to quit.” message is emitted by the **report server** (e.g., after `show-report`); avoid invoking it in CI/agent contexts. Community threads confirm this is what causes blocking in pipelines. ([Stack Overflow][5]) --- @@ -98,22 +102,22 @@ Create `.github/workflows/playwright.yml`: ```yaml name: Playwright Tests on: - push: { branches: [ main, master ] } - pull_request: { branches: [ main, master ] } + push: { branches: [main, master] } + pull_request: { branches: [main, master] } jobs: test: runs-on: ubuntu-latest timeout-minutes: 60 env: - PLAYWRIGHT_HTML_OPEN: "never" # belt & braces: never auto-open + PLAYWRIGHT_HTML_OPEN: 'never' # belt & braces: never auto-open steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: lts/* - cache: npm # npm dependency cache + cache: npm # npm dependency cache - name: Install dependencies run: npm ci @@ -144,9 +148,9 @@ jobs: Why this works: -* The **Playwright GitHub Action is deprecated**; use the CLI + `install --with-deps`. ([GitHub][6], [playwright.dev][7]) -* **Cache npm** via `setup-node@v4` (`cache: 'npm'`). ([GitHub][8], [GitHub Docs][9]) -* Upload the **static HTML folder** instead of serving it; no blocking server. The CI guide explicitly covers how to view reports and traces from artifacts. ([playwright.dev][10]) +- The **Playwright GitHub Action is deprecated**; use the CLI + `install --with-deps`. ([GitHub][6], [playwright.dev][7]) +- **Cache npm** via `setup-node@v4` (`cache: 'npm'`). ([GitHub][8], [GitHub Docs][9]) +- Upload the **static HTML folder** instead of serving it; no blocking server. The CI guide explicitly covers how to view reports and traces from artifacts. ([playwright.dev][10]) --- @@ -173,7 +177,7 @@ Why this works: } ``` -* Use `--with-deps` to install required OS packages inside the container. ([playwright.dev][7]) +- Use `--with-deps` to install required OS packages inside the container. ([playwright.dev][7]) ### Using UI Mode (manual only) @@ -199,35 +203,35 @@ From VS Code CLI: code --add-mcp "{\"name\":\"playwright\",\"command\":\"npx\",\"args\":[\"@playwright/mcp@latest\"]}" ``` -* VS Code documents `--add-mcp` for installing MCP servers. ([code.visualstudio.com][12]) -* GitHub Copilot docs explain agent mode + MCP usage and enterprise policy controls. ([GitHub Docs][13]) -* The Playwright MCP server repo provides stdio config examples and optional flags (e.g., `--storage-state`). ([GitHub][14]) +- VS Code documents `--add-mcp` for installing MCP servers. ([code.visualstudio.com][12]) +- GitHub Copilot docs explain agent mode + MCP usage and enterprise policy controls. ([GitHub Docs][13]) +- The Playwright MCP server repo provides stdio config examples and optional flags (e.g., `--storage-state`). ([GitHub][14]) > Codespaces note: MCP in a devcontainer works the same; you can embed the server in `devcontainer.json` under `customizations.vscode.mcp.servers` for team consistency. (See VS Code MCP docs for managing MCP servers via settings or command palette.) ([code.visualstudio.com][15]) ### Non-blocking when using MCP -* Ask Copilot Agent to **execute tests** (CLI) and **collect JSON/JUnit**; **do not** ask it to “open the HTML report” or “serve the report.” -* If a human wants to inspect the HTML, run `npm run show-report` interactively—outside agent flows. +- Ask Copilot Agent to **execute tests** (CLI) and **collect JSON/JUnit**; **do not** ask it to “open the HTML report” or “serve the report.” +- If a human wants to inspect the HTML, run `npm run show-report` interactively—outside agent flows. --- ## 6) Copilot prompting patterns that yield robust Playwright -* Provide **code context** (component files, routes). Then instruct: +- Provide **code context** (component files, routes). Then instruct: “Generate Playwright tests for `Checkout`. Prefer `getByRole`/`getByLabel`, use web-first assertions, do not use hard waits, baseURL is `/`. Cover add-to-cart, remove, checkout happy path.” -* These match Playwright best practices: **role-based locators**, **web-first assertions**, and keeping selectors resilient. ([playwright.dev][3]) +- These match Playwright best practices: **role-based locators**, **web-first assertions**, and keeping selectors resilient. ([playwright.dev][3]) --- ## 7) Quick compliance checklist -* ✅ **No blocking servers** in automation: don’t invoke `show-report`; set `open: 'never'` or `PLAYWRIGHT_HTML_OPEN=never`. ([playwright.dev][1]) -* ✅ Reporters: **`github`** (CI annotations), plus **JSON/JUnit** for machine reading; HTML folder uploaded as artifact. ([playwright.dev][1]) -* ✅ CI installs: `npx playwright install --with-deps` on Linux. ([playwright.dev][7]) -* ✅ Codespaces UI (manual only): `--ui-host=0.0.0.0` and a forwarded port. ([playwright.dev][2]) -* ✅ Avoid the deprecated **`microsoft/playwright-github-action`**; use CLI. ([GitHub][6]) -* ✅ Standard config hygiene: `forbidOnly`, CI-only retries, trace on first retry, reuse local dev server. ([playwright.dev][3]) +- ✅ **No blocking servers** in automation: don’t invoke `show-report`; set `open: 'never'` or `PLAYWRIGHT_HTML_OPEN=never`. ([playwright.dev][1]) +- ✅ Reporters: **`github`** (CI annotations), plus **JSON/JUnit** for machine reading; HTML folder uploaded as artifact. ([playwright.dev][1]) +- ✅ CI installs: `npx playwright install --with-deps` on Linux. ([playwright.dev][7]) +- ✅ Codespaces UI (manual only): `--ui-host=0.0.0.0` and a forwarded port. ([playwright.dev][2]) +- ✅ Avoid the deprecated **`microsoft/playwright-github-action`**; use CLI. ([GitHub][6]) +- ✅ Standard config hygiene: `forbidOnly`, CI-only retries, trace on first retry, reuse local dev server. ([playwright.dev][3]) --- @@ -253,31 +257,30 @@ Keep agent-facing guidance centralized in `AGENTS.md`. For Playwright specifics, ## 9) Troubleshooting -* **Pipeline hangs after tests**: ensure the job or agent **did not call `show-report`** and that `PLAYWRIGHT_HTML_OPEN=never` is set. See community posts about the blocking behavior. ([Stack Overflow][5]) -* **Ports in Codespaces**: use the forwarded link, not `localhost`; Codespaces manages access and visibility. ([playwright.dev][2]) -* **Slow CI**: install only needed browsers locally during dev (e.g., Chromium), all on CI; cache npm with `setup-node`. ([GitHub][8]) +- **Pipeline hangs after tests**: ensure the job or agent **did not call `show-report`** and that `PLAYWRIGHT_HTML_OPEN=never` is set. See community posts about the blocking behavior. ([Stack Overflow][5]) +- **Ports in Codespaces**: use the forwarded link, not `localhost`; Codespaces manages access and visibility. ([playwright.dev][2]) +- **Slow CI**: install only needed browsers locally during dev (e.g., Chromium), all on CI; cache npm with `setup-node`. ([GitHub][8]) --- -**Devil’s Advocate:** - -* **Agent/MCP variability**: Copilot agent mode with MCP can be **policy-restricted** in some orgs. Keep a fully functional path where Copilot **only generates** tests and CI runs them (your CLI/Actions flow already does this). ([GitHub Docs][13]) -* **HTML report expectations**: Teams often want a clickable report; uploading the static folder is safe, but people may still run `show-report` locally. Document clearly that `show-report` is **human-only**, never in CI/agents. ([playwright.dev][1]) -* **Noise from GH annotations**: With large matrices, the `github` reporter can flood PRs. Consider using `dot` + JSON/JUnit for PR checks and keep HTML as an artifact for deeper inspection. ([playwright.dev][1]) -* **Security**: UI Mode and HTML/trace reports can include sensitive data. In Codespaces, ports are private by default, but review visibility and sanitize artifacts before sharing. ([playwright.dev][2]) - -[1]: https://playwright.dev/docs/test-reporters "Reporters | Playwright" -[2]: https://playwright.dev/docs/test-ui-mode?utm_source=chatgpt.com "UI Mode" -[3]: https://playwright.dev/docs/test-configuration?utm_source=chatgpt.com "Test configuration" -[4]: https://playwright.dev/docs/test-cli?utm_source=chatgpt.com "Command line" -[5]: https://stackoverflow.com/questions/76868048/how-to-finish-running-playwright-tests-in-pipeline?utm_source=chatgpt.com "How to finish running playwright tests in pipeline?" -[6]: https://github.com/microsoft/playwright-github-action?utm_source=chatgpt.com "microsoft/playwright-github-action" -[7]: https://playwright.dev/docs/ci?utm_source=chatgpt.com "Continuous Integration" -[8]: https://github.com/actions/setup-node?utm_source=chatgpt.com "actions/setup-node" -[9]: https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs?utm_source=chatgpt.com "Building and testing Node.js - GitHub Actions" -[10]: https://playwright.dev/docs/ci-intro?utm_source=chatgpt.com "Setting up CI" -[11]: https://github.com/nitya/playwright-learn-codespaces-nn/?utm_source=chatgpt.com "nitya/playwright-learn-codespaces-nn" -[12]: https://code.visualstudio.com/docs/copilot/chat/mcp-servers?utm_source=chatgpt.com "Use MCP servers in VS Code" -[13]: https://docs.github.com/en/copilot/tutorials/enhance-agent-mode-with-mcp?utm_source=chatgpt.com "Enhancing GitHub Copilot agent mode with MCP" -[14]: https://github.com/microsoft/playwright-mcp?utm_source=chatgpt.com "microsoft/playwright-mcp: Playwright MCP server" -[15]: https://code.visualstudio.com/docs/copilot/customization/mcp-servers?utm_source=chatgpt.com "Use MCP servers in VS Code" +## 10) Potential risks to look out for + +- **HTML report expectations**: Teams often want a clickable report; uploading the static folder is safe, but people may still run `show-report` locally. Document clearly that `show-report` is **human-only**, never in CI/agents. ([playwright.dev][1]) +- **Noise from GH annotations**: With large matrices, the `github` reporter can flood PRs. Consider using `dot` + JSON/JUnit for PR checks and keep HTML as an artifact for deeper inspection. ([playwright.dev][1]) +- **Security**: UI Mode and HTML/trace reports can include sensitive data. In Codespaces, ports are private by default, but review visibility and sanitize artifacts before sharing. ([playwright.dev][2]) + +[1]: https://playwright.dev/docs/test-reporters 'Reporters | Playwright' +[2]: https://playwright.dev/docs/test-ui-mode?utm_source=chatgpt.com 'UI Mode' +[3]: https://playwright.dev/docs/test-configuration?utm_source=chatgpt.com 'Test configuration' +[4]: https://playwright.dev/docs/test-cli?utm_source=chatgpt.com 'Command line' +[5]: https://stackoverflow.com/questions/76868048/how-to-finish-running-playwright-tests-in-pipeline?utm_source=chatgpt.com 'How to finish running playwright tests in pipeline?' +[6]: https://github.com/microsoft/playwright-github-action?utm_source=chatgpt.com 'microsoft/playwright-github-action' +[7]: https://playwright.dev/docs/ci?utm_source=chatgpt.com 'Continuous Integration' +[8]: https://github.com/actions/setup-node?utm_source=chatgpt.com 'actions/setup-node' +[9]: https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs?utm_source=chatgpt.com 'Building and testing Node.js - GitHub Actions' +[10]: https://playwright.dev/docs/ci-intro?utm_source=chatgpt.com 'Setting up CI' +[11]: https://github.com/nitya/playwright-learn-codespaces-nn/?utm_source=chatgpt.com 'nitya/playwright-learn-codespaces-nn' +[12]: https://code.visualstudio.com/docs/copilot/chat/mcp-servers?utm_source=chatgpt.com 'Use MCP servers in VS Code' +[13]: https://docs.github.com/en/copilot/tutorials/enhance-agent-mode-with-mcp?utm_source=chatgpt.com 'Enhancing GitHub Copilot agent mode with MCP' +[14]: https://github.com/microsoft/playwright-mcp?utm_source=chatgpt.com 'microsoft/playwright-mcp: Playwright MCP server' +[15]: https://code.visualstudio.com/docs/copilot/customization/mcp-servers?utm_source=chatgpt.com 'Use MCP servers in VS Code' diff --git a/index.html b/index.html index 69d5980..07a040c 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + @@ -8,8 +8,11 @@ content="default-src 'self'; img-src 'self' blob: data:; connect-src 'self' https://generativelanguage.googleapis.com; script-src 'self' 'sha256-vvt4KWwuNr51XfE5m+hzeNEGhiOfZzG97ccfqGsPwvE='; style-src 'self'; object-src 'none'; base-uri 'self';" /> MoodCanvas · Interior Design Assistant - - + +
diff --git a/mutation-testing/stryker.conf.json b/mutation-testing/stryker.conf.json new file mode 100644 index 0000000..012a3bc --- /dev/null +++ b/mutation-testing/stryker.conf.json @@ -0,0 +1,13 @@ +{ + "testRunner": "jest", + "reporters": ["clear-text", "json", "progress", "html"], + "mutate": ["src/**/*.js", "!src/**/*.test.js"], + "coverageAnalysis": "perTest", + "thresholds": { "high": 90, "low": 60, "break": 50 }, + "jsonReporter": { + "fileName": "mutation-testing/reports/mutation-report.json" + }, + "htmlReporter": { + "fileName": "mutation-testing/reports/html/index.html" + } +} diff --git a/package-lock.json b/package-lock.json index 95f095a..4c0ab27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,48 +9,25 @@ "version": "0.1.0", "license": "ISC", "devDependencies": { - "@babel/core": "^7.27.4", - "@babel/preset-env": "^7.27.2", - "@eslint/js": "^9.33.0", - "@stryker-mutator/core": "^9.0.1", - "@stryker-mutator/jest-runner": "^9.0.1", - "babel-jest": "^30.0.0-beta.3", - "dependency-cruiser": "^17.0.1", - "eslint": "^9.33.0", + "@babel/core": "^7.28.4", + "@babel/preset-env": "^7.28.3", + "@eslint/js": "^9.37.0", + "@playwright/test": "^1.49.0", + "@stryker-mutator/core": "^9.2.0", + "@stryker-mutator/jest-runner": "^9.2.0", + "babel-jest": "^30.2.0", + "dependency-cruiser": "^17.0.2", + "eslint": "^9.37.0", "eslint-plugin-jest": "^29.0.1", - "fast-check": "^4.2.0", - "globals": "^16.3.0", + "fast-check": "^4.3.0", + "globals": "^16.4.0", "jest": "^29.7.0", - "jest-environment-jsdom": "^30.0.5", + "jest-environment-jsdom": "^30.2.0", "jscpd": "^4.0.5", "madge": "^8.0.0", - "tailwindcss": "^3.4.14" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "prettier": "^3.3.3", + "serve": "^14.2.1", + "tailwindcss": "^4.1.14" } }, "node_modules/@asamuzakjp/css-color": { @@ -88,32 +65,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", - "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, - "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -129,16 +104,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -176,18 +150,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.3", "semver": "^6.3.1" }, "engines": { @@ -216,22 +189,30 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", - "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.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, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", @@ -261,15 +242,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -397,27 +377,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, - "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -494,14 +472,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -511,9 +488,9 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.27.1.tgz", - "integrity": "sha512-DTxe4LBPrtFdsWzgpmbBKevg3e9PBy+dXRt19kSbucbZvL2uqtdqwwpluL1jfxYE0wIDTFp1nTy/q6gNLsxXrg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", + "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", @@ -527,23 +504,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-explicit-resource-management": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-explicit-resource-management/-/plugin-proposal-explicit-resource-management-7.27.4.tgz", - "integrity": "sha512-1SwtCDdZWQvUU1i7wt/ihP7W38WjC3CSTOHAl+Xnbze8+bbMNjRvRQydnj0k9J1jPqCAZctBFp6NHJXkrVVmEA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-explicit-resource-management instead.", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -861,15 +821,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", - "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -913,11 +872,10 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz", - "integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -946,13 +904,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -963,18 +920,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", - "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.27.1", - "globals": "^11.1.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -983,15 +939,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", @@ -1010,13 +957,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", - "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1091,6 +1038,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", @@ -1358,16 +1321,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", - "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.3", - "@babel/plugin-transform-parameters": "^7.27.1" + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1427,11 +1390,10 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", - "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1494,11 +1456,10 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz", - "integrity": "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1710,13 +1671,12 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", - "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", @@ -1724,25 +1684,26 @@ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", @@ -1759,15 +1720,15 @@ "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.3", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1780,10 +1741,10 @@ "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.40.0", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "engines": { @@ -1843,37 +1804,27 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.4", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1901,9 +1852,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", - "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "dev": true, "funding": [ { @@ -1943,9 +1894,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", - "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, "funding": [ { @@ -1958,7 +1909,7 @@ } ], "dependencies": { - "@csstools/color-helpers": "^5.0.2", + "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "engines": { @@ -2024,9 +1975,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2077,18 +2028,21 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", "dev": true, + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.15" @@ -2182,9 +2136,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2203,12 +2157,12 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -2627,100 +2581,25 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": "20 || >=22" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "dev": true, "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@isaacs/balanced-match": "^4.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": "20 || >=22" } }, "node_modules/@istanbuljs/load-nyc-config": { @@ -2833,18 +2712,18 @@ } }, "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.0.5.tgz", - "integrity": "sha512-gpWwiVxZunkoglP8DCnT3As9x5O8H6gveAOpvaJd2ATAoSh7ZSSCWbr9LQtUMvr8WD3VjG9YnDhsmkCK5WN1rQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", + "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", "dev": true, "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/jsdom": "^21.1.7", "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2860,45 +2739,32 @@ } }, "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", - "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, "dependencies": { - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5" + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", - "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2917,9 +2783,9 @@ } }, "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", - "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "dependencies": { "@jest/pattern": "30.0.1", @@ -2935,9 +2801,9 @@ } }, "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": { - "version": "0.34.39", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.39.tgz", - "integrity": "sha512-keEoFsevmLwAedzacnTVmra66GViRH3fhWO1M+nZ8rUgpPJyN4mcvqlGr3QMrQXx4L8KNwW0q9/BeHSEoO4teg==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true }, "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { @@ -2962,9 +2828,9 @@ } }, "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -2977,18 +2843,18 @@ } }, "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", - "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -2997,35 +2863,26 @@ } }, "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-util": "30.0.5" + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -3049,9 +2906,9 @@ } }, "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, "dependencies": { "@jest/schemas": "30.0.5", @@ -3124,27 +2981,25 @@ } }, "node_modules/@jest/pattern": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.0-beta.3.tgz", - "integrity": "sha512-IuB9mweyJI5ToVBRdptKb2w97LGnNHFI+V9/cGaYeFareL7BYD6KiUH022OC51K1841c6YzgYjyQmJHFxELZSQ==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", - "jest-regex-util": "30.0.0-beta.3" + "jest-regex-util": "30.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/pattern/node_modules/jest-regex-util": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.0-beta.3.tgz", - "integrity": "sha512-kiDaZ35ogPivxgLEGJ1jNW2KBtvmPwGlPjy5ASHiVE3kjn3g80galEIcWC0hZV6g5BtTx15VKzSyfOTiKXPnxQ==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, - "license": "MIT", "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/reporters": { @@ -3297,18 +3152,23 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "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/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@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, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -3321,16 +3181,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -3339,11 +3189,10 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "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" @@ -3433,14 +3282,19 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@playwright/test": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz", + "integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==", "dev": true, - "optional": true, + "dependencies": { + "playwright": "1.56.0" + }, + "bin": { + "playwright": "cli.js" + }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@sec-ant/readable-stream": { @@ -3489,9 +3343,9 @@ } }, "node_modules/@stryker-mutator/api": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-9.0.1.tgz", - "integrity": "sha512-XrfDRFzmxVOxzTtUYN7GI2KwD1iu+QXzxF5LmnTeSWJw4IPQSPpwDs5jowT2lwDXiWFcN49yX6JrIEUqLXa28A==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-9.2.0.tgz", + "integrity": "sha512-34dalOkATPR9Qh+cO0VFeway0FlfoDrDD1b9mrw3rIF6pJVEjRHlFHiEmdt1G2DewG9pL5guyPy65kkTQIekpQ==", "dev": true, "dependencies": { "mutation-testing-metrics": "3.5.1", @@ -3504,25 +3358,27 @@ } }, "node_modules/@stryker-mutator/core": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-9.0.1.tgz", - "integrity": "sha512-+XpsJ0JnFIVNdAV8RjaUe1TDRz/5SDiN29aTO5RqiyW2WpYrCtpql7d+O8TvLWe43ua7MPauIKqW3cEGsNMNGQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-9.2.0.tgz", + "integrity": "sha512-qdUSLYAX3ANNZDoFFErLZHGdF6GIk+bmeKuWgD9qdjN9s7TXsjAgPMwUd3U5FwIMTpUF5hZaajTet2IIqyObrA==", "dev": true, "dependencies": { "@inquirer/prompts": "^7.0.0", - "@stryker-mutator/api": "9.0.1", - "@stryker-mutator/instrumenter": "9.0.1", - "@stryker-mutator/util": "9.0.1", + "@stryker-mutator/api": "9.2.0", + "@stryker-mutator/instrumenter": "9.2.0", + "@stryker-mutator/util": "9.2.0", "ajv": "~8.17.1", "chalk": "~5.4.0", - "commander": "~13.1.0", + "commander": "~14.0.0", "diff-match-patch": "1.0.5", "emoji-regex": "~10.4.0", - "execa": "~9.5.0", + "execa": "~9.6.0", "file-url": "~4.0.0", + "json-rpc-2.0": "^1.7.0", "lodash.groupby": "~4.6.0", - "minimatch": "~9.0.5", - "mutation-testing-elements": "3.5.2", + "minimatch": "~10.0.0", + "mutation-server-protocol": "~0.3.0", + "mutation-testing-elements": "3.5.3", "mutation-testing-metrics": "3.5.1", "mutation-testing-report-schema": "3.5.1", "npm-run-path": "~6.0.0", @@ -3542,15 +3398,6 @@ "node": ">=20.0.0" } }, - "node_modules/@stryker-mutator/core/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@stryker-mutator/core/node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -3570,23 +3417,23 @@ "dev": true }, "node_modules/@stryker-mutator/core/node_modules/execa": { - "version": "9.5.3", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.3.tgz", - "integrity": "sha512-QFNnTvU3UjgWFy8Ef9iDHvIdcgZ344ebkwYx4/KLbR+CKQA4xBaHzv+iRpp86QfMHP8faFQLh8iOc57215y4Rg==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", "dev": true, "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", - "human-signals": "^8.0.0", + "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", - "pretty-ms": "^9.0.0", + "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.0.0" + "yoctocolors": "^2.1.1" }, "engines": { "node": "^18.19.0 || >=20.5.0" @@ -3633,15 +3480,15 @@ } }, "node_modules/@stryker-mutator/core/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3688,9 +3535,9 @@ } }, "node_modules/@stryker-mutator/core/node_modules/pretty-ms": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", - "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", "dev": true, "dependencies": { "parse-ms": "^4.0.0" @@ -3748,20 +3595,20 @@ } }, "node_modules/@stryker-mutator/instrumenter": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-9.0.1.tgz", - "integrity": "sha512-ZIIS39w6X4LkYwsTdOneUSIBIY+QFKrmuJdI5LI4XI5FCwOQVN1UnBTFYpaKuKOBznBdRiBUEZXxm5Y42/To+A==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-9.2.0.tgz", + "integrity": "sha512-fX7mYmFMDLG2XwEUBknBo2RHT3HlDYnh7eUd+fEA1wiJH25NY9o3YVFB7UzRk3ZcZJahlgbjExfS+NCUycYwGg==", "dev": true, "dependencies": { - "@babel/core": "~7.27.0", - "@babel/generator": "~7.27.0", - "@babel/parser": "~7.27.0", - "@babel/plugin-proposal-decorators": "~7.27.0", - "@babel/plugin-proposal-explicit-resource-management": "^7.24.7", + "@babel/core": "~7.28.0", + "@babel/generator": "~7.28.0", + "@babel/parser": "~7.28.0", + "@babel/plugin-proposal-decorators": "~7.28.0", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/preset-typescript": "~7.27.0", - "@stryker-mutator/api": "9.0.1", - "@stryker-mutator/util": "9.0.1", - "angular-html-parser": "~9.1.0", + "@stryker-mutator/api": "9.2.0", + "@stryker-mutator/util": "9.2.0", + "angular-html-parser": "~9.2.0", "semver": "~7.7.0", "weapon-regex": "~1.3.2" }, @@ -3770,9 +3617,9 @@ } }, "node_modules/@stryker-mutator/instrumenter/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3782,13 +3629,13 @@ } }, "node_modules/@stryker-mutator/jest-runner": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@stryker-mutator/jest-runner/-/jest-runner-9.0.1.tgz", - "integrity": "sha512-LKTDQpoB/gwXIt1VJjJT9yAl7uuSxZfUSRdzYLEvJQlk/oPVo9AuJXLQGhUoSVLQnbZseiC11dZ3oCVVNTwIiw==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/jest-runner/-/jest-runner-9.2.0.tgz", + "integrity": "sha512-o0SAVKaOMddFm9G8icwacOm3OyLIMMDeLlPqEpf2J+LlEbDyZXyAE36V3b6SlzWcI9kTdZdnhOuHl8vSQe27yA==", "dev": true, "dependencies": { - "@stryker-mutator/api": "9.0.1", - "@stryker-mutator/util": "9.0.1", + "@stryker-mutator/api": "9.2.0", + "@stryker-mutator/util": "9.2.0", "semver": "~7.7.0", "tslib": "~2.8.0" }, @@ -3796,7 +3643,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@stryker-mutator/core": "~9.0.0" + "@stryker-mutator/core": "~9.2.0" } }, "node_modules/@stryker-mutator/jest-runner/node_modules/semver": { @@ -3812,9 +3659,9 @@ } }, "node_modules/@stryker-mutator/util": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-9.0.1.tgz", - "integrity": "sha512-bpE6IMVqpxeSODZK/HH+dKwhfzzE/jc8vX3UgU3ybmBrpQvAthGpSf4lbccUCUMkBp6WQyGqTq25pGhFj3ErWA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-9.2.0.tgz", + "integrity": "sha512-wJuWqXLq/8PKjPUBqHU/N0pshFWPhFA7muVrsNiTVPjHK5DYLkjOrzUl8cUJjusHX35INDr/2uM66kWaYuQu3g==", "dev": true }, "node_modules/@ts-graphviz/adapter": { @@ -4229,8 +4076,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/@vue/compiler-core": { "version": "3.5.18", @@ -4245,21 +4091,6 @@ "source-map-js": "^1.2.1" } }, - "node_modules/@vue/compiler-core/node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.2" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@vue/compiler-core/node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -4299,21 +4130,6 @@ "source-map-js": "^1.2.1" } }, - "node_modules/@vue/compiler-sfc/node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.2" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@vue/compiler-ssr": { "version": "3.5.18", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz", @@ -4330,6 +4146,12 @@ "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", "dev": true }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4407,14 +4229,23 @@ } }, "node_modules/angular-html-parser": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-9.1.1.tgz", - "integrity": "sha512-/xDmnIkfPy7df52scKGGBnZ5Uods64nkf3xBHQSU6uOxwuVVfCFrH+Q/vBZFsc/BY7aJufWtkGjTZrBoyER49w==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-9.2.0.tgz", + "integrity": "sha512-jfnGrA5hguEcvHPrHUsrWOs8jk6SE9cQzFHxt3FPGwzvSEBXLAawReXylh492rzz5km5VgR664EUDMNnmYstSQ==", "dev": true, "engines": { "node": ">= 14" } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -4483,6 +4314,26 @@ "integrity": "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==", "dev": true }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4521,99 +4372,93 @@ } }, "node_modules/babel-jest": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.0-beta.3.tgz", - "integrity": "sha512-h7VooBet0MbW4KuDxLY38sD3VrX6ugyePeA6vKnAx0ncYDRJwGfa/ZFZtl0e4JQ7jyby4qPV9c8BMfsKOR1Big==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/transform": "30.0.0-beta.3", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^7.0.0", - "babel-preset-jest": "30.0.0-beta.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", "slash": "^3.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-0" } }, "node_modules/babel-jest/node_modules/@jest/schemas": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.0-beta.3.tgz", - "integrity": "sha512-tiT79EKOlJGT5v8fYr9UKLSyjlA3Ek+nk0cVZwJGnRqVp26EQSOTYXBCzj0dGMegkgnPTt3f7wP1kGGI8q/e0g==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, - "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-jest/node_modules/@jest/transform": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.0-beta.3.tgz", - "integrity": "sha512-2gixxaYdRh3MQaRsEenHejw0qBIW72DfwG1q9HPLXpnLkm5TKZlTOvOS33S00PGEoa4UG1Iq9tNHh7fxOJAGwQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "30.0.0-beta.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^7.0.0", - "chalk": "^4.0.0", + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "30.0.0-beta.3", - "jest-regex-util": "30.0.0-beta.3", - "jest-util": "30.0.0-beta.3", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", "micromatch": "^4.0.8", - "pirates": "^4.0.4", + "pirates": "^4.0.7", "slash": "^3.0.0", - "write-file-atomic": "^5.0.0" + "write-file-atomic": "^5.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-jest/node_modules/@jest/types": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.0-beta.3.tgz", - "integrity": "sha512-x7GyHD8rxZ4Ygmp4rea3uPDIPZ6Jglcglaav8wQNqXsVUAByapDwLF52Cp3wEYMPMnvH4BicEj56j8fqZx5jng==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.0-beta.3", - "@jest/schemas": "30.0.0-beta.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-jest/node_modules/@sinclair/typebox": { - "version": "0.34.33", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.33.tgz", - "integrity": "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==", - "dev": true, - "license": "MIT" + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true }, "node_modules/babel-jest/node_modules/babel-plugin-istanbul": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", - "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -4626,9 +4471,9 @@ } }, "node_modules/babel-jest/node_modules/ci-info": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", - "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -4636,87 +4481,81 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/babel-jest/node_modules/jest-haste-map": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.0-beta.3.tgz", - "integrity": "sha512-MafsVPIca9E4HR3Fp9gYX+AET4YZmU/VtyLcnRJ9QHdVqHSCzOaElxX30BlyNf5Nw6ZcCafkbB0RGXqSwwsjxw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/types": "30.0.0-beta.3", + "@jest/types": "30.2.0", "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "30.0.0-beta.3", - "jest-util": "30.0.0-beta.3", - "jest-worker": "30.0.0-beta.3", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", "micromatch": "^4.0.8", "walker": "^1.0.8" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "optionalDependencies": { - "fsevents": "^2.3.2" + "fsevents": "^2.3.3" } }, "node_modules/babel-jest/node_modules/jest-regex-util": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.0-beta.3.tgz", - "integrity": "sha512-kiDaZ35ogPivxgLEGJ1jNW2KBtvmPwGlPjy5ASHiVE3kjn3g80galEIcWC0hZV6g5BtTx15VKzSyfOTiKXPnxQ==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, - "license": "MIT", "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-jest/node_modules/jest-util": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.0-beta.3.tgz", - "integrity": "sha512-kob8YNaO1UPrG0TgGdH5l0ciNGuXDX93Yn2b2VCkALuqOXbqzT2xCr6O7dBuwhM7tmzBbpM6CkcK7Qyf/JmLZQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/types": "30.0.0-beta.3", + "@jest/types": "30.2.0", "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^4.0.0", - "graceful-fs": "^4.2.9", - "picomatch": "^4.0.0" + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-jest/node_modules/jest-worker": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.0-beta.3.tgz", - "integrity": "sha512-v17y4Jg9geh3tDm8aU2snuwr8oCJtFefuuPrMRqmC6Ew8K+sLfOcuB3moJ15PHoe4MjTGgsC1oO2PK/GaF1vTg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", - "@ungap/structured-clone": "^1.2.0", - "jest-util": "30.0.0-beta.3", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "supports-color": "^8.1.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-jest/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -4729,7 +4568,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "ISC", "engines": { "node": ">=14" }, @@ -4742,7 +4580,6 @@ "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" }, @@ -4758,7 +4595,6 @@ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, - "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" @@ -4802,29 +4638,25 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.0-beta.3.tgz", - "integrity": "sha512-phSBX46tzCw+6KB9lUuYzyjq16nCRYntYvsDNOx5ZXSGPBcEGbe1mQI+CgdmKUKDD4+o/NDYlvDaQSB3UaSVSw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14" + "@types/babel__core": "^7.20.5" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", - "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.4", + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { @@ -4832,38 +4664,35 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3", - "core-js-compat": "^3.40.0" + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", - "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.4" + "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -4882,24 +4711,23 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { - "version": "30.0.0-beta.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.0-beta.3.tgz", - "integrity": "sha512-2/Oy4J/MxFwNszlwYPO4L7Z+XI7CNCbiz5HZwrsfWnEEDBxJZBJzblfc8TP9lzeiQ4v+Vvem7BMS6B2dVCfzOg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", "dev": true, - "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.0.0-beta.3", - "babel-preset-current-node-syntax": "^1.0.0" + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, "node_modules/babel-walk": { @@ -4941,16 +4769,13 @@ } ] }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "baseline-browser-mapping": "dist/cli.js" } }, "node_modules/bl": { @@ -5024,6 +4849,143 @@ "node": ">=8.12.0" } }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/boxen/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5049,9 +5011,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", - "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -5067,11 +5029,11 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001718", - "electron-to-chromium": "^1.5.160", - "node-releases": "^2.0.19", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { @@ -5180,19 +5142,10 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001721", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", - "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", "dev": true, "funding": [ { @@ -5207,8 +5160,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/chalk": { "version": "4.1.2", @@ -5227,6 +5179,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -5252,42 +5219,6 @@ "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "dev": true }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -5311,6 +5242,18 @@ "dev": true, "license": "MIT" }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5359,6 +5302,23 @@ "node": ">= 12" } }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -5431,12 +5391,12 @@ } }, "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", "dev": true, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/commondir": { @@ -5445,6 +5405,51 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5462,6 +5467,15 @@ "@babel/types": "^7.6.1" } }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5470,13 +5484,12 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", - "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz", + "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==", "dev": true, - "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.26.3" }, "funding": { "type": "opencollective", @@ -5520,18 +5533,6 @@ "node": ">= 8" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -5635,9 +5636,9 @@ } }, "node_modules/dependency-cruiser": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.0.1.tgz", - "integrity": "sha512-4clZ8EPsOVoxGA8NMjaE95aJEO118Cd9D7gT5rysx5azij9cPiCSrnjYlZtV+90PFazlD2lZvjzBHkD1ZqGqlw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.0.2.tgz", + "integrity": "sha512-Aryg/E8ostay8B7OBPqrxcxeGSgtPRKosP6do3L3TiPg4dAvIJFl2EFuG/mO8JAZv70pBTveKvKxhABPyNduvg==", "dev": true, "dependencies": { "acorn": "^8.15.0", @@ -5646,8 +5647,8 @@ "acorn-loose": "^8.5.2", "acorn-walk": "^8.3.4", "ajv": "^8.17.1", - "commander": "^14.0.0", - "enhanced-resolve": "^5.18.2", + "commander": "^14.0.1", + "enhanced-resolve": "^5.18.3", "ignore": "^7.0.5", "interpret": "^3.1.1", "is-installed-globally": "^1.0.0", @@ -5673,15 +5674,6 @@ "node": "^20.12||^22||>=24" } }, - "node_modules/dependency-cruiser/node_modules/commander": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", - "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", - "dev": true, - "engines": { - "node": ">=20" - } - }, "node_modules/dependency-cruiser/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -5885,12 +5877,6 @@ "typescript": "^5.4.4" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, "node_modules/diff-match-patch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", @@ -5907,12 +5893,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, "node_modules/doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -5940,11 +5920,10 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.165", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz", - "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==", - "dev": true, - "license": "ISC" + "version": "1.5.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", + "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "dev": true }, "node_modules/emittery": { "version": "0.13.1", @@ -6082,19 +6061,19 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -6420,9 +6399,9 @@ } }, "node_modules/fast-check": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.2.0.tgz", - "integrity": "sha512-buxrKEaSseOwFjt6K1REcGMeFOrb0wk3cXifeMAG8yahcE9kV20PjQn1OdzPGL6OBFTbYXfjleNBARf/aCfV1A==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.3.0.tgz", + "integrity": "sha512-JVw/DJSxVKl8uhCb7GrwanT9VWsCIdBkK3WpP37B/Au4pyaspriSjtrY2ApbSFwTg3ViPfniT13n75PhzE7VEQ==", "dev": true, "funding": [ { @@ -6670,34 +6649,6 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fs-extra": { "version": "11.3.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", @@ -6902,9 +6853,9 @@ } }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "engines": { "node": ">=18" @@ -7192,18 +7143,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7220,6 +7159,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-expression": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", @@ -7351,6 +7305,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -7433,6 +7399,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7524,21 +7502,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -7795,13 +7758,13 @@ } }, "node_modules/jest-environment-jsdom": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.0.5.tgz", - "integrity": "sha512-BmnDEoAH+jEjkPrvE9DTKS2r3jYSJWlN/r46h0/DBUxKrkgt2jAZ5Nj4wXLAcV1KWkRpcFqA5zri9SWzJZ1cCg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", "dev": true, "dependencies": { - "@jest/environment": "30.0.5", - "@jest/environment-jsdom-abstract": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", "@types/jsdom": "^21.1.7", "@types/node": "*", "jsdom": "^26.1.0" @@ -7819,45 +7782,32 @@ } }, "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", - "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, "dependencies": { - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5" + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", - "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -7876,9 +7826,9 @@ } }, "node_modules/jest-environment-jsdom/node_modules/@jest/types": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", - "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "dependencies": { "@jest/pattern": "30.0.1", @@ -7894,9 +7844,9 @@ } }, "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": { - "version": "0.34.39", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.39.tgz", - "integrity": "sha512-keEoFsevmLwAedzacnTVmra66GViRH3fhWO1M+nZ8rUgpPJyN4mcvqlGr3QMrQXx4L8KNwW0q9/BeHSEoO4teg==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true }, "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { @@ -7921,9 +7871,9 @@ } }, "node_modules/jest-environment-jsdom/node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -7936,18 +7886,18 @@ } }, "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", - "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -7956,35 +7906,26 @@ } }, "node_modules/jest-environment-jsdom/node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-util": "30.0.5" + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/jest-environment-jsdom/node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -8008,9 +7949,9 @@ } }, "node_modules/jest-environment-jsdom/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, "dependencies": { "@jest/schemas": "30.0.5", @@ -8422,6 +8363,8 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, + "optional": true, + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -8564,6 +8507,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-rpc-2.0": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.1.tgz", + "integrity": "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -8653,18 +8602,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8689,8 +8626,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/lodash.groupby": { "version": "4.6.0", @@ -8892,6 +8828,36 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -8942,15 +8908,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/module-definition": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-6.0.1.tgz", @@ -9001,10 +8958,22 @@ "dev": true, "license": "MIT" }, + "node_modules/mutation-server-protocol": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.3.0.tgz", + "integrity": "sha512-pQY+lb80vuD33P1NwhDyCWUgwP2w6JAP5+9Hz3CWM2HpIoYxDkT7OXYKabaunKnoSCgutP3MuruzPCXxLX/lnQ==", + "dev": true, + "dependencies": { + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/mutation-testing-elements": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.5.2.tgz", - "integrity": "sha512-1S6oHiIT3pAYp0mJb8TAyNnaNLHuOJmtDwNEw93bhA0ayjTAPrlNiW8zxivvKD4pjvrZEMUyQCaX+3EBZ4cemw==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.5.3.tgz", + "integrity": "sha512-Vr76a77/mFGsiSAUL+1xFEDb3n5lFs7UJKGWHtaJ+C85kutpBU3QVQ88zobo8Y0dNZPgcMrfThjOzp7W4nmLlQ==", "dev": true }, "node_modules/mutation-testing-metrics": { @@ -9031,17 +9000,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -9067,6 +9025,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9075,11 +9042,10 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true }, "node_modules/node-sarif-builder": { "version": "2.0.3", @@ -9144,9 +9110,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "dev": true }, "node_modules/object-assign": { @@ -9158,15 +9124,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -9179,6 +9136,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9300,12 +9266,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9378,6 +9338,12 @@ "node": ">=0.10.0" } }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -9395,26 +9361,10 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", "dev": true }, "node_modules/picocolors": { @@ -9437,15 +9387,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -9469,6 +9410,50 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz", + "integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==", + "dev": true, + "dependencies": { + "playwright-core": "1.56.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz", + "integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -9506,134 +9491,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, "node_modules/postcss-values-parser": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", @@ -9698,6 +9555,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -9974,6 +9846,15 @@ "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", "dev": true }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -10011,15 +9892,6 @@ "dev": true, "license": "MIT" }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "dependencies": { - "pify": "^2.3.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -10034,18 +9906,6 @@ "node": ">= 6" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -10105,6 +9965,28 @@ "node": ">=4" } }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", @@ -10404,6 +10286,83 @@ "semver": "bin/semver.js" } }, + "node_modules/serve": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz", + "integrity": "sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==", + "dev": true, + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.12.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.8.1", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.6", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "dev": true, + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/serve/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10626,21 +10585,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "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, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -10668,19 +10612,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "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, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -10738,81 +10669,6 @@ "node": ">=18" } }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10846,41 +10702,10 @@ "dev": true }, "node_modules/tailwindcss": { - "version": "3.4.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", - "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", - "dev": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "dev": true }, "node_modules/tapable": { "version": "2.2.2", @@ -10906,27 +10731,6 @@ "node": ">=8" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -11041,12 +10845,6 @@ "node": ">=18" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -11282,6 +11080,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -11312,6 +11120,15 @@ "node": ">=10.12.0" } }, + "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==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -11374,9 +11191,9 @@ } }, "node_modules/weapon-regex": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.3.3.tgz", - "integrity": "sha512-vUIqAGXZT33ZPgIMkDUmDYDpy1SraZ0hoNAIoNpVwBJIzjCQ0irEsKH9Hui+jZEENPB1vOpT/VhXPbpwfnP0xg==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.3.6.tgz", + "integrity": "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==", "dev": true }, "node_modules/webidl-conversions": { @@ -11438,6 +11255,71 @@ "node": ">= 8" } }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/with": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", @@ -11480,24 +11362,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "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, - "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/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -11615,9 +11479,9 @@ } }, "node_modules/yoctocolors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", - "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", "dev": true, "engines": { "node": ">=18" @@ -11637,6 +11501,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index b23e543..5584326 100644 --- a/package.json +++ b/package.json @@ -4,36 +4,46 @@ "description": "MoodCanvas is a client-only interior design assistant that guides users from a single room photo to AI-generated style renders, quick wins, and a curated mini shopping list.", "main": "index.js", "scripts": { - "test": "jest --coverage --config config/jest.config.js", + "test": "npm run format && npm run lint && npm run check:all && npm run test:unit", + "build:css": "tailwindcss -i ./src/tailwind.input.pcss -o ./styles/app.css --minify", + "format": "prettier --write --ignore-unknown --no-error-on-unmatched-pattern \"src\" \"pages\" \"config\" \"docs\" index.html README.md package.json", + "test:unit": "jest --coverage --config config/jest.config.js", "lint": "eslint . --config config/eslint.config.js", "test:watch": "jest --watch --config config/jest.config.js", - "mutation": "stryker run config/stryker.conf.json; node mutation-testing/mutation-report-to-md.js", + "mutation": "stryker run mutation-testing/stryker.conf.json; node mutation-testing/mutation-report-to-md.js", "check:dup": "jscpd --config config/.jscpd.json src", - "check:cycles": "madge --circular --extensions js src", + "check:cycles": "madge --circular --extensions js --exclude '\\.stryker-tmp|playwright-report' src", "check:boundaries": "depcruise -c config/.dependency-cruiser.js src", - "build:css": "tailwindcss -i ./src/tailwind.input.pcss -o ./styles/app.css --minify", - "check:all": "npm run lint && npm run check:dup && npm run check:cycles && npm run check:boundaries", - "validate:all": "npm run test && npm run check:all && npm run mutation" + "check:all": "npm run check:dup && npm run check:cycles && npm run check:boundaries", + "validate:all": "npm run test && npm run mutation", + "serve:static": "serve -l 4173 .", + "test:e2e": "playwright test --config playwright-ui-tests/playwright.config.js", + "test:e2e:artifacts": "PLAYWRIGHT_CAPTURE=1 playwright test --config playwright-ui-tests/playwright.config.js", + "test:e2e:headed": "playwright test --config playwright-ui-tests/playwright.config.js --headed", + "test:e2e:ui": "playwright test --config playwright-ui-tests/playwright.config.js --ui" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { - "@babel/core": "^7.27.4", - "@babel/preset-env": "^7.27.2", - "@eslint/js": "^9.33.0", - "@stryker-mutator/core": "^9.0.1", - "@stryker-mutator/jest-runner": "^9.0.1", - "babel-jest": "^30.0.0-beta.3", - "dependency-cruiser": "^17.0.1", - "eslint": "^9.33.0", + "@babel/core": "^7.28.4", + "@babel/preset-env": "^7.28.3", + "@eslint/js": "^9.37.0", + "@playwright/test": "^1.49.0", + "@stryker-mutator/core": "^9.2.0", + "@stryker-mutator/jest-runner": "^9.2.0", + "babel-jest": "^30.2.0", + "dependency-cruiser": "^17.0.2", + "eslint": "^9.37.0", "eslint-plugin-jest": "^29.0.1", - "fast-check": "^4.2.0", - "globals": "^16.3.0", + "fast-check": "^4.3.0", + "globals": "^16.4.0", "jest": "^29.7.0", - "jest-environment-jsdom": "^30.0.5", + "jest-environment-jsdom": "^30.2.0", "jscpd": "^4.0.5", "madge": "^8.0.0", - "tailwindcss": "^3.4.14" + "tailwindcss": "^4.1.14", + "prettier": "^3.3.3", + "serve": "^14.2.1" } } diff --git a/pages/about.html b/pages/about.html index 7c34d11..fad8b30 100644 --- a/pages/about.html +++ b/pages/about.html @@ -1,4 +1,4 @@ - + @@ -14,46 +14,92 @@
- MoodCanvas + MoodCanvas

About the project

- MoodCanvas is a privacy-forward, client-only interior design assistant. It turns a single empty-room photo into guided inspiration, AI renders, and actionable follow-up steps—all without leaving the browser. + MoodCanvas is a privacy-forward, client-only interior design + assistant. It turns a single empty-room photo into guided + inspiration, AI renders, and actionable follow-up steps—all without + leaving the browser.

-
+

Why client-only?

- Users bring their own Gemini API key (BYOK) and every asset—from the room photo to AI renders—stays in the browser. IndexedDB stores the working project so the session remains private and responsive. + Users bring their own Gemini API key (BYOK) and every asset—from the + room photo to AI renders—stays in the browser. IndexedDB stores the + working project so the session remains private and responsive.

-
+

Design language

    -
  • Plum–Peach dark palette keeps focus on photography and accent cues.
  • -
  • Chat-style timeline mirrors a guided consultation—every edit rewinds dependent steps.
  • -
  • System sans typography for a calm, readable rhythm.
  • +
  • + Plum–Peach dark palette + keeps focus on photography and accent cues. +
  • +
  • + Chat-style timeline + mirrors a guided consultation—every edit rewinds dependent steps. +
  • +
  • + System sans typography + for a calm, readable rhythm. +
-
+

Core capabilities

-
+

Room analysis

-

Strict JSON schema ensures every Gemini response includes palette, constraints, and top-ten styles.

+

+ Strict JSON schema ensures every Gemini response includes + palette, constraints, and top-ten styles. +

-
+

Render pipeline

-

Gallery, A/B variants, and hero renders run with pooled concurrency for smooth progress.

+

+ Gallery, A/B variants, and hero renders run with pooled + concurrency for smooth progress. +

-
+

Actionable follow-up

-

Quick wins, Smart Mixed palette, and a neutral mini shopping list conclude every session.

+

+ Quick wins, Smart Mixed palette, and a neutral mini shopping + list conclude every session. +

-
+

Local persistence

-

IndexedDB keeps projects capped at 150 MB each, with automatic cleanup of older renders.

+

+ IndexedDB keeps projects capped at 150 MB each, with + automatic cleanup of older renders. +

diff --git a/pages/features.html b/pages/features.html index 42a416e..8572be7 100644 --- a/pages/features.html +++ b/pages/features.html @@ -1,4 +1,4 @@ - + @@ -14,46 +14,98 @@
- MoodCanvas + MoodCanvas

Feature overview

- A guided, linear workflow that takes you from one empty-room photo to 10 inspiration renders, Smart Mixed variants, and a neutral actionable plan. + A guided, linear workflow that takes you from one empty-room photo + to 10 inspiration renders, Smart Mixed variants, and a neutral + actionable plan.

-
+

Timeline stages

    -
  1. Upload & key capture — camera-ready input with EXIF orientation, BYOK stored only in localStorage.
  2. -
  3. Room intelligence — strict JSON analysis surfaces palette, quick wins, and Smart Mixed axes.
  4. -
  5. Gallery & favorites — 10 style renders ordered by fit score with backtrack-safe selection.
  6. -
  7. A/B mini-variants — up to three favorites explored along Smart Mixed axes.
  8. -
  9. Hero finale — three high-resolution Smart Mixed renders, palette recap, and mini shopping list.
  10. +
  11. + Upload & key capture — + camera-ready input with EXIF orientation, BYOK stored only in + localStorage. +
  12. +
  13. + Room intelligence — + strict JSON analysis surfaces palette, quick wins, and Smart Mixed + axes. +
  14. +
  15. + Gallery & favorites — + 10 style renders ordered by fit score with backtrack-safe + selection. +
  16. +
  17. + A/B mini-variants — up + to three favorites explored along Smart Mixed axes. +
  18. +
  19. + Hero finale — three + high-resolution Smart Mixed renders, palette recap, and mini + shopping list. +
-
+

Under the hood

-
+

Gemini integration

-

`gemini-2.5-flash` for JSON analysis, `gemini-2.5-flash-image` for gallery, A/B, and hero renders with pooled concurrency (5 at a time).

+

+ `gemini-2.5-flash` for JSON analysis, `gemini-2.5-flash-image` + for gallery, A/B, and hero renders with pooled concurrency (5 at + a time). +

-
+

Storage model

-

IndexedDB hybrid stores projects, timeline events, media blobs, and structured artifacts with automatic cap enforcement.

+

+ IndexedDB hybrid stores projects, timeline events, media blobs, + and structured artifacts with automatic cap enforcement. +

-
+

Resilience

-

Timeouts (45s/120s), retry hooks, and per-card error states keep the flow recoverable without reloading.

+

+ Timeouts (45s/120s), retry hooks, and per-card error states keep + the flow recoverable without reloading. +

-
+

Privacy banner

-

No backend storage, no telemetry. Everything lives in the browser; removing the key wipes credentials instantly.

+

+ No backend storage, no telemetry. Everything lives in the + browser; removing the key wipes credentials instantly. +

-
+

What’s next

  • Export kit (PDF/ZIP) for sharing design outcomes.
  • diff --git a/playwright-ui-tests/index.spec.js b/playwright-ui-tests/index.spec.js new file mode 100644 index 0000000..fe7d751 --- /dev/null +++ b/playwright-ui-tests/index.spec.js @@ -0,0 +1,100 @@ +import { test, expect } from '@playwright/test'; + +test.describe('index.html smoke test', () => { + test('loads without console warnings or errors', async ({ page }) => { + const consoleIssues = []; + const pageErrors = []; + const allowedWarningPatterns = [ + /cdn\.tailwindcss\.com should not be used in production/i, + ]; + + page.on('console', (message) => { + const type = message.type(); + const text = message.text(); + + if (type === 'error') { + consoleIssues.push({ type, text }); + } else if (type === 'warning') { + const isAllowed = allowedWarningPatterns.some((pattern) => + pattern.test(text) + ); + + if (!isAllowed) { + consoleIssues.push({ type, text }); + } + } + }); + + page.on('pageerror', (error) => { + pageErrors.push(error.message); + }); + + const response = await page.goto('/'); + + await expect(response, 'Expected to receive a valid response when loading /').not.toBeNull(); + + if (response) { + expect.soft( + response.status(), + `Expected a successful status code for index.html but received ${response.status()} ${response.statusText()}` + ).toBeLessThan(400); + } + + // Using 'load' avoids timeouts seen when parallel browsers wait for 'networkidle'. + await page.waitForLoadState('load'); + + const heroHeading = page.getByRole('heading', { + level: 1, + name: /iterative interior canvas/i, + }); + await expect(heroHeading).toBeVisible(); + + expect(consoleIssues, 'Console warning/error detected while loading index.html').toEqual([]); + expect(pageErrors, 'Unhandled page errors detected while loading index.html').toEqual([]); + }); + + test('first-run layout hides advanced controls until iterations exist', async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('load'); + + await page.waitForFunction(() => window.moodCanvas !== undefined); + + await page.evaluate(() => { + const app = window.moodCanvas; + app.state.main = null; + app.state.roomBrief = { + paragraph: 'Calm bedroom retreat with layered neutrals.', + bullets: ['Keep envelope intact', 'Lean on warm lighting'], + }; + app.state.generatingRoomBrief = false; + app.state.rounds = []; + app.state.selectedForNext.clear(); + app.state.roundGuidance = ''; + app.render(); + }); + + const sectionHeadings = await page.$$eval('#timeline > section', (sections) => + sections.map((section) => section.querySelector('h2')?.textContent?.trim() ?? '') + ); + + const briefIndex = sectionHeadings.findIndex((text) => + text.toLowerCase().includes('silent room brief') + ); + const startIndex = sectionHeadings.findIndex((text) => + text.toLowerCase().includes('ready to') + ); + + expect.soft(briefIndex, 'Silent room brief card should render').toBeGreaterThan(-1); + expect(startIndex, 'Start section should render after the brief card').toBe(briefIndex + 1); + + const startButton = page.getByRole('button', { + name: /start generating first suggestions/i, + }); + await expect(startButton).toBeVisible(); + await expect(startButton).toBeDisabled(); + + await expect(page.locator('#generateBar')).toBeHidden(); + await expect(page.locator('#final-modal')).toHaveCount(0); + await expect(page.locator('#final-backdrop')).toHaveCount(0); + }); +}); diff --git a/playwright-ui-tests/playwright.config.js b/playwright-ui-tests/playwright.config.js new file mode 100644 index 0000000..6affe10 --- /dev/null +++ b/playwright-ui-tests/playwright.config.js @@ -0,0 +1,43 @@ +// @ts-check +import { defineConfig, devices } from '@playwright/test'; + +/** + * Playwright configuration focused on a single smoke test that assures the + * static `index.html` page renders without console errors. + */ +const captureArtifacts = process.env.PLAYWRIGHT_CAPTURE === '1'; + +module.exports = defineConfig({ + testDir: './', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : 3, + reporter: process.env.CI + ? [ + ['github'], + ['json', { outputFile: '../test-results.json' }], + ['junit', { outputFile: '../junit.xml' }], + ] + : [ + ['list'], + ['html', { open: 'never' }], + ], + use: { + baseURL: 'http://127.0.0.1:4173', + trace: captureArtifacts ? 'on' : 'on-first-retry', + screenshot: captureArtifacts ? 'on' : 'only-on-failure', + video: captureArtifacts ? 'on' : 'retain-on-failure', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } }, + ], + webServer: { + command: 'npm run serve:static', + url: 'http://127.0.0.1:4173', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}); diff --git a/src/assets/defaultData.json b/src/assets/defaultData.json index 8b6d0a3..f73fa57 100644 --- a/src/assets/defaultData.json +++ b/src/assets/defaultData.json @@ -1,9 +1,9 @@ { - "_comment": "PLACEHOLDER DATA FILE — KEEP OR REMOVE. This is sample/placeholder data used for tests and demos. Once real data structure is defined, remove this file OR rename it and rewrite its contents completely to reflect real data. - Remove if not needed. - Or: Rename + fully reimplement before production use.", - "someData": [ - { - "id": "123", - "title": "Abc" - } - ] -} \ No newline at end of file + "_comment": "PLACEHOLDER DATA FILE — KEEP OR REMOVE. This is sample/placeholder data used for tests and demos. Once real data structure is defined, remove this file OR rename it and rewrite its contents completely to reflect real data. - Remove if not needed. - Or: Rename + fully reimplement before production use.", + "someData": [ + { + "id": "123", + "title": "Abc" + } + ] +} diff --git a/src/components/app.js b/src/components/app.js index 447618d..5f3ad91 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -6,51 +6,63 @@ import { onKeyChange, } from '../utils/key-store.js'; import { GeminiClient } from '../utils/gemini-client.js'; -import { normalizeImageFile, blobToBase64, blobToDataUrl, base64ToBlob } from '../utils/image.js'; import { - ensureProject, - getProject, - saveMedia, - saveArtifact, - appendEvent, -} from '../utils/project-store.js'; -import { SUPPORTED_ROOM_FUNCTIONS } from '../constants/analysis-schema.js'; + normalizeImageFile, + blobToBase64, + blobToDataUrl, + base64ToBlob, +} from '../utils/image.js'; +import { IterationApi, DEFAULT_SAMPLING } from '../utils/iteration-api.js'; import { - buildAnalysisPrompt, - buildABPrompts, - buildHeroPrompts, - buildMiniList, - normalizeRenderGallery, -} from '../utils/prompts.js'; -import { ensurePaletteOptions } from '../utils/palette.js'; + runIterationBatch, + buildFinalizationPrompt, + diversityDeck, + buildIterationPrompt, +} from '../utils/iteration-prompts.js'; +import { ensureProject, getProject } from '../utils/project-store.js'; const STORAGE_PROJECT_KEY = 'moodcanvas.currentProjectId'; -const MAX_FAVORITES = 3; +const MAX_REFERENCES = 2; + +function randomId(prefix) { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return `${prefix}-${crypto.randomUUID()}`; + } + return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`; +} export class MoodCanvasApp { constructor(root) { this.root = root; this.client = new GeminiClient(); + this.iterationApi = new IterationApi({ client: this.client }); this.state = { - loading: false, apiKey: '', project: null, - photo: null, - analysis: null, - gallery: [], - favorites: new Set(), - abVariants: [], - heroRenders: [], - quickWins: null, - miniList: null, - palette: null, - paletteOptions: [], - selectedPaletteIndex: null, - warning: null, + main: null, + references: [], + purpose: '', + roomBrief: null, + roomBriefPurpose: '', + generatingRoomBrief: false, + rounds: [], + selectedForNext: new Set(), + roundGuidance: '', + sampling: { ...DEFAULT_SAMPLING }, + generatingRound: false, + generatingFinal: false, + final: { imageUrl: '', notes: '' }, + candidate: null, error: null, }; this.unsubscribeKey = null; - this.controller = null; + this.draggedReferenceIndex = null; + this.isFinalKeyListenerAttached = false; + this.handleEscapeKeydown = (event) => { + if (event.key === 'Escape') { + this.closeFinalModal(); + } + }; } async init() { @@ -65,6 +77,25 @@ export class MoodCanvasApp { this.unsubscribeKey(); this.unsubscribeKey = null; } + if (this.isFinalKeyListenerAttached) { + document.removeEventListener('keydown', this.handleEscapeKeydown); + this.isFinalKeyListenerAttached = false; + } + if (this.finalBackdrop) { + this.finalBackdrop.remove(); + this.finalBackdrop = null; + } + if (this.finalModal) { + this.finalModal.remove(); + this.finalModal = null; + } + this.finalClose = null; + this.finalCurrent = null; + this.finalCandidate = null; + this.finalCurrentNotes = null; + this.finalCandidateNotes = null; + this.finalKeep = null; + this.finalReplace = null; } renderShell() { @@ -74,9 +105,9 @@ export class MoodCanvasApp {

    MoodCanvas

    -

    Interior Design Assistant

    +

    Iterative Interior Canvas

    - Upload a single empty-room photo, explore tailored styles, and collect actionable next steps without leaving your browser. + Upload one empty-room photo, explore iterative style variants, and finalize a polished build — all in-browser with your Gemini key.

    @@ -88,9 +119,21 @@ export class MoodCanvasApp {
    -
    +
    +
    @@ -119,6 +162,10 @@ export class MoodCanvasApp { this.settingsModal = this.root.querySelector('#settingsModal'); this.keyInput = this.root.querySelector('#keyInput'); this.clearKeyBtn = this.root.querySelector('#clearKeyBtn'); + this.generateBar = this.root.querySelector('#generateBar'); + this.guidanceInput = this.root.querySelector('#roundGuidanceInput'); + this.generateBtn = this.root.querySelector('#generateNextBtn'); + this.selectedCounter = this.root.querySelector('#selectedCounter'); this.settingsBtn.addEventListener('click', () => { if (this.settingsModal.open) return; @@ -130,25 +177,35 @@ export class MoodCanvasApp { this.keyInput.value = this.state.apiKey; }); - this.settingsModal.querySelector('form').addEventListener('submit', (event) => { - event.preventDefault(); - const formData = new FormData(event.currentTarget); - const key = formData.get('geminiKey'); - saveGeminiKey(key ?? ''); - this.settingsModal.close(); - }); + this.settingsModal + .querySelector('form') + .addEventListener('submit', (event) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const key = formData.get('geminiKey'); + saveGeminiKey(key ?? ''); + this.settingsModal.close(); + }); this.clearKeyBtn.addEventListener('click', () => { clearGeminiKey(); this.keyInput.value = ''; }); - } + this.generateBtn.addEventListener('click', () => { + if (this.state.generatingRound) return; + this.handleGenerateNext().catch((error) => this.reportError(error)); + }); + + this.guidanceInput.addEventListener('input', (event) => { + this.state.roundGuidance = event.target.value; + }); + } async bootstrapProject() { const existingId = localStorage.getItem(STORAGE_PROJECT_KEY); let project = await getProject(existingId); if (!project) { - project = await ensureProject({ name: 'Room Session' }); + project = await ensureProject({ name: 'Iteration Session' }); localStorage.setItem(STORAGE_PROJECT_KEY, project.id); } this.state.project = project; @@ -163,13 +220,14 @@ export class MoodCanvasApp { this.state.apiKey = next; this.client.setApiKey(next); this.renderKeyStatus(); - this.render(); }); } render() { this.renderKeyStatus(); this.renderTimeline(); + this.renderGenerateBar(); + this.renderFinalModal(); } renderKeyStatus() { @@ -186,739 +244,973 @@ export class MoodCanvasApp { renderTimeline() { this.timelineEl.innerHTML = ''; this.timelineEl.appendChild(this.renderUploadCard()); - if (!this.state.apiKey) { - this.timelineEl.appendChild(this.renderKeyBanner()); - } - if (this.state.photo) { - this.timelineEl.appendChild(this.renderPhotoCard()); - this.timelineEl.appendChild(this.renderAnalysisCard()); - } - if (this.state.analysis) { - this.timelineEl.appendChild(this.renderAnalysisResults()); - if (this.state.palette) { - this.timelineEl.appendChild(this.renderGalleryCard()); - } + if (this.state.generatingRoomBrief || this.state.roomBrief) { + this.timelineEl.appendChild(this.renderRoomBriefCard()); } - if (this.state.abVariants.length > 0) { - this.timelineEl.appendChild(this.renderABCard()); - } - if (this.state.heroRenders.length > 0) { - this.timelineEl.appendChild(this.renderHeroCard()); - } - if (this.state.quickWins) { - this.timelineEl.appendChild(this.renderInsightsCard()); + this.timelineEl.appendChild(this.renderStartSection()); + this.state.rounds.forEach((round) => { + this.timelineEl.appendChild(this.renderRound(round)); + }); + if (this.state.final.imageUrl) { + this.timelineEl.appendChild(this.renderFinalCard()); } } renderUploadCard() { + const { main, references, purpose } = this.state; const card = document.createElement('section'); - card.className = 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 shadow-inner shadow-black/10 flex flex-col gap-4'; + card.className = + 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-5'; + card.innerHTML = `
    -

    1. Upload your room photo

    -

    Use a single JPG or PNG. MoodCanvas stores the image locally and resizes a private thumbnail.

    +

    1. Upload your base room & references

    +

    Use a single JPG/PNG captured from your device. Long edge ≤ 2048px, metadata preserved. Optional: up to two style reference images (drag to reorder).

    - ${this.state.photo ? `` : ''}
    - +
    +
    + +
    + ${main ? `Uploaded room` : 'No photo yet'} +
    +
    +
    +
    + Reference images (optional, max 2) + drag to reorder +
    + +
    + ${references + .map( + (ref, index) => ` +
    + Reference ${index + 1} +
    +

    Reference ${index + 1}

    +

    ${ref.width}×${ref.height}

    +
    + +
    + ` + ) + .join('')} + ${references.length === 0 ? '

    No references selected.

    ' : ''} +
    +
    +
    +
    + +
    `; - const input = card.querySelector('input[type="file"]'); - input.addEventListener('change', (event) => { + const mainInput = card.querySelector('input[type="file"]'); + mainInput.addEventListener('change', (event) => { const file = event.target.files?.[0]; if (file) { - this.importPhoto(file).catch((error) => this.reportError(error)); + this.importMainPhoto(file).catch((error) => this.reportError(error)); } }); - const replaceBtn = card.querySelector('#replacePhoto'); - if (replaceBtn) { - replaceBtn.addEventListener('click', () => { - this.resetAfter('photo'); - }); - } - - return card; - } - - renderKeyBanner() { - const banner = document.createElement('section'); - banner.className = 'rounded-xl2 border border-raspberry/50 bg-raspberry/15 p-5 text-sm text-raspberry flex flex-col gap-3'; - banner.innerHTML = ` -
    Bring your own Google Gemini key
    -

    Get a free API key from Google AI Studio. The key is stored only in your browser.

    - - `; - banner.querySelector('#openSettings').addEventListener('click', () => this.settingsModal.showModal()); - return banner; - } - - renderPhotoCard() { - const { photo } = this.state; - const card = document.createElement('section'); - card.className = 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-4'; - card.innerHTML = ` -

    2. Confirm scope & intended function

    -
    -
    - Uploaded room -
    - - - - - - ${this.state.warning ? `

    ${this.state.warning}

    ` : ''} - -
    - `; + const refInput = card.querySelectorAll('input[type="file"]')[1]; + refInput.addEventListener('change', (event) => { + const file = event.target.files?.[0]; + if (file) { + this.importReference(file).catch((error) => this.reportError(error)); + } + event.target.value = ''; + }); - const form = card.querySelector('#scopeForm'); - form.addEventListener('submit', (event) => { + const referenceList = card.querySelector('#referenceList'); + referenceList.addEventListener('dragstart', (event) => { + const target = event.target.closest('[draggable="true"]'); + if (!target) return; + this.draggedReferenceIndex = Number.parseInt(target.dataset.index, 10); + event.dataTransfer.effectAllowed = 'move'; + }); + referenceList.addEventListener('dragover', (event) => { + const target = event.target.closest('[draggable="true"]'); + if (!target) return; event.preventDefault(); - if (!this.state.apiKey) { - this.settingsModal.showModal(); - return; + target.classList.add('border-peach'); + }); + referenceList.addEventListener('dragleave', (event) => { + const target = event.target.closest('[draggable="true"]'); + if (target) target.classList.remove('border-peach'); + }); + referenceList.addEventListener('drop', (event) => { + const target = event.target.closest('[draggable="true"]'); + if (!target) return; + event.preventDefault(); + target.classList.remove('border-peach'); + const toIndex = Number.parseInt(target.dataset.index, 10); + if (Number.isInteger(this.draggedReferenceIndex)) { + this.reorderReferences(this.draggedReferenceIndex, toIndex); } - const data = new FormData(form); - this.runAnalysis({ - intendedUse: data.get('function'), - scope: Number.parseInt(data.get('scope'), 10) || 1, - notes: data.get('notes')?.toString() ?? '', - }).catch((error) => this.reportError(error)); + this.draggedReferenceIndex = null; + }); + referenceList.addEventListener('dragend', () => { + this.draggedReferenceIndex = null; + }); + referenceList.addEventListener('click', (event) => { + const target = event.target.closest('button[data-remove]'); + if (!target) return; + const index = Number.parseInt(target.dataset.remove, 10); + this.removeReference(index); }); - return card; - } + const purposeInput = card.querySelector('#purposeInput'); + purposeInput.addEventListener('input', (event) => { + this.state.purpose = event.target.value; + }); + purposeInput.addEventListener('change', () => { + if (this.state.main) { + this.generateRoomBrief(true).catch((error) => this.reportError(error)); + } + }); - renderAnalysisCard() { - const card = document.createElement('section'); - card.className = 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-4'; - card.innerHTML = ` -

    3. Analysis progress

    -

    Gemini evaluates the room envelope, palette, and style fit. Strict JSON schema keeps the response predictable.

    - ${this.state.loading && !this.state.analysis ? '
    Analyzing… (120s timeout)
    ' : ''} - ${this.state.error && !this.state.analysis ? `

    ${this.state.error}

    ` : ''} - `; return card; } - renderAnalysisResults() { - const { analysis } = this.state; - const paletteOptions = (this.state.paletteOptions ?? []).slice(0, 5); + renderRoomBriefCard() { const card = document.createElement('section'); - card.className = 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-6'; - - const paletteStatus = this.state.palette - ? 'Selected' - : 'Awaiting pick'; - - card.innerHTML = ` -
    -

    4. Room intelligence

    -

    Highlights from the analysis including inspiration pulse, palette exploration, and quick wins.

    -
    -
    -
    -

    Inspiration pulse

    -
    - ${analysis.usage_candidates.slice(0, 3).map((item) => `${item.function} · ${(item.confidence * 100).toFixed(0)}%`).join('')} -
    -
    -
    -
    -
    -

    60-30-10 palette

    -

    Pick one of the suggested palettes to continue.

    -
    - ${paletteStatus} -
    -
    - ${!this.state.palette ? '

    Select a palette to unlock the 10-style gallery.

    ' : ''} -
    -
    -
    -

    Top 5 quick wins

    -
      - ${(this.state.analysis.quick_wins ?? []).map((win) => `
    1. ${win.title} — ${win.description}${win.impact ? ` (Impact: ${win.impact})` : ''}
    2. `).join('')} -
    -
    - `; - - const paletteGrid = card.querySelector('#paletteGrid'); - if (paletteGrid) { - if (paletteOptions.length === 0) { - const empty = document.createElement('p'); - empty.className = 'text-xs text-plum-muted'; - empty.textContent = 'No palette suggestions available. You can still proceed once a palette is selected later.'; - paletteGrid.appendChild(empty); - } else { - paletteOptions.forEach((option, index) => { - paletteGrid.appendChild(this.renderPaletteOption(option, index)); - }); - } + card.className = + 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-4'; + const loading = this.state.generatingRoomBrief; + const brief = this.state.roomBrief; + + const heading = document.createElement('h2'); + heading.className = 'text-lg font-semibold'; + heading.textContent = '2. Silent room brief'; + card.appendChild(heading); + + const description = document.createElement('p'); + description.className = 'text-sm text-plum-muted'; + description.textContent = + 'One-time guidance distilled into a paragraph with 3–5 constraints. Automatically regenerates when purpose changes.'; + card.appendChild(description); + + if (loading) { + const loadingLabel = document.createElement('div'); + loadingLabel.className = 'animate-pulse text-sm text-plum-muted'; + loadingLabel.textContent = 'Generating room brief…'; + card.appendChild(loadingLabel); } - return card; - } - - renderPaletteSwatch(label, info = {}) { - const hex = typeof info.hex === 'string' ? info.hex : '#2E1B2D'; - const name = typeof info.name === 'string' && info.name.trim().length > 0 ? info.name : '—'; - const finish = typeof info.finish === 'string' ? info.finish : ''; - return ` -
    -
    - -
    -
    - ${label.toUpperCase()} - ${name} - ${hex} - ${finish ? `${finish}` : ''} -
    -
    - `; - } + if (brief) { + const editor = document.createElement('div'); + editor.className = 'flex flex-col gap-4'; + + const paragraphField = document.createElement('label'); + paragraphField.className = 'flex flex-col gap-2 text-sm'; + paragraphField.setAttribute('for', 'roomBriefParagraph'); + paragraphField.innerHTML = 'Summary paragraph'; + + const paragraphInput = document.createElement('textarea'); + paragraphInput.id = 'roomBriefParagraph'; + paragraphInput.rows = 4; + paragraphInput.value = brief.paragraph || ''; + paragraphInput.disabled = loading; + paragraphInput.className = + 'rounded-xl2 border border-plum-border/60 bg-plum-surface px-3 py-2 text-sm text-plum-text focus:outline-none focus:ring-2 focus:ring-peach/70'; + paragraphInput.addEventListener('input', (event) => { + if (!this.state.roomBrief) return; + this.state.roomBrief.paragraph = event.target.value; + }); + paragraphField.appendChild(paragraphInput); + editor.appendChild(paragraphField); + + const bulletWrapper = document.createElement('div'); + bulletWrapper.className = 'flex flex-col gap-2'; + + const bulletLabel = document.createElement('span'); + bulletLabel.className = 'text-sm'; + bulletLabel.textContent = 'Constraints (edit or clear as needed)'; + bulletWrapper.appendChild(bulletLabel); + + const bullets = Array.isArray(brief.bullets) ? brief.bullets : []; + bullets.forEach((item, index) => { + const bulletRow = document.createElement('div'); + bulletRow.className = 'flex items-start gap-2'; + + const bulletInput = document.createElement('input'); + bulletInput.type = 'text'; + bulletInput.value = item || ''; + bulletInput.disabled = loading; + bulletInput.dataset.testid = 'room-brief-constraint'; + bulletInput.className = + 'flex-1 rounded-xl2 border border-plum-border/60 bg-plum-surface px-3 py-2 text-sm text-plum-text focus:outline-none focus:ring-2 focus:ring-peach/70'; + bulletInput.addEventListener('input', (event) => { + if (!this.state.roomBrief?.bullets) return; + this.state.roomBrief.bullets[index] = event.target.value; + }); + bulletRow.appendChild(bulletInput); + + if (bullets.length > 1) { + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.className = + 'rounded-full border border-plum-border/40 px-3 py-1 text-xs text-plum-muted hover:border-peach hover:text-peach'; + removeBtn.textContent = 'Remove'; + removeBtn.disabled = loading; + removeBtn.addEventListener('click', () => { + if (!this.state.roomBrief?.bullets) return; + this.state.roomBrief.bullets = this.state.roomBrief.bullets.filter( + (_, bulletIdx) => bulletIdx !== index + ); + this.renderTimeline(); + }); + bulletRow.appendChild(removeBtn); + } - renderPaletteOption(palette, index) { - const isSelected = this.state.selectedPaletteIndex === index; - const button = document.createElement('button'); - button.type = 'button'; - button.className = `group flex flex-col gap-4 rounded-xl2 border px-4 py-4 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-peach/80 ${ - isSelected ? 'border-peach/70 bg-peach/10 shadow-inner shadow-peach/20' : 'border-plum-border/30 bg-plum-surface hover:border-peach/60 hover:bg-plum-surface-2' - }`; - - const statusBadge = isSelected - ? 'Selected' - : index === 0 - ? 'Recommended' - : ''; - - button.innerHTML = ` -
    - Palette ${index + 1} - ${statusBadge} -
    -
    - ${['primary', 'secondary', 'accent'].map((key) => this.renderPaletteSwatch(key, palette?.[key] ?? {})).join('')} -
    - `; + bulletWrapper.appendChild(bulletRow); + }); - button.addEventListener('click', () => this.selectPalette(index)); - return button; - } + const addBtn = document.createElement('button'); + addBtn.type = 'button'; + addBtn.className = + 'self-start text-xs font-medium text-peach hover:text-peach/70'; + addBtn.textContent = 'Add constraint'; + addBtn.disabled = loading || bullets.length >= 5; + addBtn.addEventListener('click', () => { + if (!this.state.roomBrief) return; + if (!Array.isArray(this.state.roomBrief.bullets)) { + this.state.roomBrief.bullets = []; + } + if (this.state.roomBrief.bullets.length >= 5) return; + this.state.roomBrief.bullets = [...this.state.roomBrief.bullets, '']; + this.renderTimeline(); + }); + bulletWrapper.appendChild(addBtn); - selectPalette(index) { - const options = this.state.paletteOptions ?? []; - const palette = options[index]; - if (!palette) { - return; + editor.appendChild(bulletWrapper); + card.appendChild(editor); } - this.state.selectedPaletteIndex = index; - this.state.palette = palette; - if (this.state.analysis) { - const updatedAnalysis = { - ...this.state.analysis, - palette_60_30_10: palette, - }; - updatedAnalysis.render_gallery = normalizeRenderGallery(updatedAnalysis, palette); - this.state.analysis = updatedAnalysis; - } - this.render(); + return card; } - renderGalleryCard() { - const card = document.createElement('section'); - card.className = 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-5'; - const gallery = this.state.gallery; - card.innerHTML = ` -
    -

    5. 10-style gallery

    -

    Generate renders in parallel (pool=5). Tap to mark favorites for A/B exploration.

    + renderStartSection() { + const section = document.createElement('section'); + section.className = + 'rounded-xl2 border border-dashed border-plum-border/40 bg-plum-surface p-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between'; + const isRestart = this.state.rounds.length > 0; + const startLabel = isRestart + ? 'Restart (clear generated images)' + : 'Start generating first suggestions'; + const disabled = !this.state.main || this.state.generatingRound; + const descriptor = isRestart + ? 'Clear previous iterations and begin again with the current brief.' + : 'Kick off the first gallery once the silent brief looks right.'; + + section.innerHTML = ` +
    +

    ${ + isRestart ? 'Ready to restart?' : 'Ready to begin?' + }

    +

    ${descriptor}

    -
    -
    - -
    Favorites: ${this.state.favorites.size}/${MAX_FAVORITES}
    +
    +
    `; - const grid = card.querySelector('#galleryGrid'); - const prompts = this.state.analysis.render_gallery; - const combined = prompts.map((prompt) => ({ prompt, render: gallery.find((item) => item.style === prompt.style) ?? null })); - combined - .sort((a, b) => this.findStyleScore(b.prompt.style) - this.findStyleScore(a.prompt.style)) - .forEach(({ prompt, render }) => { - const tile = document.createElement('button'); - tile.type = 'button'; - tile.className = `group relative flex flex-col overflow-hidden rounded-xl2 border ${this.state.favorites.has(prompt.style) ? 'border-peach/80' : 'border-plum-border/30'} bg-plum-surface text-left transition`; - tile.dataset.style = prompt.style; - tile.innerHTML = ` -
    - ${render ? `${prompt.style} render` : 'Pending'} - ${prompt.style} -
    -
    -

    ${prompt.focus}

    -

    ${prompt.guidance}

    -
    - `; - tile.addEventListener('click', () => this.toggleFavorite(prompt.style)); - grid.appendChild(tile); - }); - - card.querySelector('#generateGallery').addEventListener('click', () => { + const startBtn = section.querySelector('#startRoundBtn'); + startBtn.addEventListener('click', () => { + if (this.state.rounds.length > 0) { + this.handleRestart(); + return; + } if (!this.state.apiKey) { this.settingsModal.showModal(); return; } - this.generateGalleryRenders().catch((error) => this.reportError(error)); + if (!this.state.main) { + alert('Please upload a main room photo first.'); + return; + } + this.startFirstIteration().catch((error) => this.reportError(error)); }); - if (this.state.favorites.size > 0) { - const confirm = document.createElement('div'); - confirm.className = 'flex flex-wrap items-center justify-between gap-3 rounded-xl2 border border-plum-border/40 bg-plum-surface p-4 text-sm'; - confirm.innerHTML = ` -
    Generate A/B mini-variants for ${this.state.favorites.size} favorite styles.
    - - `; - confirm.querySelector('button').addEventListener('click', () => this.generateABVariants()); - card.appendChild(confirm); - } - - return card; + return section; } - renderABCard() { + renderRound(round) { const card = document.createElement('section'); - card.className = 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-5'; + card.className = + 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-5'; + const title = + round.index === 0 + ? 'First iteration — 12 style variants' + : `Iteration ${round.index + 1}`; card.innerHTML = ` -
    -

    6. A/B mini-variants

    -

    Smart Mixed axes provide controlled variation for each favorite style.

    +
    +
    +

    ${title}

    + ${round.completed ? '' : 'Running (pool=6)…'} +
    +

    Tiles appear as they finish. Select favorites to unlock the next round.

    +
    +
    + ${round.tiles.map((tile) => this.renderTileMarkup(round, tile)).join('')}
    -
    - `; - const grid = card.querySelector('#abGrid'); - this.state.abVariants.forEach((variant) => { - const tile = document.createElement('article'); - tile.className = 'rounded-xl2 border border-plum-border/40 bg-plum-surface overflow-hidden flex flex-col'; - tile.innerHTML = ` -
    - ${variant.thumbUrl ? `${variant.style} variant` : '
    Pending
    '} -
    -
    -
    ${variant.style} · ${variant.variant}
    -
    ${variant.focus}
    -
    - `; - grid.appendChild(tile); + card.querySelectorAll('[data-tile-id]').forEach((element) => { + const tileId = element.getAttribute('data-tile-id'); + element + .querySelector('[data-action="toggle"]') + ?.addEventListener('click', () => { + this.toggleFavorite(round.id, tileId); + }); + const descriptionNode = element.querySelector('[data-action="details"]'); + const detailsNode = element.querySelector('[data-role="details"]'); + if (descriptionNode && detailsNode) { + const tile = this.findTile(tileId); + const { full, short } = this.prepareTileDescription(tile?.text); + descriptionNode.dataset.full = full; + descriptionNode.dataset.short = short; + descriptionNode.dataset.expanded = 'false'; + descriptionNode.textContent = short; + descriptionNode.title = 'Click to toggle details'; + descriptionNode.addEventListener('click', () => { + const expanded = descriptionNode.dataset.expanded === 'true'; + if (expanded) { + descriptionNode.dataset.expanded = 'false'; + descriptionNode.textContent = descriptionNode.dataset.short; + detailsNode.classList.add('hidden'); + } else { + descriptionNode.dataset.expanded = 'true'; + descriptionNode.textContent = descriptionNode.dataset.full; + detailsNode.classList.remove('hidden'); + } + }); + } + element + .querySelector('[data-action="open"]') + ?.addEventListener('click', () => { + const tile = this.findTile(tileId); + if (tile?.imageUrl) { + window.open(tile.imageUrl, '_blank', 'noopener'); + } + }); + element + .querySelector('[data-action="retry"]') + ?.addEventListener('click', () => { + this.retryTile(round.id, tileId).catch((error) => + this.reportError(error) + ); + }); + element + .querySelector('[data-action="finalize"]') + ?.addEventListener('click', () => { + this.handleFinalize(tileId).catch((error) => this.reportError(error)); + }); + const noteArea = element.querySelector('textarea[data-note]'); + if (noteArea) { + noteArea.addEventListener('input', (event) => { + this.updateTileNote(tileId, event.target.value); + }); + } }); - card.querySelector('#generateHeroes').addEventListener('click', () => this.generateHeroRenders()); return card; } + renderTileMarkup(round, tile) { + const baseClasses = [ + 'rounded-xl2', + 'border', + 'border-plum-border/40', + 'bg-plum-surface', + 'p-4', + 'flex', + 'flex-col', + 'gap-3', + 'text-sm', + ]; + if (tile.fav) { + baseClasses.push('border-peach', 'shadow-lg', 'shadow-peach/10'); + } + if (tile.status === 'error') { + baseClasses.push('border-raspberry/50'); + } + const { short: shortDescription } = this.prepareTileDescription(tile.text); + const warnings = tile.warnings?.length + ? `
    ${tile.warnings.join('; ')}
    ` + : ''; + let body = ''; + if (tile.status === 'pending') { + body = + '
    Awaiting generation…
    '; + } else if (tile.status === 'error') { + body = ` +
    +

    ${tile.error || 'Generation failed.'}

    + +
    `; + } else { + body = ` +
    + Iteration render + +
    + ${warnings} +

    ${shortDescription}

    + +
    + + +
    `; + } - renderHeroCard() { - const card = document.createElement('section'); - card.className = 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-5'; - card.innerHTML = ` -
    -

    7. Hero renders

    -

    Three flagship Smart Mixed renders at 1536×1024. Tap to open full size in a new tab.

    -
    -
    + return ` +
    +
    + Tile ${tile.index + 1} + ${tile.hint} +
    + ${body} +
    `; - - const grid = card.querySelector('#heroGrid'); - this.state.heroRenders.forEach((render) => { - const tile = document.createElement('a'); - tile.href = render.fullUrl; - tile.target = '_blank'; - tile.rel = 'noopener'; - tile.className = 'group relative block overflow-hidden rounded-xl2 border border-plum-border/40 bg-plum-surface'; - tile.innerHTML = ` - Hero render - ${render.variant} - `; - grid.appendChild(tile); - }); - - return card; } - renderInsightsCard() { - const card = document.createElement('section'); - card.className = 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-5'; - const quickWins = Array.isArray(this.state.quickWins) ? this.state.quickWins : []; - const list = Array.isArray(this.state.miniList) ? this.state.miniList : []; + renderFinalCard() { + const card = document.createElement('section'); + card.className = + 'rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 flex flex-col gap-4'; card.innerHTML = ` -
    -

    8. Quick wins & mini shopping list

    -

    Actionable instructions with neutral specifications only.

    -
    -
    -
    -

    Top 5 quick wins

    -
      - ${quickWins.map((item) => `
    1. ${item.title} — ${item.description}
    2. `).join('')} -
    -
    -
    -

    Mini shopping list

    -
      - ${list.length > 0 - ? list - .map( - (item) => - `
    1. ${item.name} — ${item.spec}
    2. ` - ) - .join('') - : '
    3. No items yet—generate hero renders to unlock shopping ideas.
    4. '} -
    -
    +

    Final selection

    +

    Latest hi-res render with build notes.

    +
    + Final render +
    ${this.state.final.notes || '(No build notes yet)'}
    `; return card; } - async importPhoto(file) { - this.state.loading = true; - this.render(); - const normalized = await normalizeImageFile(file); - const base64 = await blobToBase64(normalized.original); - const thumbUrl = URL.createObjectURL(normalized.thumb); - const fullUrl = URL.createObjectURL(normalized.original); - const projectId = this.state.project.id; - - const originalRecord = await saveMedia(projectId, { - kind: 'input', - blob: normalized.original, - mime: file.type, - width: normalized.width, - height: normalized.height, - bytes: normalized.original.size, - }); + renderGenerateBar() { + if (!this.generateBar) return; + const hasSelection = this.state.selectedForNext.size > 0; + const hasRounds = this.state.rounds.length > 0; + const allowBar = hasSelection && hasRounds; + if (allowBar) { + this.generateBar.classList.remove('hidden'); + this.generateBar.removeAttribute('hidden'); + } else { + this.generateBar.classList.add('hidden'); + this.generateBar.setAttribute('hidden', ''); + } + if (this.guidanceInput) { + this.guidanceInput.value = this.state.roundGuidance; + this.guidanceInput.disabled = !hasRounds || this.state.generatingRound; + } + if (this.generateBtn) { + this.generateBtn.disabled = !allowBar || this.state.generatingRound; + } + if (this.selectedCounter) { + this.selectedCounter.textContent = `${this.state.selectedForNext.size} favorite${ + this.state.selectedForNext.size === 1 ? '' : 's' + } selected`; + } + } - await saveMedia(projectId, { - kind: 'thumb', - relatedId: originalRecord.id, - blob: normalized.thumb, - mime: file.type, - width: normalized.width, - height: normalized.height, - bytes: normalized.thumb.size, - }); + renderFinalModal() { + if (!this.finalModal) return; + if (this.state.candidate) { + this.finalCandidate.src = this.state.candidate.imageUrl; + this.finalCandidateNotes.textContent = + this.state.candidate.notes || '(No build notes)'; + } + if (this.state.final.imageUrl) { + this.finalCurrent.src = this.state.final.imageUrl; + this.finalCurrentNotes.textContent = + this.state.final.notes || '(No build notes yet)'; + } + } - await appendEvent(projectId, { - type: 'upload_image', - payload: { mediaId: originalRecord.id, name: file.name }, - }); + prepareTileDescription(text) { + const fallback = '(Descriptor pending)'; + if (!text || !text.trim()) { + return { full: fallback, short: fallback }; + } + const normalized = text.trim().replace(/\s+/g, ' '); + const words = normalized.split(' '); + if (words.length <= 12) { + return { full: normalized, short: normalized }; + } + return { + full: normalized, + short: `${words.slice(0, 12).join(' ')}…`, + }; + } - this.state.photo = { + async importMainPhoto(file) { + const normalized = await normalizeImageFile(file); + const base64 = await blobToBase64(normalized.resized); + const displayUrl = await blobToDataUrl(normalized.display); + this.state.main = { + id: randomId('main'), + original: normalized.original, + resized: normalized.resized, + display: normalized.display, + displayUrl, base64, - thumbUrl, - fullUrl, + mime: file.type || 'image/jpeg', width: normalized.width, height: normalized.height, - fileName: file.name, }; - this.resetAfter('analysis'); - this.state.loading = false; + this.state.roomBrief = null; + this.state.roomBriefPurpose = ''; this.render(); } - resetAfter(step) { - if (step === 'photo') { - this.state.photo = null; - } - this.state.analysis = null; - this.state.gallery = []; - this.state.favorites = new Set(); - this.state.abVariants = []; - this.state.heroRenders = []; - this.state.quickWins = null; - this.state.miniList = null; - this.state.palette = null; - this.state.paletteOptions = []; - this.state.selectedPaletteIndex = null; + async importReference(file) { + if (this.state.references.length >= MAX_REFERENCES) { + alert( + 'Only two reference images are supported. Remove one before adding another.' + ); + return; + } + const normalized = await normalizeImageFile(file); + const base64 = await blobToBase64(normalized.resized); + const displayUrl = await blobToDataUrl(normalized.display); + this.state.references = [ + ...this.state.references, + { + id: randomId('ref'), + base64, + mime: file.type || 'image/jpeg', + displayUrl, + width: normalized.width, + height: normalized.height, + }, + ]; + this.render(); + } + + reorderReferences(from, to) { + if (from === to) return; + const next = [...this.state.references]; + const [item] = next.splice(from, 1); + next.splice(to, 0, item); + this.state.references = next; this.render(); } - async runAnalysis({ intendedUse, scope, notes }) { - this.state.loading = true; - this.state.error = null; + removeReference(index) { + this.state.references = this.state.references.filter( + (_, idx) => idx !== index + ); + this.render(); + } + + async startFirstIteration() { + await this.generateRoomBrief(false); + await this.runIteration([]); + } + + async generateRoomBrief(force) { + if (!this.state.main) return; + if ( + !force && + this.state.roomBrief && + this.state.roomBriefPurpose === this.state.purpose + ) { + return; + } + this.state.generatingRoomBrief = true; this.render(); try { - const prompt = buildAnalysisPrompt({ intendedUse, scope, notes }); - const controller = new AbortController(); - this.controller = controller; - const data = await this.client.analyzeRoom({ - imageBase64: this.state.photo.base64, - prompt, - signal: controller.signal, + const promptText = this.buildRoomBriefPrompt(); + const result = await this.iterationApi.generateRoomBrief({ + promptText, + base64Main: this.state.main.base64, + mimeMain: this.state.main.mime, }); - const normalizedAnalysis = { ...data }; - const paletteOptions = ensurePaletteOptions(normalizedAnalysis); - const defaultPalette = paletteOptions[0] ?? normalizedAnalysis.palette_60_30_10 ?? null; - if (defaultPalette) { - normalizedAnalysis.palette_60_30_10 = defaultPalette; - } - normalizedAnalysis.palette_options = paletteOptions; - normalizedAnalysis.render_gallery = normalizeRenderGallery(normalizedAnalysis, defaultPalette); - await saveArtifact(this.state.project.id, { - kind: 'analysis', - json: normalizedAnalysis, - }); - await appendEvent(this.state.project.id, { - type: 'analysis_done', - payload: { intendedUse, scope }, - }); - this.state.analysis = normalizedAnalysis; - this.state.quickWins = normalizedAnalysis.quick_wins; - this.state.paletteOptions = paletteOptions; - this.state.palette = null; - this.state.selectedPaletteIndex = null; - this.state.warning = evaluateScaleConfidence(normalizedAnalysis.constraints?.scale_guesses); - } catch (error) { - if (error.code === 'missing-key') { - this.settingsModal.showModal(); - } - this.reportError(error); + this.state.roomBrief = result; + this.state.roomBriefPurpose = this.state.purpose; } finally { - this.state.loading = false; + this.state.generatingRoomBrief = false; this.render(); } } - async generateGalleryRenders() { - this.state.loading = true; - this.render(); - const analysis = this.state.analysis; - const poolSize = 5; - const tasks = analysis.render_gallery.map((item) => async () => { - const assets = await this.renderAndStore({ - prompt: item.prompt, - relatedId: item.style, - }); - const entry = { - style: item.style, - prompt: item.prompt, - focus: item.focus, - thumbUrl: assets.thumbUrl, - fullUrl: assets.fullUrl, - }; - return entry; - }); + buildRoomBriefPrompt() { + const purpose = this.state.purpose || 'None'; + const notes = this.state.roundGuidance?.trim() + ? this.state.roundGuidance.trim() + : 'None'; + return [ + 'ROLE: Create a concise, practical “room brief” for interior styling iterations.', + '', + 'INPUTS:', + '- Base room photo (camera pose and architectural envelope must be preserved)', + `- Intended use: ${purpose}`, + `- Notes from user (optional): ${notes}`, + '', + 'OUTPUT FORMAT (plain text only, no headings, no markdown):', + '1) One paragraph (~60–90 words) summarizing the room’s light, openings, proportions and key opportunities/risks, with the intended use as priority.', + '2) Then 3–5 bullets (one line each) of strict constraints/guidelines.', + '', + 'CONSTRAINT HINTS (pick only what’s relevant; do not list all):', + '• Keep camera pose & envelope; no structural changes unless explicitly asked later.', + '• 60–30–10 color planning (dominant–secondary–accent).', + '• Circulation clearance ~0.9 m in main paths; avoid cluttered layouts.', + '• Sofa–table reach ≈0.35–0.45 m OR desk depth ≥0.60 m depending on use.', + '• Rug sizing: front legs of seating on rug; workspace rug fully under chair.', + '• Curtains: rod 10–15 cm above head; total width ≈2× opening.', + '• Layered lighting: ambient + task + accent; evening ~2700–3000 K.', + ].join('\n'); + } + toggleFavorite(roundId, tileId) { + const tile = this.findTile(tileId); + if (!tile || tile.status !== 'ok') return; + tile.fav = !tile.fav; + if (tile.fav) { + this.state.selectedForNext.add(tile.id); + } else { + this.state.selectedForNext.delete(tile.id); + } + this.renderGenerateBar(); + this.renderTimeline(); + } - const results = []; - const queue = tasks.slice(); - const workers = Array.from({ length: poolSize }, async () => { - while (queue.length > 0) { - const job = queue.shift(); - try { - const result = await job(); - results.push(result); - this.state.gallery = [...results]; - this.render(); - } catch (error) { - console.error('Render failed', error); - } - } - }); + updateTileNote(tileId, value) { + const tile = this.findTile(tileId); + if (!tile) return; + tile.note = value; + } - await Promise.all(workers); - await appendEvent(this.state.project.id, { - type: 'gallery_generated', - payload: { count: results.length }, - }); - this.state.loading = false; - this.render(); + async handleGenerateNext() { + if (this.state.selectedForNext.size === 0) return; + const favorites = [...this.state.selectedForNext] + .map((id) => this.findTile(id)) + .filter(Boolean); + await this.runIteration(favorites); } - async generateABVariants() { - if (this.state.favorites.size === 0) return; - this.state.loading = true; - this.render(); - const variants = []; - const axes = this.state.analysis.smart_mixed_axes; - for (const style of this.state.favorites) { - const basePrompt = this.state.analysis.render_gallery.find((item) => item.style === style); - if (!basePrompt) continue; - const prompts = buildABPrompts(basePrompt.prompt, axes); - for (let index = 0; index < prompts.length; index += 1) { - try { - const assets = await this.renderAndStore({ - prompt: prompts[index], - relatedId: `${style}-${index}`, - }); - variants.push({ - style, - variant: index === 0 ? axes.axisA.label : axes.axisB.label, - focus: index === 0 ? axes.axisA.description : axes.axisB.description, - thumbUrl: assets.thumbUrl, - fullUrl: assets.fullUrl, - }); - } catch (error) { - console.error('Variant render failed', error); + buildFavoritesTextBlock(favorites) { + if (!favorites.length) return ''; + return favorites + .map((tile) => { + const lines = [tile.text || '']; + if (tile.note?.trim()) { + lines.push(`User note: ${tile.note.trim()}`); } + return lines.filter(Boolean).join(' '); + }) + .filter(Boolean) + .join('\n'); + } + + gatherReferencesForApi() { + return this.state.references.map((ref) => ({ + mime: ref.mime, + base64: ref.base64, + })); + } + + async runIteration(favorites) { + if (!this.state.main) return; + this.state.generatingRound = true; + this.state.selectedForNext.clear(); + this.state.rounds.forEach((existingRound) => { + existingRound.tiles.forEach((tile) => { + tile.fav = false; + }); + }); + this.renderGenerateBar(); + this.renderTimeline(); + const roundIndex = this.state.rounds.length; + const hints = diversityDeck(); + const round = { + id: randomId('round'), + index: roundIndex, + hints, + tiles: hints.map((hint, idx) => ({ + id: randomId(`tile-${roundIndex}-${idx}`), + index: idx, + status: 'pending', + fav: false, + note: '', + hint, + text: '', + warnings: [], + })), + completed: false, + context: { + roomBrief: this.state.roomBrief, + purpose: this.state.purpose, + favoritesTextBlock: this.buildFavoritesTextBlock(favorites), + roundGuidance: this.state.roundGuidance, + }, + }; + this.state.rounds = [...this.state.rounds, round]; + this.renderTimeline(); + try { + await runIterationBatch({ + api: this.iterationApi, + base64Main: this.state.main.base64, + mimeMain: this.state.main.mime, + references: this.gatherReferencesForApi(), + roomBrief: + (this.state.roomBrief?.paragraph || '') + + (this.state.roomBrief?.bullets?.length + ? '\n' + + this.state.roomBrief.bullets.map((item) => `• ${item}`).join('\n') + : ''), + purpose: this.state.purpose, + roundGuidance: this.state.roundGuidance, + favoritesTextBlock: this.buildFavoritesTextBlock(favorites), + sampling: this.state.sampling, + onProgress: ({ index, ok, result, error }) => { + const target = round.tiles[index]; + if (!target) return; + if (ok) { + target.status = 'ok'; + target.imageUrl = result.imageUrl; + target.text = result.text; + target.warnings = result.warnings || []; + target.error = ''; + } else { + target.status = 'error'; + target.error = error?.message || String(error); + } + this.renderTimeline(); + }, + }); + round.completed = true; + } finally { + this.state.generatingRound = false; + this.state.selectedForNext.clear(); + this.state.roundGuidance = ''; + if (this.guidanceInput) { + this.guidanceInput.value = ''; } + this.render(); } - this.state.abVariants = variants; - await appendEvent(this.state.project.id, { - type: 'ab_generated', - payload: { count: variants.length }, + } + + async retryTile(roundId, tileId) { + const round = this.state.rounds.find((r) => r.id === roundId); + if (!round) return; + const tileIndex = round.tiles.findIndex((tile) => tile.id === tileId); + if (tileIndex === -1) return; + const tile = round.tiles[tileIndex]; + tile.status = 'pending'; + tile.error = ''; + this.renderTimeline(); + const promptText = buildIterationPrompt({ + roomBrief: + (round.context.roomBrief?.paragraph || '') + + (round.context.roomBrief?.bullets?.length + ? '\n' + + round.context.roomBrief.bullets + .map((item) => `• ${item}`) + .join('\n') + : ''), + purpose: round.context.purpose, + roundGuidance: round.context.roundGuidance, + favoritesTextBlock: round.context.favoritesTextBlock, + diversityHint: tile.hint, }); - this.state.loading = false; - this.render(); + try { + const result = await this.iterationApi.generateVariant({ + promptText, + base64Main: this.state.main.base64, + mimeMain: this.state.main.mime, + references: this.gatherReferencesForApi(), + sampling: this.state.sampling, + }); + tile.status = 'ok'; + tile.imageUrl = result.imageUrl; + tile.text = result.text; + tile.warnings = result.warnings || []; + } catch (error) { + tile.status = 'error'; + tile.error = error?.message || String(error); + } + this.renderTimeline(); } - async generateHeroRenders() { - this.state.loading = true; - this.render(); - const axes = this.state.analysis.smart_mixed_axes; - const basePrompt = this.state.analysis.render_gallery[0].prompt; - const prompts = buildHeroPrompts(basePrompt, axes); - const results = []; - for (let index = 0; index < prompts.length; index += 1) { - try { - const assets = await this.renderAndStore({ - prompt: prompts[index], - relatedId: `hero-${index}`, - }); - results.push({ - variant: index === 0 ? 'Axis A' : index === 1 ? 'Axis B' : 'Blend', - thumbUrl: assets.thumbUrl, - fullUrl: assets.fullUrl, - }); - } catch (error) { - console.error('Hero render failed', error); - } + findTile(tileId) { + for (const round of this.state.rounds) { + const match = round.tiles.find((tile) => tile.id === tileId); + if (match) return match; } - this.state.heroRenders = results; - this.state.quickWins = this.state.analysis.quick_wins; - this.state.miniList = buildMiniList(this.state.analysis); - await appendEvent(this.state.project.id, { - type: 'hero_generated', - payload: { count: results.length }, - }); - this.state.loading = false; - this.render(); + return null; } - toggleFavorite(style) { - if (this.state.favorites.has(style)) { - this.state.favorites.delete(style); - } else { - if (this.state.favorites.size >= MAX_FAVORITES) { - return; - } - this.state.favorites.add(style); + async handleFinalize(tileId) { + if (this.state.generatingFinal) return; + const tile = this.findTile(tileId); + if (!tile || tile.status !== 'ok') return; + if (!this.state.main) return; + this.state.generatingFinal = true; + this.renderGenerateBar(); + try { + const base64Child = await this.fetchBase64FromUrl(tile.imageUrl); + const childBlob = base64ToBlob(base64Child, 'image/jpeg'); + const promptText = buildFinalizationPrompt(); + const response = await this.iterationApi.finalizeHiRes({ + promptText, + base64Main: this.state.main.base64, + mimeMain: this.state.main.mime, + base64Child, + mimeChild: childBlob.type || 'image/jpeg', + references: this.gatherReferencesForApi(), + sampling: this.state.sampling, + }); + this.state.candidate = { + imageUrl: response.imageUrl, + notes: response.text || '', + }; + this.showFinalModal(); + } catch (error) { + alert(`Finalization failed: ${error?.message || error}`); + } finally { + this.state.generatingFinal = false; + this.renderGenerateBar(); } - this.render(); } - async persistRenderAssets({ blob, thumb, relatedId }) { - const renderRecord = await saveMedia(this.state.project.id, { - kind: 'render', - relatedId, - blob, - mime: blob.type, - bytes: blob.size, - }); - await saveMedia(this.state.project.id, { - kind: 'thumb', - relatedId: renderRecord.id, - blob: thumb, - mime: 'image/jpeg', - bytes: thumb.size, - }); - return renderRecord; + async fetchBase64FromUrl(url) { + const response = await fetch(url); + const blob = await response.blob(); + return blobToBase64(blob); } - async renderAndStore({ prompt, relatedId }) { - const [result] = await this.client.generateRenders({ - prompt, - imageBase64: this.state.photo.base64, + ensureFinalModal() { + if (this.finalModal && this.finalBackdrop) { + return; + } + + this.finalBackdrop = document.createElement('div'); + this.finalBackdrop.id = 'final-backdrop'; + this.finalBackdrop.className = 'fixed inset-0 hidden z-50 bg-black/70'; + document.body.appendChild(this.finalBackdrop); + + this.finalModal = document.createElement('section'); + this.finalModal.id = 'final-modal'; + this.finalModal.className = + 'fixed inset-x-4 top-1/2 hidden z-50 -translate-y-1/2 rounded-xl2 border border-plum-border/40 bg-plum-surface-2 p-6 shadow-xl'; + this.finalModal.innerHTML = ` +
    +
    +

    Compare final render

    +

    Review the hi-res candidate against the current final build notes.

    +
    + +
    +
    +
    +

    Current final

    + Current final +

    +
    +
    +

    Candidate

    + Candidate final +

    +
    +
    +
    + + +
    + `; + document.body.appendChild(this.finalModal); + + this.finalClose = this.finalModal.querySelector('#final-close'); + this.finalCurrent = this.finalModal.querySelector('#final-current'); + this.finalCandidate = this.finalModal.querySelector('#final-candidate'); + this.finalCurrentNotes = this.finalModal.querySelector( + '#final-current-notes' + ); + this.finalCandidateNotes = this.finalModal.querySelector( + '#final-candidate-notes' + ); + this.finalKeep = this.finalModal.querySelector('#final-keep'); + this.finalReplace = this.finalModal.querySelector('#final-replace'); + + this.finalClose.addEventListener('click', () => this.closeFinalModal()); + this.finalBackdrop.addEventListener('click', () => this.closeFinalModal()); + this.finalKeep.addEventListener('click', () => this.closeFinalModal()); + this.finalReplace.addEventListener('click', () => { + if (this.state.candidate) { + this.state.final = { + imageUrl: this.state.candidate.imageUrl, + notes: this.state.candidate.notes, + }; + this.state.candidate = null; + this.closeFinalModal(); + this.render(); + } }); - const blob = base64ToBlob(result.data, result.mimeType); - const thumb = await createThumb(blob); - await this.persistRenderAssets({ blob, thumb, relatedId }); - return { - fullUrl: URL.createObjectURL(blob), - thumbUrl: URL.createObjectURL(thumb), - }; } - findStyleScore(style) { - return ( - this.state.analysis?.styles_top10.find((item) => item.style === style)?.score ?? 0 - ); + showFinalModal() { + this.ensureFinalModal(); + this.finalBackdrop.classList.remove('hidden'); + this.finalModal.classList.remove('hidden'); + if (!this.isFinalKeyListenerAttached) { + document.addEventListener('keydown', this.handleEscapeKeydown); + this.isFinalKeyListenerAttached = true; + } + this.renderFinalModal(); } - reportError(error) { - console.error(error); - this.state.error = error.message ?? 'Unexpected error'; - this.state.loading = false; - this.render(); + closeFinalModal() { + if (this.finalBackdrop) { + this.finalBackdrop.classList.add('hidden'); + } + if (this.finalModal) { + this.finalModal.classList.add('hidden'); + } + if (this.isFinalKeyListenerAttached) { + document.removeEventListener('keydown', this.handleEscapeKeydown); + this.isFinalKeyListenerAttached = false; + } } -} -async function createThumb(blob) { - const canvas = document.createElement('canvas'); - const img = document.createElement('img'); - img.src = await blobToDataUrl(blob); - await new Promise((resolve, reject) => { - img.onload = resolve; - img.onerror = reject; - }); - const scale = Math.min(384 / img.naturalWidth, 384 / img.naturalHeight, 1); - canvas.width = Math.round(img.naturalWidth * scale); - canvas.height = Math.round(img.naturalHeight * scale); - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - return new Promise((resolve, reject) => { - canvas.toBlob((result) => { - if (result) { - resolve(result); - } else { - reject(new Error('Unable to create thumbnail')); - } - }, 'image/jpeg', 0.85); - }); -} + handleRestart() { + if ( + !confirm( + 'Clear all generated images and selections. Keep base photo, purpose, references, room brief and settings?' + ) + ) { + return; + } + this.state.rounds = []; + this.state.selectedForNext.clear(); + this.state.roundGuidance = ''; + this.state.final = { imageUrl: '', notes: '' }; + this.state.candidate = null; + this.render(); + } -function evaluateScaleConfidence(guesses) { - if (!guesses) return null; - const confidences = [guesses.width_m, guesses.depth_m, guesses.height_m] - .map((item) => item?.confidence) - .filter((value) => typeof value === 'number'); - if (confidences.length === 0) return null; - const min = Math.min(...confidences); - if (Number.isFinite(min) && min < 0.4) { - return 'Scale confidence is low; measurements may need manual verification.'; - } - return null; + reportError(error) { + console.error(error); + alert(error?.message || String(error)); + } } - diff --git a/src/components/app.layout.test.js b/src/components/app.layout.test.js new file mode 100644 index 0000000..be52e94 --- /dev/null +++ b/src/components/app.layout.test.js @@ -0,0 +1,88 @@ +import { MoodCanvasApp } from './app.js'; + +describe('MoodCanvasApp layout scaffolding', () => { + let root; + + beforeEach(() => { + document.body.innerHTML = '
    '; + root = document.getElementById('app-root'); + global.fetch = jest.fn(); + }); + + afterEach(() => { + delete global.fetch; + // Clean up any stray modal/backdrop nodes created during tests. + document + .querySelectorAll('#final-backdrop, #final-modal') + .forEach((node) => node.remove()); + }); + + test('start button sits after the silent room brief card', () => { + const app = new MoodCanvasApp(root); + app.renderShell(); + app.state.main = { id: 'main-photo' }; + app.state.roomBrief = { paragraph: 'Sample brief', bullets: ['alpha'] }; + app.renderTimeline(); + + const sections = Array.from(root.querySelectorAll('#timeline > section')); + const briefIndex = sections.findIndex((section) => { + const heading = section.querySelector('h2'); + return heading?.textContent?.includes('Silent room brief'); + }); + const startIndex = sections.findIndex((section) => + section.querySelector('#startRoundBtn') + ); + + expect(briefIndex).toBeGreaterThan(-1); + expect(startIndex).toBe(briefIndex + 1); + + const paragraphField = root.querySelector('#roomBriefParagraph'); + expect(paragraphField).not.toBeNull(); + expect(paragraphField.value).toBe('Sample brief'); + + const constraintInputs = root.querySelectorAll( + '[data-testid="room-brief-constraint"]' + ); + expect(constraintInputs).toHaveLength(1); + expect(constraintInputs[0].value).toBe('alpha'); + + app.destroy(); + }); + + test('final comparison modal is created lazily', () => { + const app = new MoodCanvasApp(root); + app.renderShell(); + + expect(document.querySelector('#final-modal')).toBeNull(); + expect(document.querySelector('#final-backdrop')).toBeNull(); + + app.state.final = { imageUrl: 'current.jpg', notes: 'Keep' }; + app.state.candidate = { imageUrl: 'candidate.jpg', notes: 'Swap' }; + app.showFinalModal(); + + expect(document.querySelector('#final-modal')).not.toBeNull(); + expect(document.querySelector('#final-backdrop')).not.toBeNull(); + + app.closeFinalModal(); + app.destroy(); + }); + + test('generate bar toggles the hidden attribute with selection state', () => { + const app = new MoodCanvasApp(root); + app.renderShell(); + + app.state.rounds = []; + app.state.selectedForNext.clear(); + app.renderGenerateBar(); + + expect(app.generateBar.hasAttribute('hidden')).toBe(true); + + app.state.rounds.push({ id: 'round-1', tiles: [] }); + app.state.selectedForNext.add('tile-1'); + app.renderGenerateBar(); + + expect(app.generateBar.hasAttribute('hidden')).toBe(false); + + app.destroy(); + }); +}); diff --git a/src/constants/analysis-schema.test.js b/src/constants/analysis-schema.test.js index 8c5bea3..6697044 100644 --- a/src/constants/analysis-schema.test.js +++ b/src/constants/analysis-schema.test.js @@ -1,6 +1,11 @@ import { ANALYSIS_SCHEMA } from './analysis-schema.js'; -const FORBIDDEN_KEYS = new Set(['$schema', '$id', 'additionalProperties', 'anyOf']); +const FORBIDDEN_KEYS = new Set([ + '$schema', + '$id', + 'additionalProperties', + 'anyOf', +]); function collectForbiddenKeys(node, path = []) { const hits = []; @@ -28,7 +33,8 @@ describe('ANALYSIS_SCHEMA', () => { }); it('allows nullable scale guesses without numeric bounds', () => { - const scaleGuesses = ANALYSIS_SCHEMA.properties.constraints.properties.scale_guesses; + const scaleGuesses = + ANALYSIS_SCHEMA.properties.constraints.properties.scale_guesses; const sampleAxis = scaleGuesses.properties.width_m; expect(sampleAxis.properties.value.type).toBe('number'); expect(sampleAxis.properties.value.nullable).toBe(true); diff --git a/src/utils/gemini-client.js b/src/utils/gemini-client.js index 8f8de88..1545ace 100644 --- a/src/utils/gemini-client.js +++ b/src/utils/gemini-client.js @@ -29,7 +29,7 @@ export class GeminiClient { throw new Error('imageBase64 is required for analysis'); } - const response = await this.#request({ + const json = await this.generateContent({ model: ANALYSIS_MODEL, body: { contents: [ @@ -54,8 +54,6 @@ export class GeminiClient { timeoutMs, signal, }); - - const json = await response.json(); return parseAnalysisPayload(json); } @@ -74,7 +72,7 @@ export class GeminiClient { } const jobs = Array.from({ length: count }, (_, index) => - this.#request({ + this.generateContent({ model: RENDER_MODEL, timeoutMs, signal, @@ -94,18 +92,35 @@ export class GeminiClient { }, ], }, - }) - .then((response) => response.json()) - .then((payload) => parseRenderPayload(payload)) + }).then((payload) => parseRenderPayload(payload)) ); return Promise.all(jobs); } + async generateContent({ + model, + body, + timeoutMs = DEFAULT_RENDER_TIMEOUT_MS, + signal, + }) { + if (!model) { + throw new Error('model is required'); + } + if (!body) { + throw new Error('body is required'); + } + const response = await this.#request({ model, body, timeoutMs, signal }); + return response.json(); + } + async #request({ model, body, timeoutMs, signal }) { this.#ensureKey(); const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(new Error('Request timed out')), timeoutMs); + const timeoutId = setTimeout( + () => controller.abort(new Error('Request timed out')), + timeoutMs + ); const mergedSignal = signal ? mergeAbortSignals(signal, controller.signal) @@ -122,14 +137,17 @@ export class GeminiClient { signal: mergedSignal, }; - const response = await Reflect.apply( - this.fetchImpl, - globalThis, - [`${API_ROOT}/${model}`, requestOptions] - ); + const response = await Reflect.apply(this.fetchImpl, globalThis, [ + `${API_ROOT}/${model}`, + requestOptions, + ]); if (!response.ok) { - throw buildGeminiError(await safeJson(response), 'Gemini request failed', response.status); + throw buildGeminiError( + await safeJson(response), + 'Gemini request failed', + response.status + ); } return response; @@ -140,7 +158,9 @@ export class GeminiClient { #ensureKey() { if (!this.apiKey) { - throw Object.assign(new Error('Gemini API key missing'), { code: 'missing-key' }); + throw Object.assign(new Error('Gemini API key missing'), { + code: 'missing-key', + }); } } } @@ -149,7 +169,8 @@ function mergeAbortSignals(a, b) { if (!a) return b; if (!b) return a; const controller = new AbortController(); - const abort = (event) => controller.abort(event?.target?.reason ?? a.reason ?? b.reason); + const abort = (event) => + controller.abort(event?.target?.reason ?? a.reason ?? b.reason); a.addEventListener('abort', abort); b.addEventListener('abort', abort); return controller.signal; diff --git a/src/utils/gemini-client.test.js b/src/utils/gemini-client.test.js index 6054397..23515ac 100644 --- a/src/utils/gemini-client.test.js +++ b/src/utils/gemini-client.test.js @@ -38,9 +38,11 @@ describe('GeminiClient', () => { json: jest.fn().mockResolvedValue(mockPayload), }; - const fetchImpl = jest.fn(function (url) { + const fetchImpl = jest.fn(function (url) { capturedThis = this; - expect(url).toContain('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'); + expect(url).toContain( + 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent' + ); return Promise.resolve(fetchResponse); }); diff --git a/src/utils/image.js b/src/utils/image.js index 040fe08..8d1aa7d 100644 --- a/src/utils/image.js +++ b/src/utils/image.js @@ -1,20 +1,46 @@ /* istanbul ignore file */ -export async function normalizeImageFile(file) { +export async function normalizeImageFile( + file, + { maxLongEdge = 2048, quality = 0.9 } = {} +) { if (!(file instanceof Blob)) { throw new TypeError('Expected a File or Blob instance'); } - const blob = file.slice(0, file.size, file.type || 'image/jpeg'); - const bitmap = await createImageBitmapWithOrientation(blob); - const original = await drawBitmapToBlob(bitmap, blob.type); - const thumb = await createThumbnail(bitmap, blob.type); + const mime = file.type || 'image/jpeg'; + const original = file.slice(0, file.size, mime); + const bitmap = await createImageBitmapWithOrientation(original); + const { width, height } = getBitmapSize(bitmap); + + const displayBlob = await drawBitmapToBlob( + bitmap, + mime, + width, + height, + quality + ); + + const longest = Math.max(width, height); + const resizeScale = longest > maxLongEdge ? maxLongEdge / longest : 1; + const targetWidth = Math.round(width * resizeScale); + const targetHeight = Math.round(height * resizeScale); + const resized = await drawBitmapToBlob( + bitmap, + mime, + targetWidth, + targetHeight, + quality + ); + const thumb = await createThumbnail(bitmap, mime); return { original, + display: displayBlob, + resized, thumb, - width: bitmap.width, - height: bitmap.height, + width, + height, }; } @@ -37,13 +63,17 @@ async function drawBitmapToBlob(bitmap, mime, width, height, quality = 0.92) { const ctx = canvas.getContext('2d'); ctx.drawImage(bitmap, 0, 0, targetWidth, targetHeight); const blob = await new Promise((resolve, reject) => { - canvas.toBlob((result) => { - if (!result) { - reject(new Error('Unable to create blob from canvas')); - return; - } - resolve(result); - }, mime, quality); + canvas.toBlob( + (result) => { + if (!result) { + reject(new Error('Unable to create blob from canvas')); + return; + } + resolve(result); + }, + mime, + quality + ); }); return blob; } diff --git a/src/utils/iteration-api.js b/src/utils/iteration-api.js new file mode 100644 index 0000000..a565add --- /dev/null +++ b/src/utils/iteration-api.js @@ -0,0 +1,169 @@ +import { blobToBase64, base64ToBlob } from './image.js'; + +export const GEMINI_TEXT_MODEL = 'models/gemini-2.5-flash:generateContent'; +export const GEMINI_IMAGE_MODEL = + 'models/gemini-2.5-flash-image:generateContent'; + +export const DEFAULT_SAMPLING = Object.freeze({ + temperature: 0.7, + topP: 0.9, + topK: 32, +}); + +export const inlinePart = (mimeType, base64) => ({ + inlineData: { mimeType, data: base64 }, +}); +export const textPart = (text) => ({ text }); + +export class IterationApi { + constructor({ client }) { + if (!client) { + throw new Error('client is required'); + } + this.client = client; + } + + async generateRoomBrief({ promptText, base64Main, mimeMain }) { + const body = { + contents: [ + { + parts: [textPart(promptText), inlinePart(mimeMain, base64Main)], + }, + ], + generationConfig: { ...DEFAULT_SAMPLING }, + }; + const json = await this.client.generateContent({ + model: GEMINI_TEXT_MODEL, + body, + timeoutMs: 120_000, + }); + const raw = (json?.candidates?.[0]?.content?.parts || []) + .map((part) => part.text) + .filter(Boolean) + .join('\n') + .trim(); + /* istanbul ignore next */ + if (!raw) { + throw new Error('room_brief_empty'); + } + const lines = raw + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + const [paragraph = ''] = lines; + const bullets = lines + .slice(1) + .map((line) => line.replace(/^[•\-–]\s?/, '')) + .filter(Boolean); + return { paragraph, bullets, raw }; + } + + async generateVariant({ + promptText, + base64Main, + mimeMain, + references = [], + sampling = DEFAULT_SAMPLING, + }) { + const parts = [textPart(promptText), inlinePart(mimeMain, base64Main)]; + references.forEach((ref) => parts.push(inlinePart(ref.mime, ref.base64))); + return this._runImageRequest(parts, sampling); + } + + async finalizeHiRes({ + promptText, + base64Main, + mimeMain, + base64Child, + mimeChild, + references = [], + sampling = DEFAULT_SAMPLING, + }) { + const parts = [ + textPart(promptText), + inlinePart(mimeMain, base64Main), + inlinePart(mimeChild, base64Child), + ]; + references.forEach((ref) => parts.push(inlinePart(ref.mime, ref.base64))); + return this._runImageRequest(parts, sampling); + } + + async _runImageRequest(parts, sampling) { + const body = { contents: [{ parts }], generationConfig: { ...sampling } }; + const json = await this.client.generateContent({ + model: GEMINI_IMAGE_MODEL, + body, + timeoutMs: 180_000, + }); + return this.parseFlashImageResponse(json); + } + + async parseFlashImageResponse(json) { + const out = { imageUrl: null, text: '', warnings: [] }; + const candidate = json?.candidates?.[0]; + const parts = candidate?.content?.parts || []; + for (const part of parts) { + /* istanbul ignore next */ + if ( + !out.imageUrl && + part.inlineData && + /^image\//i.test(part.inlineData.mimeType || '') + ) { + const base64 = part.inlineData.data || part.inlineData.bytes || ''; + const blob = base64ToBlob(base64, part.inlineData.mimeType); + out.imageUrl = URL.createObjectURL(blob); + } + if (!out.text && typeof part.text === 'string' && part.text.trim()) { + out.text = part.text.trim(); + } + if (out.imageUrl && out.text) break; + } + /* istanbul ignore next */ + if (!out.imageUrl) { + throw new Error('no_image_in_response'); + } + if (!out.text) { + out.warnings.push('auto-fixed: descriptor missing'); + out.text = await this.autoDescribeFromBlobUrl(out.imageUrl); + } + return out; + } + + async autoDescribeFromBlobUrl(imageUrl) { + const blob = await (await fetch(imageUrl)).blob(); + const base64 = await blobToBase64(blob); + const body = { + contents: [ + { + parts: [ + { + text: 'Write ONE short free-text paragraph describing this interior image (mood, key changes vs base, 2–3 ideas for the next iteration). Avoid lists, headings or markdown.', + }, + { + inlineData: { + mimeType: blob.type || 'image/jpeg', + data: base64, + }, + }, + ], + }, + ], + generationConfig: { temperature: 0.7 }, + }; + const json = await this.client.generateContent({ + model: GEMINI_TEXT_MODEL, + body, + timeoutMs: 90_000, + }); + const text = (json?.candidates?.[0]?.content?.parts || []) + .map((part) => part.text) + .filter(Boolean) + .join(' ') + .trim(); + /* istanbul ignore next */ + if (!text) { + throw new Error('mini_text_call_empty'); + } + return text; + } +} diff --git a/src/utils/iteration-api.test.js b/src/utils/iteration-api.test.js new file mode 100644 index 0000000..42712ab --- /dev/null +++ b/src/utils/iteration-api.test.js @@ -0,0 +1,239 @@ +import { IterationApi } from './iteration-api.js'; + +describe('IterationApi', () => { + let client; + let api; + let originalCreateObjectURL; + let originalFetch; + const createResponse = (parts) => ({ candidates: [{ content: { parts } }] }); + + beforeEach(() => { + if (!global.URL) { + global.URL = {}; + } + originalCreateObjectURL = global.URL.createObjectURL; + global.URL.createObjectURL = jest.fn(() => 'blob:test'); + originalFetch = global.fetch; + global.fetch = jest.fn(() => + Promise.resolve({ + blob: () => Promise.resolve(new Blob(['test'], { type: 'image/png' })), + }) + ); + client = { generateContent: jest.fn() }; + api = new IterationApi({ client }); + }); + + afterEach(() => { + if (originalCreateObjectURL) { + global.URL.createObjectURL = originalCreateObjectURL; + } else { + delete global.URL.createObjectURL; + } + global.fetch = originalFetch; + jest.clearAllMocks(); + }); + + it('returns image and text when both are present', async () => { + client.generateContent.mockResolvedValue( + createResponse([ + { inlineData: { mimeType: 'image/png', data: btoa('fake') } }, + { text: 'Mood description' }, + ]) + ); + + const result = await api.generateVariant({ + promptText: 'prompt', + base64Main: 'base', + mimeMain: 'image/png', + }); + + expect(result.imageUrl).toBe('blob:test'); + expect(result.text).toBe('Mood description'); + expect(result.warnings).toEqual([]); + expect(client.generateContent).toHaveBeenCalledTimes(1); + }); + + it('supports inlineData bytes field for images', async () => { + client.generateContent.mockResolvedValue( + createResponse([ + { inlineData: { mimeType: 'image/png', bytes: btoa('fake-bytes') } }, + { text: 'Bytes payload' }, + ]) + ); + + const result = await api.generateVariant({ + promptText: 'prompt', + base64Main: 'base', + mimeMain: 'image/png', + }); + + expect(result.imageUrl).toBe('blob:test'); + expect(result.text).toBe('Bytes payload'); + }); + + it('falls back to default mime type when blob type is missing', async () => { + const altFetch = jest.fn(() => + Promise.resolve({ + blob: () => Promise.resolve(new Blob(['alt'], { type: '' })), + }) + ); + global.fetch = altFetch; + client.generateContent + .mockResolvedValueOnce( + createResponse([ + { inlineData: { mimeType: 'image/png', data: btoa('fake') } }, + ]) + ) + .mockResolvedValueOnce(createResponse([{ text: 'Repaired text' }])); + + const result = await api.generateVariant({ + promptText: 'prompt', + base64Main: 'base', + mimeMain: 'image/png', + }); + + expect(result.text).toBe('Repaired text'); + expect(altFetch).toHaveBeenCalledTimes(1); + }); + + it('auto describes blobs directly when invoked standalone', async () => { + const altFetch = jest.fn(() => + Promise.resolve({ + blob: () => Promise.resolve(new Blob(['direct'], { type: '' })), + }) + ); + global.fetch = altFetch; + client.generateContent.mockResolvedValue( + createResponse([{ text: 'Standalone description' }]) + ); + + const text = await api.autoDescribeFromBlobUrl('blob:direct'); + + expect(text).toBe('Standalone description'); + expect(altFetch).toHaveBeenCalledTimes(1); + }); + + it('auto-repairs missing text using a follow-up description call', async () => { + client.generateContent + .mockResolvedValueOnce( + createResponse([ + { inlineData: { mimeType: 'image/png', data: btoa('fake') } }, + ]) + ) + .mockResolvedValueOnce( + createResponse([{ text: 'Auto generated paragraph' }]) + ); + + const result = await api.generateVariant({ + promptText: 'prompt', + base64Main: 'base', + mimeMain: 'image/png', + }); + + expect(result.text).toBe('Auto generated paragraph'); + expect(result.warnings).toEqual(['auto-fixed: descriptor missing']); + expect(client.generateContent).toHaveBeenCalledTimes(2); + }); + + it('throws when the response does not include an image', async () => { + client.generateContent.mockResolvedValue( + createResponse([{ text: 'Only text' }]) + ); + + await expect( + api.generateVariant({ + promptText: 'prompt', + base64Main: 'base', + mimeMain: 'image/png', + }) + ).rejects.toThrow('no_image_in_response'); + }); + + it('requires a client instance', () => { + expect(() => new IterationApi({})).toThrow('client is required'); + }); + + it('parses room brief paragraph and bullets', async () => { + client.generateContent.mockResolvedValue( + createResponse([ + { text: 'Paragraph here\n• keep envelope\n- add layers' }, + ]) + ); + + const result = await api.generateRoomBrief({ + promptText: 'brief prompt', + base64Main: 'base', + mimeMain: 'image/png', + }); + + expect(result.paragraph).toBe('Paragraph here'); + expect(result.bullets).toEqual(['keep envelope', 'add layers']); + expect(client.generateContent).toHaveBeenCalledTimes(1); + }); + + it('handles room brief responses missing a leading paragraph', async () => { + client.generateContent.mockResolvedValue( + createResponse([{ text: '\n• bullet only' }]) + ); + + const result = await api.generateRoomBrief({ + promptText: 'brief prompt', + base64Main: 'base', + mimeMain: 'image/png', + }); + + expect(result.paragraph).toBe('• bullet only'); + expect(result.bullets).toEqual([]); + }); + + it('supports hi-res finalization requests', async () => { + client.generateContent.mockResolvedValue( + createResponse([ + { inlineData: { mimeType: 'image/png', data: btoa('image') } }, + { text: 'Build notes paragraph' }, + ]) + ); + + const result = await api.finalizeHiRes({ + promptText: 'final prompt', + base64Main: 'base', + mimeMain: 'image/png', + base64Child: 'child', + mimeChild: 'image/png', + }); + + expect(result.text).toBe('Build notes paragraph'); + expect(result.imageUrl).toBe('blob:test'); + expect(client.generateContent).toHaveBeenCalledTimes(1); + }); + + it('throws when room brief output is empty', async () => { + client.generateContent.mockResolvedValue(createResponse([])); + + await expect( + api.generateRoomBrief({ + promptText: 'brief prompt', + base64Main: 'base', + mimeMain: 'image/png', + }) + ).rejects.toThrow('room_brief_empty'); + }); + + it('throws when auto describe returns no text', async () => { + client.generateContent + .mockResolvedValueOnce( + createResponse([ + { inlineData: { mimeType: 'image/png', data: btoa('fake') } }, + ]) + ) + .mockResolvedValueOnce(createResponse([])); + + await expect( + api.generateVariant({ + promptText: 'prompt', + base64Main: 'base', + mimeMain: 'image/png', + }) + ).rejects.toThrow('mini_text_call_empty'); + }); +}); diff --git a/src/utils/iteration-prompts.js b/src/utils/iteration-prompts.js new file mode 100644 index 0000000..9193f32 --- /dev/null +++ b/src/utils/iteration-prompts.js @@ -0,0 +1,116 @@ +import { DEFAULT_SAMPLING } from './iteration-api.js'; + +export function diversityDeck() { + return [ + 'slightly warmer palette; gentler contrast', + 'slightly cooler palette; crisper contrast', + 'increase textile presence; softer layering', + 'reduce textiles; cleaner lines, fewer throws', + 'airier layout with more negative space', + 'denser, cozier seating cluster', + 'wood-forward materials; minimize metal', + 'metal accents slightly emphasized; reduce wood', + 'soft evening lighting (~2700–3000K), cozy ambience', + 'neutral daytime lighting (~4000–4500K), clear ambience', + 'tone-on-tone, low-contrast composition', + 'bolder accents; stronger figure-ground definition', + ]; +} + +export function buildIterationPrompt({ + roomBrief, + purpose, + roundGuidance, + favoritesTextBlock, + diversityHint, +}) { + return [ + 'Use the attached base room photo as fixed camera pose and architectural envelope. Keep the existing room geometry unchanged.', + 'If reference images are attached, transfer only their style (palette, textures, furniture mood, lighting mood) and ignore their layout.', + 'Guidance for this image:', + `• Room brief (silent): ${roomBrief || 'None'}`, + `• Intended use: ${purpose || 'None'}`, + `• User’s global guidance for this round: ${roundGuidance || 'None'}`, + `• Selected favorites (as free-text, raw): ${favoritesTextBlock || 'None'}`, + '', + `Diversity hint: ${diversityHint || 'None'}`, + '', + 'Generate ONE updated interior variant at 1024×682 while keeping the camera angle and room envelope identical to the base photo.', + 'Return ONE image and ONE short free-text paragraph (single paragraph) describing the mood, the key changes vs the base, and 2–3 ideas for the next iteration. Avoid logos/text.', + ].join('\n'); +} + +export async function runIterationBatch({ + api, + base64Main, + mimeMain, + references, + roomBrief, + purpose, + roundGuidance, + favoritesTextBlock, + sampling = DEFAULT_SAMPLING, + onProgress, +}) { + if (!api || typeof api.generateVariant !== 'function') { + throw new Error('api with generateVariant is required'); + } + const deck = diversityDeck(); + const tasks = deck.map((hint, index) => async () => { + const promptText = buildIterationPrompt({ + roomBrief, + purpose, + roundGuidance, + favoritesTextBlock, + diversityHint: hint, + }); + try { + const result = await api.generateVariant({ + promptText, + base64Main, + mimeMain, + references, + sampling, + }); + onProgress?.({ index, ok: true, result, hint }); + } catch (error) { + onProgress?.({ index, ok: false, error, hint }); + } + }); + await pool(tasks, 6); +} + +export function buildFinalizationPrompt() { + return [ + 'Use the attached base room photo as fixed camera & envelope and the attached selected child image as strict visual target.', + 'Do not move architectural elements (walls, openings, ceiling heights); treat the room geometry as immutable.', + 'Keep doors, windows, and room entries in their original locations from the base photo.', + 'Produce ONE faithful high-resolution re-render at 1536×1024 (polish only; no new composition).', + 'Then return ONE short paragraph of Build Notes (5–7 sentences): materials & finishes, palette accents, lighting layers (day/evening), 1–2 dimensional tips in meters, and any “avoid” reminders. Avoid logos/text.', + ].join(' '); +} + +async function pool(taskFns, size) { + let cursor = 0; + const running = new Set(); + + async function launchNext() { + if (cursor >= taskFns.length) return; + const fn = taskFns[cursor++]; + const promise = fn().finally(() => running.delete(promise)); + running.add(promise); + if (running.size < size) { + await launchNext(); + } + } + + const starters = Math.min(size, taskFns.length); + for (let i = 0; i < starters; i += 1) { + await launchNext(); + } + + while (running.size > 0) { + await Promise.race([...running]); + await launchNext(); + } +} diff --git a/src/utils/iteration-prompts.test.js b/src/utils/iteration-prompts.test.js new file mode 100644 index 0000000..ff216ce --- /dev/null +++ b/src/utils/iteration-prompts.test.js @@ -0,0 +1,121 @@ +import { + diversityDeck, + buildIterationPrompt, + buildFinalizationPrompt, + runIterationBatch, +} from './iteration-prompts.js'; + +describe('iteration prompts', () => { + it('provides 12 diversity hints covering multiple axes', () => { + const deck = diversityDeck(); + expect(deck).toHaveLength(12); + const unique = new Set(deck); + expect(unique.size).toBe(deck.length); + expect(deck.join(' ')).toContain('palette'); + expect(deck.join(' ')).toContain('lighting'); + }); + + it('builds an iteration prompt containing all guidance sections', () => { + const prompt = buildIterationPrompt({ + roomBrief: 'Paragraph here', + purpose: 'Bedroom', + roundGuidance: 'lean warmer', + favoritesTextBlock: 'Favorite descriptor', + diversityHint: 'slightly warmer palette', + }); + expect(prompt).toContain('Room brief (silent): Paragraph here'); + expect(prompt).toContain('Intended use: Bedroom'); + expect(prompt).toContain( + 'User’s global guidance for this round: lean warmer' + ); + expect(prompt).toContain( + 'Selected favorites (as free-text, raw): Favorite descriptor' + ); + expect(prompt).toContain('Diversity hint: slightly warmer palette'); + expect(prompt).toContain('Keep the existing room geometry unchanged'); + expect(prompt).toContain( + 'transfer only their style (palette, textures, furniture mood, lighting mood) and ignore their layout' + ); + }); + + it('uses fallback placeholders when data is missing', () => { + const prompt = buildIterationPrompt({}); + expect(prompt).toContain('Room brief (silent): None'); + expect(prompt).toContain('Intended use: None'); + expect(prompt).toContain('User’s global guidance for this round: None'); + expect(prompt).toContain('Selected favorites (as free-text, raw): None'); + expect(prompt).toContain('Diversity hint: None'); + }); + + it('builds a finalization prompt with hi-res guidance', () => { + const prompt = buildFinalizationPrompt(); + expect(prompt).toContain('1536×1024'); + expect(prompt).toContain('Build Notes'); + expect(prompt).toContain('room geometry as immutable'); + expect(prompt).toContain('room entries in their original locations'); + }); + + it('runs a full iteration batch and surfaces progress events', async () => { + const results = []; + const api = { + generateVariant: jest.fn(), + }; + api.generateVariant + .mockImplementationOnce( + () => + new Promise((resolve) => + setTimeout( + () => + resolve({ imageUrl: 'url-1', text: 'desc-1', warnings: [] }), + 0 + ) + ) + ) + .mockImplementationOnce( + () => + new Promise((_, reject) => + setTimeout(() => reject(new Error('failure')), 0) + ) + ) + .mockImplementation( + () => + new Promise((resolve) => + setTimeout( + () => resolve({ imageUrl: 'url', text: 'desc', warnings: [] }), + 0 + ) + ) + ); + + await runIterationBatch({ + api, + base64Main: 'base', + mimeMain: 'image/jpeg', + references: [], + roomBrief: 'brief', + purpose: 'Bedroom', + roundGuidance: 'guidance', + favoritesTextBlock: '', + onProgress: (event) => results.push(event), + }); + + expect(api.generateVariant).toHaveBeenCalledTimes(12); + expect(results.some((event) => event.ok === false)).toBe(true); + expect(results.some((event) => event.ok === true)).toBe(true); + }); + + it('throws when the API object is missing the generateVariant function', async () => { + await expect( + runIterationBatch({ + api: {}, + base64Main: 'base', + mimeMain: 'image/jpeg', + references: [], + roomBrief: 'brief', + purpose: 'Bedroom', + roundGuidance: '', + favoritesTextBlock: '', + }) + ).rejects.toThrow('api with generateVariant is required'); + }); +}); diff --git a/src/utils/palette.test.js b/src/utils/palette.test.js index 26faa3f..e72b3b1 100644 --- a/src/utils/palette.test.js +++ b/src/utils/palette.test.js @@ -46,6 +46,10 @@ describe('generatePaletteVariations', () => { expect(palettesEqual(variant, base)).toBe(false); }); }); + + it('returns empty array when the base palette is missing', () => { + expect(generatePaletteVariations(null)).toEqual([]); + }); }); describe('ensurePaletteOptions', () => { @@ -120,6 +124,31 @@ describe('ensurePaletteOptions', () => { expect(options).toHaveLength(1); expect(options[0].primary?.name).toBe('Untinted base'); }); + + it('stops collecting additional palettes once five options are assembled', () => { + const base = buildBasePalette(); + const provided = [ + '#112233', + '#223344', + '#334455', + '#445566', + '#556677', + '#667788', + ].map((hex, index) => ({ + primary: { hex, name: `Primary ${index}` }, + secondary: { hex: '#8899AA' }, + accent: { hex: '#AABBCC' }, + })); + + const options = ensurePaletteOptions({ + palette_60_30_10: base, + palette_options: provided, + }); + + expect(options).toHaveLength(5); + const collectedSignatures = options.map(signature); + expect(collectedSignatures).not.toContain(signature(provided[5])); + }); }); describe('normalizeHexString & palettesEqual', () => { @@ -152,6 +181,8 @@ describe('generatePaletteVariations with grayscale input', () => { const variants = generatePaletteVariations(base); expect(variants.length).toBeGreaterThanOrEqual(4); expect(new Set(variants.map(signature)).size).toBe(variants.length); - expect(variants.some((variant) => variant.primary.hex !== '#777777')).toBe(true); + expect(variants.some((variant) => variant.primary.hex !== '#777777')).toBe( + true + ); }); }); diff --git a/src/utils/project-store.js b/src/utils/project-store.js index 72f997d..bb41f83 100644 --- a/src/utils/project-store.js +++ b/src/utils/project-store.js @@ -129,12 +129,19 @@ async function getDb() { function openDatabase(name, version, { upgrade } = {}) { if (!('indexedDB' in window)) { - return Promise.reject(new Error('IndexedDB is not available in this environment')); + return Promise.reject( + new Error('IndexedDB is not available in this environment') + ); } return new Promise((resolve, reject) => { const request = indexedDB.open(name, version); request.onupgradeneeded = (event) => { - upgrade?.(request.result, event.oldVersion, event.newVersion, request.transaction); + upgrade?.( + request.result, + event.oldVersion, + event.newVersion, + request.transaction + ); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); @@ -151,9 +158,16 @@ function transactionDone(tx) { } async function enforceProjectLimit(db, projectId) { - const total = await sumMediaBytes(db, (record) => record.projectId === projectId); + const total = await sumMediaBytes( + db, + (record) => record.projectId === projectId + ); if (total <= PROJECT_LIMIT_BYTES) return; - await evictRenders(db, (record) => record.projectId === projectId, total - PROJECT_LIMIT_BYTES); + await evictRenders( + db, + (record) => record.projectId === projectId, + total - PROJECT_LIMIT_BYTES + ); } async function enforceGlobalLimit(db) { @@ -218,7 +232,10 @@ async function evictRenders(db, predicate, requiredBytes) { } function generateId() { - if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + if ( + typeof crypto !== 'undefined' && + typeof crypto.randomUUID === 'function' + ) { return crypto.randomUUID(); } return `id-${Math.random().toString(36).slice(2, 10)}-${Date.now()}`; diff --git a/src/utils/prompts.js b/src/utils/prompts.js index 1c76747..305b5fb 100644 --- a/src/utils/prompts.js +++ b/src/utils/prompts.js @@ -50,10 +50,14 @@ export function normalizeRenderGallery(analysis, paletteOverride) { if (!analysis) return []; const existing = Array.isArray(analysis.render_gallery) - ? analysis.render_gallery.filter((item) => item && typeof item.style === 'string') + ? analysis.render_gallery.filter( + (item) => item && typeof item.style === 'string' + ) : []; const styleScores = Array.isArray(analysis.styles_top10) - ? analysis.styles_top10.filter((item) => item && typeof item.style === 'string') + ? analysis.styles_top10.filter( + (item) => item && typeof item.style === 'string' + ) : []; const palette = paletteOverride ?? analysis.palette_60_30_10; @@ -74,7 +78,9 @@ export function normalizeRenderGallery(analysis, paletteOverride) { } } - const styleReason = new Map(styleScores.map((item) => [item.style, item.why])); + const styleReason = new Map( + styleScores.map((item) => [item.style, item.why]) + ); const existingByStyle = new Map(existing.map((item) => [item.style, item])); const results = []; const used = new Set(); @@ -143,10 +149,16 @@ function buildGalleryFallback({ ); } guidanceParts.push('Preserve existing architecture and layout.'); + guidanceParts.push( + 'Room structure is fixed; no moving walls, openings, or ceiling lines.' + ); + guidanceParts.push( + 'Doors, windows, and room entries stay exactly where they are in the source photo.' + ); const promptLines = [ `Photorealistic interior render of the provided room styled as ${style}.`, - 'Maintain the original camera angle, envelope, and proportions.', + 'Maintain the original camera angle, envelope, and proportions. Do not relocate walls, doors, windows, entryways, or ceiling heights.', ]; if (architecturalNotes) { @@ -163,7 +175,9 @@ function buildGalleryFallback({ if (limitations?.length) { promptLines.push(`Respect constraints: ${limitations.join('; ')}.`); } - promptLines.push(`Highlight signature ${style} materials, silhouettes, and styling.`); + promptLines.push( + `Highlight signature ${style} materials, silhouettes, and styling.` + ); if (negativePrompts.length) { promptLines.push(`Avoid: ${negativePrompts.join('; ')}.`); } diff --git a/src/utils/prompts.test.js b/src/utils/prompts.test.js index f9e0957..f708835 100644 --- a/src/utils/prompts.test.js +++ b/src/utils/prompts.test.js @@ -9,7 +9,11 @@ import { SUPPORTED_STYLES } from '../constants/analysis-schema.js'; describe('prompt builders', () => { test('buildAnalysisPrompt returns multiline prompt with defaults', () => { - const prompt = buildAnalysisPrompt({ intendedUse: 'Living Room', scope: 3, notes: '' }); + const prompt = buildAnalysisPrompt({ + intendedUse: 'Living Room', + scope: 3, + notes: '', + }); expect(prompt).toContain('ROLE: Interior design analyst for empty rooms.'); expect(prompt).toContain('INTENDED USE: Living Room.'); expect(prompt).toContain('INTERVENTION SCOPE: 3.'); @@ -27,7 +31,9 @@ describe('prompt builders', () => { }); test('buildABPrompts throws when required data missing', () => { - expect(() => buildABPrompts('', null)).toThrow('basePrompt and axes are required'); + expect(() => buildABPrompts('', null)).toThrow( + 'basePrompt and axes are required' + ); }); test('buildHeroPrompts returns three hero mixes', () => { @@ -40,7 +46,9 @@ describe('prompt builders', () => { }); test('buildHeroPrompts throws when base prompt missing', () => { - expect(() => buildHeroPrompts('', null)).toThrow('basePrompt and axes are required'); + expect(() => buildHeroPrompts('', null)).toThrow( + 'basePrompt and axes are required' + ); }); }); @@ -95,7 +103,12 @@ describe('normalizeRenderGallery', () => { test('preserves existing prompts and fills missing styles to ten entries', () => { const analysis = { render_gallery: [ - { style: 'Scandi', prompt: 'Existing Scandi prompt', focus: 'Scandi focus', guidance: 'Scandi guidance' }, + { + style: 'Scandi', + prompt: 'Existing Scandi prompt', + focus: 'Scandi focus', + guidance: 'Scandi guidance', + }, { style: 'Japandi', prompt: 'Existing Japandi prompt' }, { style: 'Modern Minimal', prompt: 'Existing Modern prompt' }, { style: 'Contemporary Cozy', prompt: 'Existing Cozy prompt' }, @@ -104,9 +117,15 @@ describe('normalizeRenderGallery', () => { { style: 'Scandi', why: 'Light woods and serene palette' }, { style: 'Japandi', why: 'Calm, crafted minimalism' }, { style: 'Modern Minimal', why: 'Clean planes and negative space' }, - { style: 'Contemporary Cozy', why: 'Soft textures with modern silhouettes' }, + { + style: 'Contemporary Cozy', + why: 'Soft textures with modern silhouettes', + }, { style: 'Mid-Century', why: 'Warm woods and iconic shapes' }, - { style: 'Industrial Soft', why: 'Tactile contrast without harsh edges' }, + { + style: 'Industrial Soft', + why: 'Tactile contrast without harsh edges', + }, ], palette_60_30_10: palette, constraints: { limitations: ['Do not remove existing flooring'] }, @@ -125,16 +144,24 @@ describe('normalizeRenderGallery', () => { expect(artDeco).toBeDefined(); expect(artDeco.prompt).toContain('Art-Deco'); expect(artDeco.guidance).toContain('Preserve existing architecture'); + expect(artDeco.guidance).toContain('Room structure is fixed'); + expect(artDeco.guidance).toContain( + 'Doors, windows, and room entries stay exactly where they are' + ); + expect(artDeco.prompt).toContain('entryways'); }); test('trims existing text values and falls back when empty', () => { const analysis = { render_gallery: [ - { style: 'Rustic', prompt: ' Custom rustic prompt ', focus: ' ', guidance: '' }, - ], - styles_top10: [ - { style: 'Rustic', why: ' ' }, + { + style: 'Rustic', + prompt: ' Custom rustic prompt ', + focus: ' ', + guidance: '', + }, ], + styles_top10: [{ style: 'Rustic', why: ' ' }], palette_60_30_10: palette, negative_prompts: [], photo_findings: {}, @@ -146,6 +173,10 @@ describe('normalizeRenderGallery', () => { expect(rustic.prompt).toBe('Custom rustic prompt'); expect(rustic.focus).toBe('Rustic concept direction'); expect(rustic.guidance).toContain('Preserve existing architecture'); + expect(rustic.guidance).toContain('Room structure is fixed'); + expect(rustic.guidance).toContain( + 'Doors, windows, and room entries stay exactly where they are' + ); }); test('handles missing palette and limitations gracefully', () => { @@ -162,7 +193,9 @@ describe('normalizeRenderGallery', () => { }; const normalized = normalizeRenderGallery(analysis); - const industrial = normalized.find((item) => item.style === 'Industrial Soft'); + const industrial = normalized.find( + (item) => item.style === 'Industrial Soft' + ); expect(industrial.prompt).toContain('Exposed brick wall'); expect(industrial.prompt).toContain('Avoid: No text'); }); @@ -187,14 +220,16 @@ describe('normalizeRenderGallery', () => { expect(boho.prompt).toContain('Golden hour glow'); expect(boho.focus).toBe('Existing focus'); expect(boho.guidance).toContain('Preserve existing architecture'); + expect(boho.guidance).toContain('Room structure is fixed'); + expect(boho.guidance).toContain( + 'Doors, windows, and room entries stay exactly where they are' + ); }); test('describes palette even when swatches are partial', () => { const analysis = { render_gallery: [], - styles_top10: [ - { style: 'Mediterranean', why: 'Sun-baked textures' }, - ], + styles_top10: [{ style: 'Mediterranean', why: 'Sun-baked textures' }], palette_60_30_10: { primary: { hex: '#101010' }, secondary: { name: 'Sea Glass' }, @@ -207,7 +242,9 @@ describe('normalizeRenderGallery', () => { }; const normalized = normalizeRenderGallery(analysis); - const mediterranean = normalized.find((item) => item.style === 'Mediterranean'); + const mediterranean = normalized.find( + (item) => item.style === 'Mediterranean' + ); expect(mediterranean.prompt).toContain('Primary #101010'); expect(mediterranean.prompt).toContain('Secondary Sea Glass'); expect(mediterranean.prompt).not.toContain('Accent'); @@ -220,10 +257,16 @@ describe('normalizeRenderGallery', () => { prompt: index === 0 ? ' ' : `${style} prompt`, })); const analysis = { - render_gallery: [...baseGallery, { style: extraStyle, prompt: `${extraStyle} prompt` }], + render_gallery: [ + ...baseGallery, + { style: extraStyle, prompt: `${extraStyle} prompt` }, + ], styles_top10: [ { style: extraStyle, why: 'Just for exploration' }, - ...SUPPORTED_STYLES.map((style) => ({ style, why: `${style} fit reason` })), + ...SUPPORTED_STYLES.map((style) => ({ + style, + why: `${style} fit reason`, + })), { style: 'Japandi', why: 'duplicate should be skipped' }, { style: '', why: 'invalid should be ignored' }, ], @@ -239,9 +282,7 @@ describe('normalizeRenderGallery', () => { test('uses palette override when provided', () => { const analysis = { render_gallery: [], - styles_top10: [ - { style: 'Scandi', why: 'Calm minimalism' }, - ], + styles_top10: [{ style: 'Scandi', why: 'Calm minimalism' }], palette_60_30_10: { primary: { name: 'Base Primary', hex: '#101010' }, secondary: { name: 'Base Secondary', hex: '#222222' }, @@ -262,6 +303,28 @@ describe('normalizeRenderGallery', () => { expect(scandi.prompt).toContain('#778899'); }); + test('enters fallback fill path when fewer than ten supported styles exist', () => { + const originalStyles = [...SUPPORTED_STYLES]; + SUPPORTED_STYLES.splice(0, SUPPORTED_STYLES.length, 'Scandi', 'Japandi'); + + try { + const analysis = { + render_gallery: [], + styles_top10: [{ style: 'Scandi', why: 'Calm balance' }], + negative_prompts: [], + photo_findings: {}, + }; + + const normalized = normalizeRenderGallery(analysis); + expect(normalized).toHaveLength(2); + expect( + normalized.every((item) => SUPPORTED_STYLES.includes(item.style)) + ).toBe(true); + } finally { + SUPPORTED_STYLES.splice(0, SUPPORTED_STYLES.length, ...originalStyles); + } + }); + test('returns empty array when analysis missing', () => { expect(normalizeRenderGallery(null)).toEqual([]); });