diff --git a/.claude/skills/audit-architecture/SKILL.md b/.claude/skills/audit-architecture/SKILL.md index 814654e..0da5805 100644 --- a/.claude/skills/audit-architecture/SKILL.md +++ b/.claude/skills/audit-architecture/SKILL.md @@ -15,10 +15,13 @@ When the user asks to audit, review, or check architecture compliance, code stru Run each check and report findings: ### 1. Component Size Limits + Search `src/components/` for `.tsx` files over 300 lines. Flag each with line count and suggestion to extract sub-components or data files. ### 2. Folder-Based Topic Structure + For each folder in `src/components/visualizations/*/`, verify these files exist: + - `index.tsx` - `types.ts` - `helpers.ts` @@ -27,26 +30,37 @@ For each folder in `src/components/visualizations/*/`, verify these files exist: Report any missing files. ### 3. `"use client"` Directive + Check all visualization components in `src/components/visualizations/` for the `"use client"` directive on line 1. Flag any missing. ### 4. Named Exports + Check visualization components use named exports (not `export default`). Search for `export default` in visualization files and flag. ### 5. Import Alias Usage + Search for deep relative imports (e.g., `../../../`) in `src/components/`. These should use the `@/` alias. Flag violations. ### 6. Const-Form Components -Search for `export function` declarations in `src/components/visualizations/` that should be `const`-form arrow functions. Note: the existing codebase uses `export function` for main visualization components, so only flag new patterns that deviate from the established convention. + +Search for `export function` or `function X(` component declarations in `src/components/` and `src/hooks/`. Every component and hook uses const arrow form; only Next.js `page.tsx` and `layout.tsx` under `src/app/` use `export default function`. ### 7. Panel Title Constants + Search visualization files for hardcoded strings `"Source Code"` or `"Console Output"` that should use `VISUALIZATION_PANEL_TITLES` from `uiCopy.ts`. Flag violations. ### 8. ExampleSelector Registration -For topics using `ExampleSelector`, verify their ID appears in `SELECTOR_TOOLBAR_TOPIC_IDS` in `VisualizationPageShell.tsx`. Cross-reference by searching for `ExampleSelector` imports in visualization files. + +For topics with no example picker (`useStepPlayback` called directly, no `ExamplePicker`), verify the registry entry sets `toolbar: "simple"`. For topics that hand-roll a toolbar (`ToolbarPortal`, `TransportControls`, or `VISUALIZATION_EMPTY_STATES.stepDescription` imported in a topic `index.tsx`), flag them: topics must use `VisualizationToolbar`, `SourceCodePanel`, and `useExampleTopic`. + +### 9. Category Branching + +Search `src/components/` and `src/lib/` for `=== "javascript"` or `=== "react"`. Components must read `CATEGORIES[topic.category]` from `src/lib/categories.ts` instead. Flag violations. ## Output Format Report as a numbered list: + ``` 1. PASS: Component size limits (all under 300 lines) 2. FAIL: Missing types.ts in src/components/visualizations/closures/ diff --git a/.claude/skills/audit-release/SKILL.md b/.claude/skills/audit-release/SKILL.md index a83116a..3a858e7 100644 --- a/.claude/skills/audit-release/SKILL.md +++ b/.claude/skills/audit-release/SKILL.md @@ -13,29 +13,37 @@ When the user asks to verify, check, or validate before release, deploy, or merg ## Checks ### 1. Lint + Run `npm run lint`. Report any errors or warnings. ### 2. Build + Run `npm run build`. Report any build failures. Capture and display the generated routes list from build output. ### 3. Route Page Coverage -For every topic in `src/lib/topics.ts`, verify a route page file exists at `src/app///page.tsx`. Flag missing route pages. + +Run `npm run typecheck`. The `VISUALIZATIONS`, theory, keyword, and description registries are typed by `TopicId`, so a topic missing from any of them fails here. Also confirm each topic id has a folder under `src/components/visualizations/`. ### 4. ExampleSelector Registration -Search visualization files for `ExampleSelector` imports. For each topic using it, verify the topic ID is in `SELECTOR_TOOLBAR_TOPIC_IDS` in `src/components/layout/VisualizationPageShell.tsx`. Flag missing registrations. + +For topics whose `index.tsx` does not use `ExamplePicker`, verify the registry entry sets `toolbar: "simple"` so the loading skeleton matches. ### 5. Theory Registration -For every theory content file in `src/content/theory/javascript/` and `src/content/theory/react/`, verify it is imported and registered in `src/content/theory/index.ts`. Flag unregistered files. + +For every theory content file in `src/content/theory//` (every folder), verify it is imported and registered in `src/content/theory/index.ts`. Flag unregistered files. ### 6. Console.log Audit + Search for `console.log` statements in `src/` (excluding `src/content/` and `ConsoleOutput` rendering). Flag any found in production code. ### 7. Topic-Keyword Cross-Check + Verify every topic in `src/lib/topics.ts` has a `TOPIC_KEYWORDS` entry in `src/lib/metadata.ts`. ## Output Format Report as a numbered pass/fail list with actionable items: + ``` 1. PASS: Lint (0 errors, 0 warnings) 2. PASS: Build (17 routes generated) diff --git a/.claude/skills/audit-seo/SKILL.md b/.claude/skills/audit-seo/SKILL.md index 870c150..a95bcbf 100644 --- a/.claude/skills/audit-seo/SKILL.md +++ b/.claude/skills/audit-seo/SKILL.md @@ -15,35 +15,45 @@ When the user asks to audit, check, or verify SEO compliance, metadata, or topic Run each check and report findings: ### 1. TOPIC_KEYWORDS Coverage + Read `src/lib/topics.ts` to get all topic IDs. Read `src/lib/metadata.ts` and verify every topic ID has an entry in `TOPIC_KEYWORDS`. Flag missing topics. ### 2. docsUrl Validation + Verify every topic in `src/lib/topics.ts` has a non-empty `docsUrl` field. Flag any empty or missing values. ### 3. Theory Registration + Read `src/content/theory/index.ts`. For each theory file imported, verify: + - The corresponding `.ts` file exists at the expected path - The topic ID exists in `src/lib/topics.ts` -Flag orphaned theory files or unregistered topics. + Flag orphaned theory files or unregistered topics. ### 4. TOPIC_THEORY_DESCRIPTIONS Coverage + For every topic registered in `src/content/theory/index.ts`, verify it has a corresponding entry in `TOPIC_THEORY_DESCRIPTIONS` in `src/lib/metadata.ts`. Flag missing descriptions. ### 5. relatedTopicIds Validation + For each theory content file, check that: + - `relatedTopicIds` is not an empty array - Each ID in `relatedTopicIds` exists in `src/lib/topics.ts` -Flag empty arrays and invalid IDs. + Flag empty arrays and invalid IDs. ### 6. Sitemap and Robots -Verify `src/app/sitemap.ts` and `src/app/robots.ts` exist. + +Verify `src/app/sitemap.ts` and `src/app/robots.ts` exist, and that every category in `src/lib/categories.ts` has a route page at `src/app//page.tsx`. ### 7. Route Page Coverage -For every topic in `src/lib/topics.ts`, verify a route page file exists at `src/app///page.tsx`. Flag missing route pages. + +Routes are generated from `src/lib/topics.ts` by `src/app/[category]/[topic]/page.tsx`, so every registered topic has a page. Verify `generateStaticParams` still maps over `topics` and that `sitemap.ts` lists every topic route. ## Output Format Report as a pass/fail table: + ``` | Check | Status | Details | |-------------------------------|--------|---------| diff --git a/.claude/skills/create-js-topic/SKILL.md b/.claude/skills/create-js-topic/SKILL.md index b096029..51b7d05 100644 --- a/.claude/skills/create-js-topic/SKILL.md +++ b/.claude/skills/create-js-topic/SKILL.md @@ -1,61 +1,62 @@ --- -description: Create a new JavaScript visualization topic, scaffold all required files, register in topic and theory registries, and verify the build passes. +description: Create a new visualization topic in any category (JavaScript, Frameworks, Backend, AI), scaffold all required files, register in topic and theory registries, and verify the build passes. --- -# Create JS Topic +# Create Topic -Scaffold a new JavaScript visualization topic end-to-end. This skill handles the full multi-file workflow from topic registration to build verification. +Scaffold a new visualization topic end-to-end for any non-React category (for React use `create-react-topic`). This skill handles the full multi-file workflow from topic registration to build verification. ## When to Use -When the user asks to create, add, or build a new JavaScript topic or visualization. +When the user asks to create, add, or build a new topic or visualization in the javascript, frameworks, backend, or ai category. ## Workflow 1. **Gather inputs** from the user: + - Category (`javascript`, `frameworks`, `backend`, or `ai`; see `src/lib/categories.ts`) - Topic ID (kebab-case, e.g., `async-await`) - Title (display name, e.g., "Async/Await") - Difficulty (`beginner`, `intermediate`, or `advanced`) - `docsUrl` (MDN or authoritative reference URL) - Description (1-2 sentences for SEO and landing page cards) - Whether it uses ExampleSelector (multiple sub-examples) - - Whether it should be folder-based (recommended for complex topics) -2. **Register the topic** in `src/lib/topics.ts`. Add to the `topics` array following the existing order convention. +2. **Register the topic** in `src/lib/topics.ts`. Add to `TOPIC_DEFINITIONS` following the existing order convention. The route is derived; set `toolbar: "simple"` only if the topic has no example picker. Once registered, `npm run typecheck` reports every registry that still lacks an entry. 3. **Add SEO keywords** in `src/lib/metadata.ts` under `TOPIC_KEYWORDS`. Add 4-7 relevant search terms. -4. **Create the route page** at `src/app/javascript//page.tsx`. Follow the exact pattern from existing pages (see `src/app/javascript/generators/page.tsx` for reference). +4. **Register the component** in `VISUALIZATIONS` in `src/components/visualizations/registry.tsx` (one `dynamic()` entry keyed by topic id). Routing is generated from the registry. -5. **Scaffold the visualization component:** - - **Single-file:** `src/components/visualizations/.tsx` - - **Folder-based:** `src/components/visualizations//` with `index.tsx`, `types.ts`, `helpers.ts`, `data.ts`, `components/` +5. **Scaffold the visualization folder** at `src/components/visualizations//` with `index.tsx`, `types.ts`, `helpers.ts`, `data.ts`, `components/`. - Read `docs/topic-authoring.md` for the full skeleton template. - - Read `docs/component-reference.md` for component APIs and hooks. + - Read `docs/component-reference.md` for component APIs and hooks, including PipelineDiagram, TokenChips, HeatmapGrid, MetricBars, and MessageFlow for backend and AI topics. + - Use `useExampleTopic(EXAMPLES)` with 2 or 3 examples, `VisualizationToolbar` with an `ExamplePicker` in `leading`, and `SourceCodePanel` for the code. -6. **If using ExampleSelector:** Add topic ID to `SELECTOR_TOOLBAR_TOPIC_IDS` in `src/components/layout/VisualizationPageShell.tsx`. +6. **Extend the shared types:** the step interface extends `BaseStep`, the example interface extends `SourceExample` (both from `src/types/visualization.ts`). -7. **Create theory content** at `src/content/theory/javascript/.ts`. Read `docs/theory-authoring.md` for field-by-field guidance and tone requirements. +7. **Create theory content** at `src/content/theory//.ts`. Read `docs/theory-authoring.md` for field-by-field guidance and tone requirements. 8. **Register theory** in `src/content/theory/index.ts`. Import and add to `THEORY_CONTENT_BY_TOPIC_ID`. 9. **Add theory description** in `src/lib/metadata.ts` under `TOPIC_THEORY_DESCRIPTIONS`. -10. **Verify:** Run `npm run build`. Fix any errors. +10. **Add inbound links:** add the new ID to `relatedTopicIds` of 2 or more existing theory files (keep each list at 3 to 5). + +11. **Verify:** Run `npm run audit:steps -- ` and fix anything it flags, then `npm run verify` (lint, typecheck, format, step audit, build). ## Key Files - `src/lib/topics.ts` -- topic registry - `src/lib/metadata.ts` -- keywords and theory descriptions -- `src/app/javascript//page.tsx` -- route page (new) +- `src/components/visualizations/registry.tsx` -- component registry - `src/components/visualizations/` -- visualization component (new) -- `src/content/theory/javascript/.ts` -- theory content (new) +- `src/content/theory//.ts` -- theory content (new) - `src/content/theory/index.ts` -- theory registry -- `src/components/layout/VisualizationPageShell.tsx` -- SELECTOR_TOOLBAR_TOPIC_IDS ## Documentation -- `docs/topic-authoring.md` -- full JS topic workflow and checklist +- `docs/topic-authoring.md` -- full topic workflow and checklist +- `docs/categories.md` -- category registry - `docs/component-reference.md` -- component APIs, hooks, animations - `docs/theory-authoring.md` -- theory content requirements @@ -64,7 +65,7 @@ When the user asks to create, add, or build a new JavaScript topic or visualizat - No em dashes or AI-sounding language in content - No emojis in step descriptions or UI text - Use `VISUALIZATION_PANEL_TITLES` and `VISUALIZATION_EMPTY_STATES` from `uiCopy.ts` -- Wrap `setActiveExampleId` in `handleExampleChange` callback +- Use `useExampleTopic` + `VisualizationToolbar` + `ExamplePicker`; never hand-roll the toolbar or step pill - Theory `relatedTopicIds` must have 3-5 valid IDs (never empty) -- Code line fading guard: `isDone && !isActive` +- Source code renders through `SourceCodePanel` - Components use `"use client"` directive and named exports diff --git a/.claude/skills/create-react-topic/SKILL.md b/.claude/skills/create-react-topic/SKILL.md index ed018f6..5ac4262 100644 --- a/.claude/skills/create-react-topic/SKILL.md +++ b/.claude/skills/create-react-topic/SKILL.md @@ -22,25 +22,27 @@ When the user asks to create, add, or build a new React topic or visualization. 2. **Read the reference implementation** at `src/components/visualizations/reconciliation/` -- read all files (`types.ts`, `helpers.ts`, `data.ts`, `index.tsx`, `components/DiffPanel.tsx`) to understand the exact pattern. -3. **Register the topic** in `src/lib/topics.ts` with `category: "react"` and `route: "/react/"`. +3. **Register the topic** in `src/lib/topics.ts` with `category: "react"`. The route is derived; do not add one. Once registered, `npm run typecheck` reports every registry that still lacks an entry. 4. **Add SEO keywords** in `src/lib/metadata.ts` under `TOPIC_KEYWORDS`. -5. **Create the route page** at `src/app/react//page.tsx`. Follow the pattern from `src/app/react/reconciliation/page.tsx`. +5. **Register the component** in `VISUALIZATIONS` in `src/components/visualizations/registry.tsx` (one `dynamic()` entry keyed by topic id). Routing is generated from the registry. 6. **Scaffold the folder structure:** + ``` src/components/visualizations// types.ts # Step, Example, Kind types helpers.ts # kindBadgeClass, kindLabel using shared factories data.ts # EXAMPLES array with step data - index.tsx # Main component with ExampleSelector + ComponentTreeDiagram + index.tsx # useExampleTopic + VisualizationToolbar + ExamplePicker + ComponentTreeDiagram components/ # Sub-components as needed ``` + - Read `docs/react-topic-authoring.md` for React-specific patterns - Read `docs/component-reference.md` for ComponentTreeDiagram API -7. **Add to SELECTOR_TOOLBAR_TOPIC_IDS** in `src/components/layout/VisualizationPageShell.tsx`. +7. **Set `toolbar: "simple"`** in the topic entry only if the topic has no example picker (React topics normally do, so skip this). 8. **Create theory content** at `src/content/theory/react/.ts`. Read `docs/theory-authoring.md` for requirements. @@ -48,17 +50,16 @@ When the user asks to create, add, or build a new React topic or visualization. 10. **Add theory description** in `src/lib/metadata.ts` under `TOPIC_THEORY_DESCRIPTIONS`. -11. **Verify:** Run `npm run build`. Fix any errors. +11. **Verify:** Run `npm run audit:steps -- ` and fix anything it flags, then `npm run verify`. ## Key Files - `src/lib/topics.ts` -- topic registry - `src/lib/metadata.ts` -- keywords and theory descriptions -- `src/app/react//page.tsx` -- route page (new) +- `src/components/visualizations/registry.tsx` -- component registry - `src/components/visualizations//` -- visualization folder (new) - `src/content/theory/react/.ts` -- theory content (new) - `src/content/theory/index.ts` -- theory registry -- `src/components/layout/VisualizationPageShell.tsx` -- SELECTOR_TOOLBAR_TOPIC_IDS - `src/components/visualizations/reconciliation/` -- reference implementation ## Documentation @@ -71,7 +72,7 @@ When the user asks to create, add, or build a new React topic or visualization. ## NeonPanel Tone Conventions | Panel | Tone | -|----------------|----------| +| -------------- | -------- | | Source Code | `amber` | | Previous Tree | `cyan` | | New Tree | `green` | @@ -83,5 +84,5 @@ When the user asks to create, add, or build a new React topic or visualization. - Use `ComponentTreeDiagram` for tree visualizations (auto-scales, no horizontal scroll) - No em dashes or AI-sounding language in content - No emojis in step descriptions or UI text -- Use `handleExampleChange` callback wrapper +- Use `useExampleTopic` + `VisualizationToolbar` + `ExamplePicker`; never hand-roll the toolbar or step pill - Theory `relatedTopicIds` must have 3-5 valid IDs (never empty) diff --git a/.claude/skills/visualizejs-conventions/SKILL.md b/.claude/skills/visualizejs-conventions/SKILL.md index c7ab39c..8dec7b1 100644 --- a/.claude/skills/visualizejs-conventions/SKILL.md +++ b/.claude/skills/visualizejs-conventions/SKILL.md @@ -8,40 +8,48 @@ Quick reference for project conventions and file locations. Read the full docs f ## Key Files -- **Topic registry:** `src/lib/topics.ts` (single source of truth for all topics) -- **Theory registry:** `src/content/theory/index.ts` -- **SEO metadata:** `src/lib/metadata.ts` (TOPIC_KEYWORDS, TOPIC_THEORY_DESCRIPTIONS) -- **Page shell:** `src/components/layout/VisualizationPageShell.tsx` (SELECTOR_TOOLBAR_TOPIC_IDS) +- **Category registry:** `src/lib/categories.ts` (javascript, react, frameworks, backend, ai) +- **Topic registry:** `src/lib/topics.ts` (single source of truth; exports the `TopicId` union; routes derived from category and id) +- **Component registry:** `src/components/visualizations/registry.tsx` (`VISUALIZATIONS`, typed by `TopicId`) +- **Routes:** `src/app/[category]/[topic]/page.tsx` and `src/app/[category]/page.tsx` (generated from the registries, never add per-topic pages) +- **Theory registry:** `src/content/theory/index.ts` (typed by `TopicId`) +- **SEO metadata:** `src/lib/metadata.ts` (TOPIC_KEYWORDS, TOPIC_THEORY_DESCRIPTIONS, typed by `TopicId`) +- **Page shell:** `src/components/layout/VisualizationPageShell.tsx` +- **Topic scaffold:** `src/components/visualization-ui/VisualizationToolbar.tsx`, `SourceCodePanel.tsx`, `ExamplePicker.tsx`, `VisualizationLayout.tsx` - **UI strings:** `src/lib/visualization/uiCopy.ts` (VISUALIZATION_PANEL_TITLES, VISUALIZATION_EMPTY_STATES) -- **Playback engine:** `src/hooks/useStepPlayback.ts` -- **Change detection:** `src/hooks/useChangeFlash.ts` -- **Theory types:** `src/content/theory/types.ts` +- **Hooks:** `src/hooks/useExampleTopic.ts` (selector topics), `src/hooks/useStepPlayback.ts` (playback, shortcuts, progress), `src/hooks/useChangeFlash.ts` +- **Progress:** `src/lib/progress/topicProgress.ts`, `src/hooks/useTopicProgress.ts` +- **Search:** `src/components/search/CommandPalette.tsx` +- **Shared types:** `src/types/visualization.ts` (BaseStep, SourceExample, SourceLine), `src/content/theory/types.ts` ## Non-Negotiable Rules 1. No em dashes or AI-sounding language in user-facing content 2. No emojis in step descriptions or UI text 3. Use `VISUALIZATION_PANEL_TITLES` / `VISUALIZATION_EMPTY_STATES` from `uiCopy.ts` (never hardcode) -4. Wrap `setActiveExampleId` in a `handleExampleChange` callback -5. Add ExampleSelector topics to `SELECTOR_TOOLBAR_TOPIC_IDS` in `VisualizationPageShell.tsx` -6. Theory `relatedTopicIds` must have 3-5 valid IDs (never empty) -7. Code line fading guard: `isDone && !isActive` (never fade active lines) +4. Selector topics use `useExampleTopic` + `VisualizationToolbar` + `ExamplePicker`; never hand-roll the toolbar or step pill +5. Source code renders through `SourceCodePanel`; it owns the `isDone && !isActive` fading guard +6. Topics without an example picker set `toolbar: "simple"` in `src/lib/topics.ts` +7. Theory `relatedTopicIds` must have 3-5 valid IDs (never empty) 8. Components use `"use client"` directive and named exports -9. React components use const form, not function declarations +9. React components use const arrow form; only Next.js `page.tsx`/`layout.tsx` use `export default function` 10. Component files target 200 lines, hard limit 300 +11. State updaters stay pure: no setter calls inside `setState(prev => ...)` +12. Never branch on category IDs; read `CATEGORIES[topic.category]` +13. Run `npm run format`; CI checks Prettier ## Topic Patterns -- **Single-file:** `src/components/visualizations/TopicName.tsx` (legacy JS) -- **Folder-based:** `src/components/visualizations//` with `index.tsx`, `types.ts`, `helpers.ts`, `data.ts`, `components/` (React, complex JS) -- **Reference implementations:** Reconciliation (React folder pattern), Event Loop (sandbox) +- **Folder-based (every topic):** `src/components/visualizations//` with `index.tsx`, `types.ts`, `helpers.ts`, `data.ts`, `components/` +- **Reference implementations:** Reconciliation (selector topic), Closures (no selector), Event Loop (sandbox), Reference vs Value (two selection axes), Attention and HTTP Request Lifecycle (PipelineDiagram, HeatmapGrid, MessageFlow) ## Full Documentation -- `docs/topic-authoring.md` -- JS topic creation workflow +- `docs/topic-authoring.md` -- topic creation workflow - `docs/react-topic-authoring.md` -- React topic extensions - `docs/component-reference.md` -- design system, components, hooks, animations - `docs/theory-authoring.md` -- theory page authoring - `docs/sandbox-authoring.md` -- sandbox mode guide - `docs/architecture.md` -- frontend architecture rules +- `docs/categories.md` -- category registry - `docs/seo.md` -- SEO implementation diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 81258a5..28b6171 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,21 +14,22 @@ - [ ] `npm run lint` passes - [ ] `npm run build` passes -- [ ] `npm run check:registries` passes +- [ ] `npm run verify` passes (lint, typecheck, format, build) ## For a new topic Confirm each registry was updated, or delete this section. -- [ ] `src/lib/topics.ts` -- [ ] Route at `src/app///page.tsx` +- [ ] `src/lib/topics.ts` (set `toolbar: "simple"` if the topic has no example picker) - [ ] Visualization at `src/components/visualizations//` +- [ ] `VISUALIZATIONS` in `src/components/visualizations/registry.tsx` - [ ] Theory content at `src/content/theory//.ts` - [ ] `THEORY_CONTENT_BY_TOPIC_ID` in `src/content/theory/index.ts` - [ ] `TOPIC_KEYWORDS` and `TOPIC_THEORY_DESCRIPTIONS` in `src/lib/metadata.ts` -- [ ] `SELECTOR_TOOLBAR_TOPIC_IDS` if the topic uses `ExampleSelector` - [ ] Added to the `relatedTopicIds` of 2 or more existing theory files +The registries are typed by `TopicId`, so a missing entry fails `npm run typecheck`. + ## Screenshots diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9114ee..89817e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: verify: - name: Lint and build + name: Lint, test, and build runs-on: ubuntu-latest steps: @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm - name: Install dependencies @@ -29,19 +29,17 @@ jobs: - name: Lint run: npm run lint - - name: Build - run: npm run build + - name: Type check + run: npm run typecheck - registry-check: - name: Registry integrity - runs-on: ubuntu-latest + - name: Format check + run: npm run format:check - steps: - - uses: actions/checkout@v4 + - name: Test + run: npm run test - - uses: actions/setup-node@v4 - with: - node-version: 20 + - name: Audit step data + run: npm run audit:steps - - name: Verify every topic is fully registered - run: node scripts/check-registries.mjs + - name: Build + run: npm run build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..325734a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +.next +node_modules +out +build +package-lock.json +tsconfig.tsbuildinfo +public +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f52db5f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 80 +} diff --git a/CLAUDE.md b/CLAUDE.md index 572d1b0..7365c7d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,25 +1,37 @@ # VisualizeJS -Interactive JavaScript and React concept visualizations. Next.js 16, React 19, TypeScript strict, Tailwind CSS v4, shadcn/ui. +Interactive visualizations of JavaScript, React, framework (Vue, Svelte, Angular), backend, and AI internals. Next.js 16, React 19, TypeScript strict, Tailwind CSS v4, shadcn/ui. ## Build Commands - `npm run dev` -- dev server (webpack mode) - `npm run build` -- production build (webpack mode) - `npm run lint` -- ESLint +- `npm run typecheck` -- TypeScript (also validates every topic registry) +- `npm run format` / `npm run format:check` -- Prettier +- `npm run test` -- Vitest unit and component tests (`npm run test:watch` for watch mode, `npm run test:coverage` for a coverage report) +- `npm run audit:steps` -- checks every step's activeLine and doneLines against its code (`-- ` to narrow, `-- --strict` for extra hints) +- `npm run verify` -- lint, typecheck, format check, tests, step audit, build (what CI runs) - `npm run start` -- serve production build ## Project Structure -- `src/lib/topics.ts` -- topic registry (single source of truth for all topics) -- `src/content/theory/index.ts` -- theory content registry -- `src/lib/metadata.ts` -- SEO metadata factory, keyword maps, theory descriptions -- `src/components/layout/VisualizationPageShell.tsx` -- page shell + SELECTOR_TOOLBAR_TOPIC_IDS -- `src/components/visualization-ui/` -- shared viz primitives (NeonPanel, CodeBlock, TransportControls, ExampleSelector, ComponentTreeDiagram) -- `src/hooks/useStepPlayback.ts` -- shared playback engine +- `src/lib/categories.ts` -- category registry (labels, routes, colours, SEO). Never branch on category IDs in components +- `src/lib/topics.ts` -- topic registry and the `TopicId` union derived from it. Routes are derived from category and id +- `src/components/visualizations/registry.tsx` -- `VISUALIZATIONS`, lazy component per topic, typed by `TopicId` +- `src/app/[category]/[topic]/page.tsx` -- the one topic route; `src/app/[category]/page.tsx` -- category index +- `src/content/theory/index.ts` -- theory content registry, typed by `TopicId` +- `src/lib/metadata.ts` -- SEO metadata factory, keyword maps, theory descriptions, typed by `TopicId` +- `src/components/layout/VisualizationPageShell.tsx` -- page shell (header, toolbar slot, theory, pager) +- `src/components/visualization-ui/` -- shared viz primitives (VisualizationToolbar, SourceCodePanel, ExamplePicker, ExplanationToggle, FlowConnector, VisualizationLayout, NeonPanel, CodeBlock, TransportControls, ComponentTreeDiagram, PipelineDiagram, TokenChips, HeatmapGrid, MetricBars, MessageFlow) +- `src/hooks/useExampleTopic.ts` -- example selection + playback + current step for selector topics +- `src/hooks/useStepPlayback.ts` -- playback engine (keyboard shortcuts, marks topic progress) - `src/hooks/useChangeFlash.ts` -- panel change detection +- `src/lib/progress/topicProgress.ts` -- localStorage completion tracking +- `src/components/search/CommandPalette.tsx` -- Cmd+K topic search - `src/lib/visualization/uiCopy.ts` -- shared UI strings (VISUALIZATION_PANEL_TITLES, VISUALIZATION_EMPTY_STATES) -- `src/lib/sandbox/` -- sandbox mode infrastructure +- `src/lib/sandbox/` -- sandbox mode infrastructure; `src/components/sandbox/` -- sandbox UI +- `src/types/visualization.ts` -- BaseStep, SourceExample, SourceLine, tree types - `src/content/theory/types.ts` -- TopicTheoryContent interface ## Non-Negotiable Conventions @@ -27,19 +39,25 @@ Interactive JavaScript and React concept visualizations. Next.js 16, React 19, T - No em dashes or AI-sounding language in user-facing content - No emojis in step descriptions or UI text - Use `VISUALIZATION_PANEL_TITLES` and `VISUALIZATION_EMPTY_STATES` from `uiCopy.ts` (never hardcode panel titles) -- Wrap `setActiveExampleId` in a `handleExampleChange` callback -- Add ExampleSelector topics to `SELECTOR_TOOLBAR_TOPIC_IDS` in `VisualizationPageShell.tsx` +- Selector topics use `useExampleTopic(EXAMPLES)` and render `VisualizationToolbar` with an `ExamplePicker` in `leading`; do not hand-roll the toolbar, step pill, or source panel +- Source code goes through `SourceCodePanel` (it owns the `isDone && !isActive` fading guard) +- Topics without an example picker set `toolbar: "simple"` in `src/lib/topics.ts`; topics listed under a sub-heading set `group` +- AI topics carry `simpleHtml` on every step (plain-language version); keep it in sync when editing `descriptionHtml` +- Every topic needs an entry in `VISUALIZATIONS`, `THEORY_CONTENT_BY_TOPIC_ID`, `TOPIC_KEYWORDS`, and `TOPIC_THEORY_DESCRIPTIONS`; `npm run typecheck` fails otherwise - Theory `relatedTopicIds` must be populated (3-5 IDs, never empty) -- Code line fading guard: `isDone && !isActive` (never fade active lines) -- React components use const form (`const Component = () =>`), not function declarations +- React components use const arrow form (`const Component = () =>`); Next.js `page.tsx` and `layout.tsx` keep `export default function` +- State updaters stay pure: never call another setter inside a `setState(prev => ...)` callback - Component files target 200 lines or fewer, hard limit at 300 - Visualization components use `"use client"` directive and named exports +- Run `npm run format` before finishing; CI checks formatting +- Tests live next to the code as `*.test.ts(x)`; cross-topic convention checks live in `src/test/conventions/`. New library code, hooks, and shared primitives get tests; run `npm run test` before finishing ## Topic Patterns -- **Single-file:** `src/components/visualizations/TopicName.tsx` (legacy JS topics: EventLoop, Hoisting, Closures, Promises, TypeCoercion) -- **Folder-based:** `src/components/visualizations//` with `index.tsx`, `types.ts`, `helpers.ts`, `data.ts`, `components/` (all React topics, most newer JS topics) -- **Reference implementations:** Reconciliation (React folder pattern), Event Loop (sandbox) +- **Folder-based:** `src/components/visualizations//` with `index.tsx`, `types.ts`, `helpers.ts`, `data.ts`, `components/`. Every topic uses this shape +- **Step type:** topic step interfaces extend `BaseStep` from `src/types/visualization.ts` +- **Reference implementations:** Reconciliation (selector topic), Closures (no selector), Event Loop (sandbox), Reference vs Value (two selection axes), Attention and HTTP Request Lifecycle (newer primitives) +- **Categories:** javascript, react, frameworks, backend, ai. Theory lives in `src/content/theory//`; routes are generated from the registry ## Documentation @@ -49,4 +67,6 @@ Interactive JavaScript and React concept visualizations. Next.js 16, React 19, T - `docs/theory-authoring.md` -- theory page authoring - `docs/sandbox-authoring.md` -- sandbox mode guide - `docs/architecture.md` -- frontend architecture rules +- `docs/categories.md` -- category registry and how to add a category - `docs/seo.md` -- SEO implementation +- `docs/testing.md` -- test setup, what is covered, and how to add tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad77255..2c58672 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to VisualizeJS -Thanks for wanting to help. This project explains JavaScript and React internals through animations, so contributions range from fixing a typo in a theory page to building a whole new topic. +Thanks for wanting to help. This project explains JavaScript, React, framework, backend, and AI internals through animations, so contributions range from fixing a typo in a theory page to building a whole new topic. ## Ways to Contribute @@ -21,15 +21,14 @@ npm run dev Node 20 or newer. Before opening a pull request: ```bash -npm run lint -npm run build +npm run verify ``` Both must pass. CI runs the same two commands. ## Project Conventions -These are enforced in review. Most exist because inconsistency between 28 topics is more expensive than it looks. +These are enforced in review. Most exist because inconsistency between 45 topics is more expensive than it looks. **Content** @@ -44,7 +43,7 @@ These are enforced in review. Most exist because inconsistency between 28 topics - Visualization components need the `"use client"` directive and named exports. - Component files target 200 lines, hard limit 300. Split into `components/` when you exceed it. Data files (`data.ts`) are exempt since they hold content. - Import panel titles and empty states from `src/lib/visualization/uiCopy.ts`. Never hardcode them. -- Wrap `setActiveExampleId` in a `handleExampleChange` callback rather than passing the setter to `onSelect` directly. +- Topics with several examples use `useExampleTopic`, `VisualizationToolbar`, and `ExamplePicker`; do not hand-roll the toolbar or step pill. - Code line fading uses the guard `isDone && !isActive`. Never fade an active line. **Structure** @@ -59,23 +58,23 @@ helpers.ts Pure functions used by the panels components/ Panel components ``` -Reconciliation is the reference implementation for React topics. Event Loop is the reference for sandbox mode. +Reconciliation is the reference implementation for topic structure. Event Loop is the reference for sandbox mode. Attention and HTTP Request Lifecycle show the newer shared primitives (HeatmapGrid, PipelineDiagram, MessageFlow). ## Adding a Topic -A topic is not finished when the visualization renders. It is registered in several places, and missing one produces a page that half works. Work through all of these. +A topic is registered in several places. The registries are typed by `TopicId`, which is derived from `src/lib/topics.ts`, so once the topic is registered `npm run typecheck` names every place that still needs an entry. **1. Topic registry** in [`src/lib/topics.ts`](src/lib/topics.ts) -Add an entry with `id`, `title`, `category`, `route`, `description`, `difficulty`, and `docsUrl`. The `id` is the slug used everywhere else, so pick it carefully. +Add an entry with `id`, `title`, `category`, `description`, `difficulty`, and `docsUrl`. The route is derived from category and id. The `id` is the slug used everywhere else, so pick it carefully. `category` must be one of the IDs in `src/lib/categories.ts` (see [`docs/categories.md`](docs/categories.md) to add a new category). Set `toolbar: "simple"` if the topic has no example picker, so the loading skeleton matches. -**2. Route** at `src/app///page.tsx` +**2. Component registry** in [`src/components/visualizations/registry.tsx`](src/components/visualizations/registry.tsx) -Copy an existing page. It resolves the topic with `getTopicOrThrow`, lazy-loads the visualization with `next/dynamic`, wraps it in `ErrorBoundary` and `VisualizationPageShell`, and exports `createTopicMetadata(topic)`. +Add one `dynamic()` entry keyed by the topic id. Routing is generated from the registries by `src/app/[category]/[topic]/page.tsx`; there is no per-topic page file. **3. Visualization** at `src/components/visualizations//` -Use `useStepPlayback` from `src/hooks/useStepPlayback.ts` for transport controls. Read [`docs/topic-authoring.md`](docs/topic-authoring.md), or [`docs/react-topic-authoring.md`](docs/react-topic-authoring.md) for React topics. +Use `useExampleTopic`, `VisualizationToolbar`, `ExamplePicker`, and `SourceCodePanel` from the shared scaffold. Read [`docs/topic-authoring.md`](docs/topic-authoring.md), or [`docs/react-topic-authoring.md`](docs/react-topic-authoring.md) for React topics. **4. Theory content** at `src/content/theory//.ts` @@ -85,25 +84,20 @@ Implements `TopicTheoryContent`: `summary`, `whatItIs`, `howItWorks`, `commonMis **5. Theory registry** in [`src/content/theory/index.ts`](src/content/theory/index.ts) -Add the import and the entry in `THEORY_CONTENT_BY_TOPIC_ID`. `VisualizationPageShell` reads this map to render the theory sections beneath the visualization, so a topic missing here renders without any theory. +Add the import and the entry in `THEORY_CONTENT_BY_TOPIC_ID`. `VisualizationPageShell` reads this map to render the theory sections beneath the visualization. **6. SEO metadata** in [`src/lib/metadata.ts`](src/lib/metadata.ts) Add entries to both `TOPIC_KEYWORDS` (4 to 8 search phrases) and `TOPIC_THEORY_DESCRIPTIONS` (one or two sentences, under 160 characters). -**7. Toolbar registry** in [`src/components/layout/VisualizationPageShell.tsx`](src/components/layout/VisualizationPageShell.tsx) - -If your topic uses `ExampleSelector`, add its ID to `SELECTOR_TOOLBAR_TOPIC_IDS`. Skipping this makes the loading skeleton the wrong shape. - -**8. Inbound links** +**7. Inbound links** Add your topic ID to the `relatedTopicIds` of 2 or more existing theory files. A topic nothing links to is a dead end for readers and for search engines. Keep each list within the 3 to 5 range when you do this, swapping out a weaker link if needed. ### Before You Open the PR ```bash -npm run lint -npm run build +npm run verify ``` Then check that `/your-route` renders with the theory sections below the visualization, the sitemap at `/sitemap.xml` includes the route, and the related topic links at the bottom of the page resolve. diff --git a/README.md b/README.md index cc9bcb8..526fd54 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # VisualizeJS -[![Live site](https://img.shields.io/badge/live-visualizejs.com-3fb950?style=flat-square&labelColor=24292f)](https://visualizejs.com) [![CI](https://img.shields.io/github/actions/workflow/status/tornike14/visualizejs/ci.yml?branch=main&style=flat-square&label=CI&labelColor=24292f)](https://github.com/tornike14/visualizejs/actions/workflows/ci.yml) [![Topics](https://img.shields.io/badge/topics-28-a371f7?style=flat-square&labelColor=24292f)](#topics) [![Next.js](https://img.shields.io/badge/Next.js-16-000000?style=flat-square&logo=nextdotjs&logoColor=white&labelColor=24292f)](https://nextjs.org) [![React](https://img.shields.io/badge/React-19-61DAFB?style=flat-square&logo=react&logoColor=61DAFB&labelColor=24292f)](https://react.dev) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white&labelColor=24292f)](https://www.typescriptlang.org) [![License](https://img.shields.io/github/license/tornike14/visualizejs?style=flat-square&color=3fb950&labelColor=24292f)](LICENSE) +[![Live site](https://img.shields.io/badge/live-visualizejs.com-3fb950?style=flat-square&labelColor=24292f)](https://visualizejs.com) [![CI](https://img.shields.io/github/actions/workflow/status/tornike14/visualizejs/ci.yml?branch=main&style=flat-square&label=CI&labelColor=24292f)](https://github.com/tornike14/visualizejs/actions/workflows/ci.yml) [![Topics](https://img.shields.io/badge/topics-45-a371f7?style=flat-square&labelColor=24292f)](#topics) [![Next.js](https://img.shields.io/badge/Next.js-16-000000?style=flat-square&logo=nextdotjs&logoColor=white&labelColor=24292f)](https://nextjs.org) [![React](https://img.shields.io/badge/React-19-61DAFB?style=flat-square&logo=react&logoColor=61DAFB&labelColor=24292f)](https://react.dev) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white&labelColor=24292f)](https://www.typescriptlang.org) [![License](https://img.shields.io/github/license/tornike14/visualizejs?style=flat-square&color=3fb950&labelColor=24292f)](LICENSE) -Interactive visualizations for JavaScript and React concepts. Step through animations that show what the engine does at each stage, then read the theory behind it. +Interactive visualizations of how JavaScript, React, Vue, Svelte, Angular, backend systems, and AI models work under the hood. Step through animations that show what the engine does at each stage, then read the theory behind it. -28 topics. Each one pairs a step-by-step visualization with theory sections covering how it works, common mistakes, and interview questions, all on a single page. +45 topics across five categories. Each one pairs a step-by-step visualization with theory sections covering how it works, common mistakes, and interview questions, all on a single page. [![VisualizeJS homepage](docs/images/homepage.jpg)](https://visualizejs.com) @@ -21,54 +21,99 @@ Node 20 or newer is required. ## Scripts -| Command | Description | -|---|---| -| `npm run dev` | Start dev server (webpack mode) | -| `npm run build` | Production build | -| `npm run start` | Serve the production build | -| `npm run lint` | Run ESLint | +| Command | Description | +| ------------------- | ---------------------------------------------------- | +| `npm run dev` | Start dev server (webpack mode) | +| `npm run build` | Production build | +| `npm run start` | Serve the production build | +| `npm run lint` | Run ESLint | +| `npm run typecheck` | Run TypeScript (also validates every topic registry) | +| `npm run format` | Format with Prettier (`format:check` in CI) | +| `npm run verify` | Lint, typecheck, format check, and build | ## Topics -Each topic lives at a single route, with the visualization at the top and the theory sections below it. - -### JavaScript (17) - -| Topic | Difficulty | -|---|---| -| [Event Loop](/javascript/event-loop) | Intermediate | -| [Hoisting](/javascript/hoisting) | Beginner | -| [Execution Context](/javascript/execution-context) | Intermediate | -| [Closures](/javascript/closures) | Intermediate | -| [Promises](/javascript/promises) | Intermediate | -| [this Keyword](/javascript/this-keyword) | Intermediate | -| [Scope Chain](/javascript/scope-chain) | Intermediate | -| [Type Coercion](/javascript/type-coercion) | Beginner | -| [Destructuring](/javascript/destructuring) | Beginner | -| [Spread & Rest](/javascript/spread-rest) | Beginner | -| [Prototypal Inheritance](/javascript/prototypal-inheritance) | Advanced | -| [Reference vs Value](/javascript/reference-value) | Beginner | -| [Heap & Stack](/javascript/heap-stack) | Advanced | -| [Garbage Collection](/javascript/garbage-collection) | Advanced | -| [Generators & Iterators](/javascript/generators) | Advanced | -| [Event Delegation](/javascript/event-delegation) | Beginner | -| [Modules & Imports](/javascript/modules-imports) | Intermediate | - -### React (11) - -| Topic | Difficulty | -|---|---| -| [Virtual DOM](/react/virtual-dom) | Beginner | -| [Reconciliation](/react/reconciliation) | Intermediate | -| [Context Propagation](/react/context-propagation) | Intermediate | -| [Fiber Tree](/react/fiber-tree) | Advanced | -| [Hooks](/react/hooks) | Intermediate | -| [Render Cycle](/react/render-cycle) | Advanced | -| [Memoization](/react/memoization) | Intermediate | -| [Suspense](/react/suspense) | Intermediate | -| [Server Components](/react/server-components) | Advanced | -| [Error Boundaries](/react/error-boundaries) | Intermediate | -| [useEffect Lifecycle](/react/use-effect-lifecycle) | Beginner | +Each topic lives at a single route, with the visualization at the top and the theory sections below it. Press `Cmd+K` (or `/`) anywhere to search topics. Space plays and pauses, the arrow keys step, `R` resets, and the scrubber next to the step counter jumps to any step. Topics you finish get a check mark, stored in your browser. + +### JavaScript (19) + +Runtime internals: the event loop, scope, memory, and async. + +| Topic | Difficulty | +| ------------------------------------------------------------ | ------------ | +| [Event Loop](/javascript/event-loop) | Intermediate | +| [Hoisting](/javascript/hoisting) | Beginner | +| [Execution Context](/javascript/execution-context) | Intermediate | +| [Closures](/javascript/closures) | Intermediate | +| [Promises](/javascript/promises) | Intermediate | +| [this Keyword](/javascript/this-keyword) | Intermediate | +| [Scope Chain](/javascript/scope-chain) | Intermediate | +| [Type Coercion](/javascript/type-coercion) | Beginner | +| [Destructuring](/javascript/destructuring) | Beginner | +| [Spread & Rest](/javascript/spread-rest) | Beginner | +| [Prototypal Inheritance](/javascript/prototypal-inheritance) | Advanced | +| [Reference vs Value](/javascript/reference-value) | Beginner | +| [Heap & Stack](/javascript/heap-stack) | Advanced | +| [Garbage Collection](/javascript/garbage-collection) | Advanced | +| [Generators & Iterators](/javascript/generators) | Advanced | +| [Event Delegation](/javascript/event-delegation) | Beginner | +| [Modules & Imports](/javascript/modules-imports) | Intermediate | +| [Async/Await](/javascript/async-await) | Intermediate | +| [Debounce & Throttle](/javascript/debounce-throttle) | Beginner | + +### React (13) + +How React decides what to render and what it commits to the DOM. + +| Topic | Difficulty | +| --------------------------------------------------- | ------------ | +| [Virtual DOM](/react/virtual-dom) | Beginner | +| [Reconciliation](/react/reconciliation) | Intermediate | +| [Context Propagation](/react/context-propagation) | Intermediate | +| [Fiber Tree](/react/fiber-tree) | Advanced | +| [Hooks](/react/hooks) | Intermediate | +| [Render Cycle](/react/render-cycle) | Advanced | +| [Memoization](/react/memoization) | Intermediate | +| [Suspense](/react/suspense) | Intermediate | +| [Server Components](/react/server-components) | Advanced | +| [Error Boundaries](/react/error-boundaries) | Intermediate | +| [useEffect Lifecycle](/react/use-effect-lifecycle) | Beginner | +| [State Batching](/react/state-batching) | Intermediate | +| [Concurrent Rendering](/react/concurrent-rendering) | Advanced | + +### Frameworks (3) + +How Vue, Svelte, and Angular track changes, compared side by side. + +| Topic | Difficulty | +| ---------------------------------------------------------------- | ------------ | +| [Vue Reactivity](/frameworks/vue-reactivity) | Intermediate | +| [Svelte Runes](/frameworks/svelte-runes) | Intermediate | +| [Angular Change Detection](/frameworks/angular-change-detection) | Advanced | + +### Backend (5) + +What happens between a request leaving the browser and the response. + +| Topic | Difficulty | +| --------------------------------------------------------- | ------------ | +| [HTTP Request Lifecycle](/backend/http-request-lifecycle) | Beginner | +| [Database Indexing](/backend/database-indexing) | Intermediate | +| [Caching Strategies](/backend/caching-strategies) | Intermediate | +| [JWT Authentication](/backend/jwt-authentication) | Intermediate | +| [Rate Limiting](/backend/rate-limiting) | Intermediate | + +### AI (5) + +What a language model does with your text, from tokens to the next word. + +| Topic | Difficulty | +| -------------------------------------------------- | ------------ | +| [Tokenization](/ai/tokenization) | Beginner | +| [Embeddings](/ai/embeddings) | Beginner | +| [Attention](/ai/attention) | Intermediate | +| [Next Token Prediction](/ai/next-token-prediction) | Intermediate | +| [Backpropagation](/ai/backpropagation) | Advanced | Some topics also have a sandbox mode where you can edit the code and watch the visualization respond. Event Loop is the reference implementation. @@ -76,30 +121,30 @@ Some topics also have a sandbox mode where you can edit the code and watch the v ``` src/ - app/ Next.js App Router routes + app/[category]/ Generated category and topic routes components/ layout/ Page shell, navigation - visualization-ui/ Shared primitives (NeonPanel, CodeBlock, TransportControls) - visualizations/ One folder per topic + visualization-ui/ Shared primitives (VisualizationToolbar, SourceCodePanel, NeonPanel, ...) + visualizations/ One folder per topic, plus registry.tsx content/theory/ Theory content, one file per topic - hooks/ useStepPlayback, useChangeFlash + hooks/ useExampleTopic, useStepPlayback, useChangeFlash lib/ topics.ts Topic registry, the single source of truth metadata.ts SEO metadata factory sandbox/ Sandbox mode infrastructure ``` -Adding a topic touches several registries. [`CONTRIBUTING.md`](CONTRIBUTING.md) lists all of them. +Adding a topic touches several registries, all typed by `TopicId` so a missing entry fails the type check. [`CONTRIBUTING.md`](CONTRIBUTING.md) lists them. ## Environment Variables All are optional and have working defaults. Copy [`.env.example`](.env.example) to `.env.local` to override. -| Variable | Default | Purpose | -|---|---|---| -| `NEXT_PUBLIC_SITE_URL` | `https://visualizejs.com` | Canonical URLs, sitemap, Open Graph tags | -| `NEXT_PUBLIC_CREATOR_LINKEDIN_URL` | Maintainer's profile | Credit link in the footer | -| `NEXT_PUBLIC_CREATOR_AVATAR_SRC` | `/personal-image.png` | Avatar in the footer | +| Variable | Default | Purpose | +| ---------------------------------- | ------------------------- | ---------------------------------------- | +| `NEXT_PUBLIC_SITE_URL` | `https://visualizejs.com` | Canonical URLs, sitemap, Open Graph tags | +| `NEXT_PUBLIC_CREATOR_LINKEDIN_URL` | Maintainer's profile | Credit link in the footer | +| `NEXT_PUBLIC_CREATOR_AVATAR_SRC` | `/personal-image.png` | Avatar in the footer | If you fork this, set the last two to your own. @@ -109,15 +154,16 @@ Read [`CONTRIBUTING.md`](CONTRIBUTING.md) first. New topics are welcome, and the ## Documentation -| Doc | Covers | -|---|---| -| [`docs/topic-authoring.md`](docs/topic-authoring.md) | JavaScript topic creation workflow | -| [`docs/react-topic-authoring.md`](docs/react-topic-authoring.md) | React topic extensions | -| [`docs/component-reference.md`](docs/component-reference.md) | Design system, components, hooks, animations | -| [`docs/theory-authoring.md`](docs/theory-authoring.md) | Theory content authoring | -| [`docs/sandbox-authoring.md`](docs/sandbox-authoring.md) | Sandbox mode | -| [`docs/architecture.md`](docs/architecture.md) | Frontend architecture rules | -| [`docs/seo.md`](docs/seo.md) | SEO implementation | +| Doc | Covers | +| ---------------------------------------------------------------- | -------------------------------------------- | +| [`docs/topic-authoring.md`](docs/topic-authoring.md) | JavaScript topic creation workflow | +| [`docs/react-topic-authoring.md`](docs/react-topic-authoring.md) | React topic extensions | +| [`docs/component-reference.md`](docs/component-reference.md) | Design system, components, hooks, animations | +| [`docs/theory-authoring.md`](docs/theory-authoring.md) | Theory content authoring | +| [`docs/sandbox-authoring.md`](docs/sandbox-authoring.md) | Sandbox mode | +| [`docs/architecture.md`](docs/architecture.md) | Frontend architecture rules | +| [`docs/categories.md`](docs/categories.md) | Category registry and adding a category | +| [`docs/seo.md`](docs/seo.md) | SEO implementation | ## Tech Stack diff --git a/docs/architecture.md b/docs/architecture.md index 09e49a6..329e6c2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -25,6 +25,7 @@ src/components// ``` Rules: + - Keep rendering logic in `.tsx` components. - Keep pure helpers in `.ts` files. - Keep static/config data in dedicated `*-data.ts` files. @@ -36,6 +37,7 @@ Rules: If behavior can be reused across features, move it to `src/components/ui`. Examples: + - carousel primitives - dropdown primitives - repeated navigation controls diff --git a/docs/categories.md b/docs/categories.md new file mode 100644 index 0000000..155d90f --- /dev/null +++ b/docs/categories.md @@ -0,0 +1,79 @@ +# Categories + +Topics are grouped into categories. Everything that depends on a category (label, route, colours, hero copy, SEO keywords, sidebar toggle, landing page sections, sitemap) reads from one registry. + +**File:** `src/lib/categories.ts` + +## Current Categories + +| ID | Label | Route | Accent | +| ------------ | ---------- | ------------- | ------- | +| `javascript` | JavaScript | `/javascript` | yellow | +| `react` | React | `/react` | cyan | +| `frameworks` | Frameworks | `/frameworks` | emerald | +| `backend` | Backend | `/backend` | violet | +| `ai` | AI | `/ai` | pink | + +`CATEGORY_ORDER` controls the order in the sidebar toggle, the landing page, and the sitemap. + +## CategoryConfig Fields + +| Field | Used by | +| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `label`, `headingLabel` | Page titles, sidebar, breadcrumbs, structured data | +| `route` | Links, sitemap, `categoryFromPathname` | +| `tagline`, `description`, `kicker`, `indexTitle` | Landing page sections, category hero, category metadata | +| `iconSrc`, `iconAlt`, `iconShellClass` | Sidebar toggle, landing page, hero | +| `docsLabel` | The external docs link label in the theory section ("on MDN", "on react.dev") | +| `titleAccentClass`, `kickerClass`, `toggleActiveClass`, `badgeClass`, `ctaClass`, `glowRgb`, `hero` | Colours. Keep one hue per category | +| `keywords` | Merged into every topic's SEO keywords in that category | +| `indexKeywords` | Keywords of the category index page | + +## Adding a Category + +1. Add the ID to the `Category` union in `src/types/index.ts`. +2. Add a `CategoryConfig` entry to `CATEGORIES` and the ID to `CATEGORY_ORDER` in `src/lib/categories.ts`. +3. Add an icon at `public/icons/.svg` (48x48 viewBox, single colour family). +4. Create `src/content/theory//` for theory files. +5. Add at least one topic (see `docs/topic-authoring.md`). Empty categories still render a hero with no cards. +6. Run `npm run verify`. + +Nothing else needs to change. The category page is generated by `src/app/[category]/page.tsx`, and the sidebar, search palette, landing page, sitemap, metadata, and page shell all iterate over `CATEGORY_LIST`. + +## Helpers + +```typescript +import { + CATEGORIES, + CATEGORY_LIST, + getCategory, + categoryFromPathname, +} from "@/lib/categories"; + +CATEGORIES.ai.label; // "AI" +CATEGORY_LIST.map((c) => c.route); // ["/javascript", "/react", ...] +categoryFromPathname("/backend/jwt-authentication"); // "backend" +``` + +Never branch on `category === "javascript"` in components. Read the field you need from the config instead. + +## Topic Groups + +A topic can carry `group: "Vue"` (any label) in `src/lib/topics.ts`. The category page and the sidebar then list topics under that heading, in first-appearance order, with ungrouped topics first. Frameworks uses this to keep Vue, Svelte, and Angular apart; the other categories are flat. + +## Frameworks Roadmap + +Frameworks has one topic per framework today. The category is meant to reach two or three per framework plus a few cross-framework comparisons. Candidates, in rough priority: + +| Group | Topic | What it shows | +| ---------- | -------------------------- | ------------------------------------------------------------------------------------------------ | +| Vue | Computed and Watchers | Lazy computed caching vs eager watchers, and when each re-runs | +| Vue | Template Compilation | A template becoming a render function with patch flags, and why block trees skip static nodes | +| Svelte | Stores and Context | Writable, derived, and readable stores, subscriptions, and how `$store` compiles | +| Svelte | Compiler Output | Side by side: one component, the generated `$.template` and effect code | +| Angular | Dependency Injection | Injector tree lookup, `providedIn`, and why the same token can resolve to different instances | +| Angular | RxJS in Templates | The `async` pipe, subscription lifetime, and change detection triggered by emissions | +| Solid | Fine-Grained Signals | createSignal and createEffect without a component re-render model, for contrast with React | +| Comparison | Reactivity Models Compared | The same counter in React, Vue, Svelte, Solid, and Angular, showing what re-executes on a change | + +Each one follows `docs/topic-authoring.md`. Add `group` to the registry entry so it lands under the right heading. diff --git a/docs/component-reference.md b/docs/component-reference.md index 203deef..70e828a 100644 --- a/docs/component-reference.md +++ b/docs/component-reference.md @@ -20,23 +20,23 @@ Design system, reusable visualization components, shared hooks, and animation sy ### Color Palette (CSS Variables) -| Variable | Value | Usage | -|---|---|---| -| `--app-bg` | `#090f1f` | Page background | -| `--app-surface` | `#0f1a30` | Card backgrounds | -| `--app-surface-strong` | `#0d1528` | Sidebar, stronger surfaces | -| `--app-border` | `rgba(71, 85, 105, 0.65)` | Borders | -| `--app-text-primary` | `#e2e8f0` | Primary text | -| `--app-text-secondary` | `#94a3b8` | Secondary/muted text | -| `--app-accent` | `#f472b6` | Pink accent | +| Variable | Value | Usage | +| ---------------------- | ------------------------- | -------------------------- | +| `--app-bg` | `#090f1f` | Page background | +| `--app-surface` | `#0f1a30` | Card backgrounds | +| `--app-surface-strong` | `#0d1528` | Sidebar, stronger surfaces | +| `--app-border` | `rgba(71, 85, 105, 0.65)` | Borders | +| `--app-text-primary` | `#e2e8f0` | Primary text | +| `--app-text-secondary` | `#94a3b8` | Secondary/muted text | +| `--app-accent` | `#f472b6` | Pink accent | ### Surface Classes -| Class | Use | -|---|---| -| `.app-surface` | Main cards - gradient bg, border, shadow, blur | +| Class | Use | +| --------------------- | ------------------------------------------------------ | +| `.app-surface` | Main cards - gradient bg, border, shadow, blur | | `.app-surface-subtle` | Lighter containers - controls row and explanation pill | -| `.app-surface-flat` | Minimal surface - inline code backgrounds | +| `.app-surface-flat` | Minimal surface - inline code backgrounds | ### Difficulty Badge Colors @@ -54,6 +54,100 @@ export const DIFFICULTY_COLORS = { ## Reusable Components +### Topic Scaffold + +Four components plus one hook carry the layout every step-driven topic shares. Use them instead of assembling `ToolbarPortal`, `TransportControls`, `NeonPanel`, and `CodeBlock` by hand. + +**`VisualizationToolbar`** (`src/components/visualization-ui/VisualizationToolbar.tsx`) + +Portals the transport controls and the step description pill into the page shell's toolbar slot. + +```tsx +} // optional: picker, badges, sandbox buttons + align="center" // optional: keep leading and transport together + hideTransport={isEditingSandbox} // optional + descriptionOverride={} // optional: replaces the pill content +/> +``` + +Pass `simpleHtml` and `hasSimpleText` (both from `useExampleTopic`) to enable the Simple / Detailed toggle; `ExplanationToggle` and `useExplanationMode` back it. `StepDescription` is exported from the same file for the rare case a topic needs the pill on its own. + +**`ExamplePicker`** (`src/components/visualization-ui/ExamplePicker.tsx`) + +`ExampleSelector` plus the active example's badge. `KindBadge` is the small outline badge used inside `renderBadge`. + +```tsx + ( + + {kindLabel(ex.kind)} + + )} +/> +``` + +**`SourceCodePanel`** (`src/components/visualization-ui/SourceCodePanel.tsx`) + +The amber Source Code panel. Derives line classes once per step and never fades the active line. + +```tsx + +``` + +**`VisualizationSection`, `SourceGrid`, `WaitingPlaceholder`** (`src/components/visualization-ui/VisualizationLayout.tsx`) + +The section wrapper below the toolbar, the two-column grid (source left, panels right at `xl`), and the "waiting" empty state for a panel before playback starts. + +**`useExampleTopic`** (`src/hooks/useExampleTopic.ts`) + +```tsx +const { example, activeExampleId, handleExampleChange, playback, currentStep } = + useExampleTopic(EXAMPLES); +``` + +Owns the active example, restarts playback when it changes, and types `currentStep` from the example's `steps`. + +**`FlowConnector`** (`src/components/visualization-ui/FlowConnector.tsx`) + +A dotted track between two nodes with glowing dots travelling along it while `active`. `PipelineDiagram` uses it on the edges next to the active stage and `MessageFlow` on the active message. Tone is one of `cyan`, `emerald`, `pink`, `amber`, `violet`, `slate`; size it with width or height classes. Respects `prefers-reduced-motion` (the dot sits still in the middle). + +```tsx + + +``` + +### CategoryTopicGrid / DifficultyFilter + +`src/components/layout/CategoryTopicGrid.tsx` renders a category's topic list +with a difficulty filter. The selected level lives in the `level` query +parameter (`?level=beginner`) so a filtered view can be shared, but the page +stays static: the server and first client render show every topic, and +`useLocationSearch` (`src/hooks/useLocationSearch.ts`) applies the URL value +right after hydration through `useSyncExternalStore`. Changing the filter +rewrites the query with `history.replaceState`, so it never adds history +entries. The pure helpers (`parseDifficultyParam`, `filterByDifficulty`, +`countByDifficulty`) live in `src/lib/difficulty.ts`. + +`DifficultyFilter` is a `radiogroup` of chips with a count per level. A level +with no topics in the category renders disabled rather than hidden, so the +set of chips is the same on every category page. Filtered cards keep their +registry index number so the "fundamentals to internals" order stays legible. + ### NeonPanel Themed container with a glowing dot header. @@ -161,7 +255,7 @@ const EXAMPLES: MyExample[] = [ ]; // In your component: -const [activeExampleId, setActiveExampleId] = useState(EXAMPLES[0].id); +const { activeExampleId, handleExampleChange } = useExampleTopic(EXAMPLES); void` | Called when the user picks a different example | +| Prop | Type | Description | +| ------------- | --------------------------- | ------------------------------------------------------------------- | +| `examples` | `T[]` | Array of example objects (must extend `ExampleOption`) | +| `activeId` | `string` | Currently selected example ID | +| `onSelect` | `(id: string) => void` | Called when the user picks a different example | | `renderBadge` | `(example: T) => ReactNode` | Optional render prop for a badge next to each title in the dropdown | -**Integration with playback:** When switching examples, reset playback using `useStepPlayback`'s `resetKey` option: +**Integration with playback:** `useExampleTopic` wires the selector to playback (it passes `activeExampleId` as `resetKey`). Topics normally render it through `ExamplePicker` inside `VisualizationToolbar`'s `leading` slot. -```typescript -const example = EXAMPLES.find((e) => e.id === activeExampleId) ?? EXAMPLES[0]; +The list supports arrow keys, Home, End, Enter, and Space; it uses `useClickOutside` for outside-click and Escape dismissal. -const { currentStepIndex, ... } = useStepPlayback({ - totalSteps: example.steps.length, - initialStep: -1, - resetKey: activeExampleId, // resets playback when the example changes -}); +### PipelineDiagram + +**File:** `src/components/visualization-ui/PipelineDiagram.tsx` + +Ordered stages with one active. Used for request lifecycles, middleware chains, model forward passes. + +```tsx + +``` + +Statuses: `pending` (slate), `active` (cyan), `done` (emerald), `skipped` (struck through). + +### TokenChips + +**File:** `src/components/visualization-ui/TokenChips.tsx` + +Row of labelled chips with optional value and tone. Used for token sequences, cache entries, timestamps, queue contents. + +```tsx + +``` + +Tones: `neutral`, `active`, `match`, `miss`, `muted`, `amber`, `violet`. + +### HeatmapGrid + +**File:** `src/components/visualization-ui/HeatmapGrid.tsx` + +Matrix of 0 to 1 values with colour intensity, optional row/column highlight and a boolean mask (masked cells render dimmed with a dash). Used for attention weights and similarity matrices. + +```tsx + +``` + +### MetricBars + +**File:** `src/components/visualization-ui/MetricBars.tsx` + +Labelled horizontal bars for values in 0 to 1. Used for probabilities, bucket levels, hit ratios, gradients. + +```tsx + +``` + +### MessageFlow + +**File:** `src/components/visualization-ui/MessageFlow.tsx` + +Sequence-diagram style panel: actors across the top, messages listed in order with direction arrows. Used for client/server exchanges and handshakes. + +```tsx + ``` -The selector renders in the toolbar area (inside ``), typically left-aligned with badges while transport controls stay on the right. It uses the shared `useClickOutside` hook internally for outside-click and Escape key dismissal. +Statuses: `done`, `active`, `pending`, `failed`. + +### StepScrubber + +**File:** `src/components/visualization-ui/TransportControls/StepScrubber.tsx` + +Rendered automatically by `TransportControls` when `onJumpTo` is passed. A native range input mapped to step indexes. `VisualizationToolbar` always passes `playback.jumpTo`, so every topic gets the scrubber. ### Tooltip @@ -228,20 +430,22 @@ import { TopicLink } from "@/components/visualization-ui/TopicLink"; **Props:** -| Prop | Type | Description | -|---|---|---| -| `href` | `string` | Route to the related topic | -| `label` | `string` | Descriptive link text | -| `className` | `string?` | Optional extra classes | +| Prop | Type | Description | +| ----------- | --------- | -------------------------- | +| `href` | `string` | Route to the related topic | +| `label` | `string` | Descriptive link text | +| `className` | `string?` | Optional extra classes | Renders as a pink pill with an arrow icon. Show it conditionally on the last step of a relevant example: ```tsx -{currentStepIndex === example.steps.length - 1 && ( -
- -
-)} +{ + currentStepIndex === example.steps.length - 1 && ( +
+ +
+ ); +} ``` --- @@ -250,54 +454,75 @@ Renders as a pink pill with an arrow icon. Show it conditionally on the last ste **File:** `src/hooks/useStepPlayback.ts` -Central playback engine used by all visualizations. Manages step index, auto-play timer, speed levels. +Central playback engine used by all visualizations. Manages step index, auto-play timer, speed levels. Selector topics get it through `useExampleTopic`; single-example and sandbox topics call it directly. The return type is exported as `StepPlayback`, which is what `VisualizationToolbar` takes as its `playback` prop. ```typescript import { useStepPlayback } from "@/hooks/useStepPlayback"; const { - currentStepIndex, // -1 (not started) or 0..N - isPlaying, // auto-advance active - speedLevel, // 1-6 - speedLabel, // "0.25x" .. "2x" - canStep, // can advance forward - canStepBack, // can go backward - togglePlay, // play/pause - step, // advance one step - stepBack, // go back one step - reset, // return to initial state - setSpeedLevel, // change speed - jumpTo, // jump to specific step index + currentStepIndex, // -1 (not started) or 0..N + isPlaying, // auto-advance active + speedLevel, // 1-6 + speedLabel, // "0.25x" .. "2x" + canStep, // can advance forward + canStepBack, // can go backward + togglePlay, // play/pause + step, // advance one step + stepBack, // go back one step + reset, // return to initial state + setSpeedLevel, // change speed + jumpTo, // jump to specific step index } = useStepPlayback({ totalSteps: STEPS.length, - initialStep: -1, // -1 = "not started", 0 = "first step visible" - resetKey: someValue, // optional: reset when this changes + initialStep: -1, // -1 = "not started", 0 = "first step visible" + resetKey: someValue, // optional: reset when this changes }); ``` **Options:** -| Option | Type | Default | Description | -|---|---|---|---| -| `totalSteps` | `number` | required | Total number of steps | -| `initialStep` | `number` | `-1` | Starting index. Use `-1` for "press play to start", `0` for "first step shown immediately" | -| `resetKey` | `string \| number` | `undefined` | When this changes, playback resets. Use for switching between sub-examples (like Hoisting's example selector) | +| Option | Type | Default | Description | +| ------------- | ------------------ | ----------- | ------------------------------------------------------------------------------------------------------------- | +| `totalSteps` | `number` | required | Total number of steps | +| `initialStep` | `number` | `-1` | Starting index. Use `-1` for "press play to start", `0` for "first step shown immediately" | +| `resetKey` | `string \| number` | `undefined` | When this changes, playback resets. Use for switching between sub-examples (like Hoisting's example selector) | Default speed level is `4` (`1x`). **Speed levels:** -| Level | Label | Delay | -|---|---|---| -| 1 | 0.25x | 5000ms | -| 2 | 0.5x | 2500ms | -| 3 | 0.75x | 1800ms | -| 4 | 1x | 1200ms | -| 5 | 1.5x | 700ms | -| 6 | 2x | 400ms | +| Level | Label | Delay | +| ----- | ----- | ------ | +| 1 | 0.25x | 5000ms | +| 2 | 0.5x | 2500ms | +| 3 | 0.75x | 1800ms | +| 4 | 1x | 1200ms | +| 5 | 1.5x | 700ms | +| 6 | 2x | 400ms | --- +### Keyboard Shortcuts + +`useStepPlayback` binds global shortcuts while no text field, CodeMirror editor, or dialog has focus: + +| Key | Action | +| ----------- | ------------ | +| Space | Play / pause | +| Right arrow | Step forward | +| Left arrow | Step back | +| R | Reset | + +Pass `keyboardShortcuts: false` to opt out (for example when two playback instances share a page). The page shell shows a `KeyboardHint` next to the back link. + +### Topic Progress + +Reaching the last step marks the topic as completed in `localStorage` (`src/lib/progress/topicProgress.ts`). `useStepPlayback` reads the topic ID from `TopicProgressProvider`, which `VisualizationPageShell` renders, so topics need no changes. Components read completion state with `useTopicProgress` / `useIsTopicCompleted` from `src/hooks/useTopicProgress.ts`. `TopicProgressMark` renders the check badge in the sidebar, cards, and search results; `ProgressSummary` renders the per-category bars on the landing page. + +### Search Palette + +`src/components/search/CommandPalette.tsx` mounts once in the root layout. Cmd+K, Ctrl+K, or `/` opens it; `openCommandPalette()` opens it from any component (`SearchTrigger` uses this). Ranking lives in `searchTopics.ts` and is pure, so it can be unit tested. + ## Change Flash - useChangeFlash **File:** `src/hooks/useChangeFlash.ts` @@ -327,10 +552,10 @@ const flashes = useChangeFlash( **Parameters:** -| Param | Type | Description | -|---|---|---| -| `channels` | `Record` | Named data slices from the current step. Values are compared via `JSON.stringify` | -| `stepIndex` | `number` | Current step index from `useStepPlayback`. Used as the effect trigger | +| Param | Type | Description | +| ----------- | -------------------- | --------------------------------------------------------------------------------- | +| `channels` | `Record` | Named data slices from the current step. Values are compared via `JSON.stringify` | +| `stepIndex` | `number` | Current step index from `useStepPlayback`. Used as the effect trigger | **Returns:** `Record` - same keys as `channels`, each `true` if that channel changed on the most recent step transition. @@ -345,9 +570,9 @@ const flashes = useChangeFlash( Two animation classes are available in `globals.css`: -| Class | Effect | Apply to | -|---|---|---| -| `.viz-change-flash` | Outer pink glow pulse (`box-shadow`) | NeonPanels - via `className` prop | +| Class | Effect | Apply to | +| ------------------------ | ------------------------------------ | ------------------------------------- | +| `.viz-change-flash` | Outer pink glow pulse (`box-shadow`) | NeonPanels - via `className` prop | | `.viz-change-flash-pill` | Inset pink glow pulse (`box-shadow`) | Explanation pill wrapper - via `cn()` | Both animations run for 850ms with peak at 40%. They honor `prefers-reduced-motion`. @@ -401,7 +626,10 @@ function itemFingerprint(item: Item): string { function HeapItems({ items }: { items: Item[] }) { return items.map((item) => ( -
+
{item.label}
)); @@ -421,12 +649,12 @@ The fingerprint should include all properties that visually affect the item (lab Each channel key should map to the data that drives a specific panel. Common channels: -| Channel | Data | Panel | -|---|---|---| -| `description` | `descriptionHtml` | Explanation pill | -| `stack` | `stackFrames` / `stack` | Call Stack NeonPanel | -| `heap` | `heapObjects` / `heapAllocations` | Heap NeonPanel | -| `console` | `consoleOutput` | Console Output | +| Channel | Data | Panel | +| ------------- | --------------------------------- | -------------------- | +| `description` | `descriptionHtml` | Explanation pill | +| `stack` | `stackFrames` / `stack` | Call Stack NeonPanel | +| `heap` | `heapObjects` / `heapAllocations` | Heap NeonPanel | +| `console` | `consoleOutput` | Console Output | Add topic-specific channels as needed (e.g., `roots`, `scope`, `taskQueue`). @@ -442,16 +670,16 @@ Lightweight regex tokenizer for educational JavaScript snippets. Not a general p ### Token Types -| Type | CSS Class | Color | Examples | -|---|---|---|---| -| `keyword` | `.tok-kw` | `#c084fc` (purple) | `let`, `const`, `function`, `return`, `if`, `async`, `await` | -| `builtin` | `.tok-builtin` | `#e5c07b` (warm yellow) | `console`, `Promise`, `Array`, `Math`, `JSON`, `Error` | -| `function` | `.tok-fn` | `#60a5fa` (blue) | `log`, `setTimeout`, `resolve`, `then`, `push`, `map` | -| `string` | `.tok-str` | `#34d399` (green) | `'hello'`, `"world"`, `` `template` `` | -| `number` | `.tok-num` | `#fbbf24` (amber) | `42`, `3.14` | -| `comment` | `.tok-comment` | `#64748b` (slate) | `// comment` | -| `punctuation` | `.tok-punct` | `#94a3b8` (light slate) | `{`, `}`, `(`, `)`, `;` | -| `plain` | (none) | inherits | identifiers, whitespace | +| Type | CSS Class | Color | Examples | +| ------------- | -------------- | ----------------------- | ------------------------------------------------------------ | +| `keyword` | `.tok-kw` | `#c084fc` (purple) | `let`, `const`, `function`, `return`, `if`, `async`, `await` | +| `builtin` | `.tok-builtin` | `#e5c07b` (warm yellow) | `console`, `Promise`, `Array`, `Math`, `JSON`, `Error` | +| `function` | `.tok-fn` | `#60a5fa` (blue) | `log`, `setTimeout`, `resolve`, `then`, `push`, `map` | +| `string` | `.tok-str` | `#34d399` (green) | `'hello'`, `"world"`, `` `template` `` | +| `number` | `.tok-num` | `#fbbf24` (amber) | `42`, `3.14` | +| `comment` | `.tok-comment` | `#64748b` (slate) | `// comment` | +| `punctuation` | `.tok-punct` | `#94a3b8` (light slate) | `{`, `}`, `(`, `)`, `;` | +| `plain` | (none) | inherits | identifiers, whitespace | ### Distinction: builtins vs functions @@ -469,22 +697,20 @@ All animations are CSS-only (no Framer Motion). Shared keyframes live in `src/ap ### Shared Animation Classes -| Class | Effect | Use Case | -|---|---|---| -| `.viz-slide-in` | Slide in from left with spring | Queue items entering | -| `.viz-float-up` | Float up with fade-in | Hoisted declarations moving to top | -| `.viz-pulse-dot` | Pulse opacity (0.55 to 1) | Playing indicator dot | -| `.viz-change-flash` | Outer pink glow pulse (850ms) | NeonPanels whose data changed between steps | -| `.viz-change-flash-pill` | Inset pink glow pulse (850ms) | Explanation pill when description changed | +| Class | Effect | Use Case | +| ------------------------ | ------------------------------ | ------------------------------------------- | +| `.viz-slide-in` | Slide in from left with spring | Queue items entering | +| `.viz-float-up` | Float up with fade-in | Hoisted declarations moving to top | +| `.viz-pulse-dot` | Pulse opacity (0.55 to 1) | Playing indicator dot | +| `.viz-change-flash` | Outer pink glow pulse (850ms) | NeonPanels whose data changed between steps | +| `.viz-change-flash-pill` | Inset pink glow pulse (850ms) | Explanation pill when description changed | ### Using `.viz-slide-in` Apply to items that appear in queues/lists: ```tsx -
- {item} -
+
{item}
``` ### Using `.viz-float-up` @@ -503,7 +729,9 @@ Apply to elements that represent hoisting (moving declarations upward): Shows a pulsing green dot when playback is active: ```tsx -{isPlaying ? : null} +{ + isPlaying ? : null; +} ``` ### Topic-Specific Animations @@ -532,23 +760,20 @@ Keep topic-specific CSS minimal. If an animation pattern is reused across 2+ top Applied via `className` on `CodeBlock` / `CodeLine` items: -| Class | Effect | When to Use | -|---|---|---| -| `.is-active` | Yellow left border + amber background | Current execution line | -| `.is-done` | 40% opacity | Lines already executed (never combine with `is-active`) | -| `.is-highlighted` | Soft amber background + faint left border | Lines being referenced but not executing | -| `.is-floating` | Green background + green left border | Lines being hoisted upward | -| `.is-tdz` | Red background + red left border | Lines in Temporal Dead Zone | +| Class | Effect | When to Use | +| ----------------- | ----------------------------------------- | ------------------------------------------------------- | +| `.is-active` | Yellow left border + amber background | Current execution line | +| `.is-done` | 40% opacity | Lines already executed (never combine with `is-active`) | +| `.is-highlighted` | Soft amber background + faint left border | Lines being referenced but not executing | +| `.is-floating` | Green background + green left border | Lines being hoisted upward | +| `.is-tdz` | Red background + red left border | Lines in Temporal Dead Zone | These classes are defined in `globals.css` under `@layer components` and apply to `.code-line` elements. **Critical rule:** When mapping lines, always guard `is-done` with `!isActive`: ```typescript -className: cn( - isActive && "is-active", - isDone && !isActive && "is-done", -) +className: cn(isActive && "is-active", isDone && !isActive && "is-done"); ``` ### Code Line Structure diff --git a/docs/react-topic-authoring.md b/docs/react-topic-authoring.md index 8b12a09..abc9823 100644 --- a/docs/react-topic-authoring.md +++ b/docs/react-topic-authoring.md @@ -23,8 +23,8 @@ React topics visualize component/element trees instead of linear structures (cal ```typescript interface ComponentTreeDiagramProps { - tree: TreeNodeData; // Root node of the tree - activeNodeId?: string; // Node highlighted with a cyan ring + tree: TreeNodeData; // Root node of the tree + activeNodeId?: string; // Node highlighted with a cyan ring } ``` @@ -33,7 +33,12 @@ interface ComponentTreeDiagramProps { Defined in `src/types/visualization.ts`: ```typescript -export type TreeNodeHighlight = "unchanged" | "updated" | "added" | "removed" | "active"; +export type TreeNodeHighlight = + | "unchanged" + | "updated" + | "added" + | "removed" + | "active"; export interface TreeNodeData { id: string; @@ -46,17 +51,18 @@ export interface TreeNodeData { ### Highlight Color Scheme -| Highlight | Border/BG | Text Color | Use Case | -|-------------|-------------|-------------|-----------------------------------| -| `unchanged` | slate | slate-400 | Nodes that didn't change | -| `updated` | amber | amber-300 | Props or text content changed | -| `added` | emerald | emerald-300 | Newly mounted nodes | -| `removed` | rose | rose-400 | Nodes being unmounted (strikethrough) | -| `active` | cyan | cyan-300 | Currently being compared/diffed | +| Highlight | Border/BG | Text Color | Use Case | +| ----------- | --------- | ----------- | ------------------------------------- | +| `unchanged` | slate | slate-400 | Nodes that didn't change | +| `updated` | amber | amber-300 | Props or text content changed | +| `added` | emerald | emerald-300 | Newly mounted nodes | +| `removed` | rose | rose-400 | Nodes being unmounted (strikethrough) | +| `active` | cyan | cyan-300 | Currently being compared/diffed | ### Connector Lines (Split-Half Technique) Tree connectors use a split-half pattern for each child column: + - **First child**: right half of horizontal rail only - **Middle children**: both left and right halves - **Last child**: left half only @@ -67,6 +73,7 @@ Child columns use `flex` layout edge-to-edge (no gap) so the rail segments conne ### Auto-Scaling The component automatically scales down when the tree is wider than its container: + - Uses `ResizeObserver` + `useLayoutEffect` to measure natural width vs container width - Applies `transform: scale(ratio)` with `transformOrigin: "top center"` - Adjusts wrapper height to `Math.ceil(naturalHeight * scale)` to prevent layout gaps @@ -75,7 +82,7 @@ The component automatically scales down when the tree is wider than its containe ### NeonPanel Tone Conventions for React Topics | Panel | Tone | -|----------------|----------| +| -------------- | -------- | | Source Code | `amber` | | Previous Tree | `cyan` | | New Tree | `green` | @@ -86,7 +93,10 @@ The component automatically scales down when the tree is wider than its containe ```tsx import { ComponentTreeDiagram } from "@/components/visualization-ui/ComponentTreeDiagram"; - {currentStep ? ( @@ -99,7 +109,7 @@ import { ComponentTreeDiagram } from "@/components/visualization-ui/ComponentTre waiting

)} -
+; ``` --- @@ -152,7 +162,10 @@ export interface YourExample extends ExampleOption { Use the shared helper factories for consistent badge/label patterns: ```typescript -import { createKindBadgeClass, createKindLabel } from "@/lib/visualization-helpers"; +import { + createKindBadgeClass, + createKindLabel, +} from "@/lib/visualization-helpers"; import type { YourKind } from "./types"; export const kindBadgeClass = createKindBadgeClass({ @@ -171,18 +184,11 @@ export const kindLabel = createKindLabel({ ### Main Component Pattern ```typescript -export function YourTopic() { - const [activeExampleId, setActiveExampleId] = useState(EXAMPLES[0].id); - const handleExampleChange = (id: string) => setActiveExampleId(id); - const example = EXAMPLES.find((e) => e.id === activeExampleId) ?? EXAMPLES[0]; - - const { currentStepIndex, ... } = useStepPlayback({ - totalSteps: example.steps.length, - initialStep: -1, - resetKey: activeExampleId, - }); - - const currentStep = currentStepIndex >= 0 ? example.steps[currentStepIndex] : null; +export const YourTopic = () => { + const { example, activeExampleId, handleExampleChange, playback, currentStep } = + useExampleTopic(EXAMPLES); + const { currentStepIndex } = playback; + const flashes = useChangeFlash({ description: currentStep?.descriptionHtml, previousTree: currentStep?.previousTree, @@ -192,15 +198,22 @@ export function YourTopic() { return ( <> - - {/* ExampleSelector + Badge + TransportControls + step description pill */} - -
- {/* CodeBlock (amber) | Previous Tree (cyan) + New Tree (green) | DiffPanel (violet) */} -
+ } + /> + + + + {/* Previous Tree (cyan) + New Tree (green) | DiffPanel (violet) */} + + ); -} +}; ``` ### Reference Implementation @@ -213,12 +226,10 @@ The **Reconciliation** topic (`src/components/visualizations/reconciliation/`) i When adding a new React topic, follow these additional steps beyond the base checklist: -1. Register in `src/lib/topics.ts` with `category: "react"` and `route: "/react/"` -2. Create route page at `src/app/react//page.tsx` -3. Create theory file at `src/content/theory/react/.ts` (if applicable) -4. Add to `SELECTOR_TOOLBAR_TOPIC_IDS` in `VisualizationPageShell.tsx` (if using ExampleSelector) -5. Sitemap updates automatically (category-agnostic), and the theory sections - render on the topic page once the content is registered +1. Register in `src/lib/topics.ts` with `category: "react"` (the route is derived) +2. Add the component to `VISUALIZATIONS` in `src/components/visualizations/registry.tsx` +3. Create theory file at `src/content/theory/react/.ts` +4. The route, sitemap, and theory sections all follow from the registries; `npm run typecheck` fails until every registry has the new id --- @@ -226,7 +237,7 @@ When adding a new React topic, follow these additional steps beyond the base che React-specific items (in addition to the "All Topics" checklist in [topic-authoring.md](topic-authoring.md)): -- [ ] Route page at `src/app/react//page.tsx` +- [ ] Component registered in `VISUALIZATIONS` - [ ] Topic uses folder-based structure: `types.ts`, `helpers.ts`, `data.ts`, `components/`, `index.tsx` - [ ] Tree panels use `ComponentTreeDiagram` from `visualization-ui/` - [ ] Tree panels follow tone conventions: Previous Tree (cyan), New Tree (green) diff --git a/docs/sandbox-authoring.md b/docs/sandbox-authoring.md index e3ca38f..d2a979a 100644 --- a/docs/sandbox-authoring.md +++ b/docs/sandbox-authoring.md @@ -86,12 +86,12 @@ A generator is a function with this signature: import type { Program } from "acorn"; import type { StepGenerationResult } from "@/types/sandbox"; -export function generateYourTopicSteps( +export const generateYourTopicSteps = ( ast: Program, source: string, -): StepGenerationResult { +): StepGenerationResult => { // Walk the AST, build steps, return them -} +}; ``` ### Define Your Types @@ -119,11 +119,17 @@ Use Acorn's AST node types. The `ast.body` array contains top-level statements. ```ts for (const node of ast.body) { - if (node.type === "VariableDeclaration") { /* ... */ } - if (node.type === "FunctionDeclaration") { /* ... */ } + if (node.type === "VariableDeclaration") { + /* ... */ + } + if (node.type === "FunctionDeclaration") { + /* ... */ + } if (node.type === "ExpressionStatement") { const expr = node.expression; - if (expr.type === "CallExpression") { /* ... */ } + if (expr.type === "CallExpression") { + /* ... */ + } } } ``` @@ -152,6 +158,7 @@ return { ### Reference Implementation See `src/lib/sandbox/generators/event-loop.ts` for a complete example (~500 lines) that handles: + - `console.log()` calls - `setTimeout()` with callbacks - `Promise.resolve().then()` chains (including nested) @@ -167,11 +174,13 @@ Add an entry to `src/lib/sandbox/configs.ts`: ```ts export const SANDBOX_CONFIGS: Record = { - "event-loop": { /* ... existing ... */ }, + "event-loop": { + /* ... existing ... */ + }, // Add your topic: "your-topic": { - topicId: "your-topic", // Must match topic registry ID + topicId: "your-topic", // Must match topic registry ID defaultCode: `// Starter code function outer() { let x = 10; @@ -180,14 +189,15 @@ function outer() { } return inner; }`, - supportedPatterns: [ // Shown in error display when unsupported code is used + supportedPatterns: [ + // Shown in error display when unsupported code is used "Function declarations", "Variable declarations (let, const)", "Nested functions", "console.log()", ], - maxCodeLines: 25, // Max lines allowed - maxCodeLength: 1000, // Max characters allowed + maxCodeLines: 25, // Max lines allowed + maxCodeLength: 1000, // Max characters allowed }, }; ``` @@ -196,32 +206,30 @@ function outer() { ## Step 3 - Wire Into the Visualization -In your visualization component, add sandbox support with minimal boilerplate: +The sandbox UI is packaged as two components, so a sandbox topic differs from a normal one in only a few lines. `src/components/visualizations/event-loop/index.tsx` is the full reference. ### 1. Add Imports ```tsx -import dynamic from "next/dynamic"; -import { Play, RotateCcw, Pencil } from "lucide-react"; -import { SandboxToggle } from "@/components/sandbox/SandboxToggle"; -import { SandboxErrorDisplay } from "@/components/sandbox/SandboxErrorDisplay"; +import { + SandboxControls, + SandboxEditingHint, +} from "@/components/sandbox/SandboxControls"; +import { SandboxPanel } from "@/components/sandbox/SandboxPanel"; import { useSandboxUIState } from "@/hooks/useSandboxUIState"; +import { useStepPlayback } from "@/hooks/useStepPlayback"; import { SANDBOX_CONFIGS } from "@/lib/sandbox/configs"; import { generateYourTopicSteps } from "@/lib/sandbox/generators/your-topic"; - -// Dynamic import - CodeMirror only loads when sandbox is active -const SandboxEditor = dynamic( - () => import("@/components/sandbox/SandboxEditor").then((m) => ({ default: m.SandboxEditor })), - { ssr: false, loading: () =>
}, -); ``` +`SandboxPanel` lazy-loads the CodeMirror editor, so nothing from CodeMirror ships until the reader opens the sandbox. + ### 2. Use the Hook ```tsx const sandboxConfig = SANDBOX_CONFIGS["your-topic"]; -export function YourTopic() { +export const YourTopic = () => { const { sandbox, isEditing, @@ -232,110 +240,86 @@ export function YourTopic() { handleEditCode, } = useSandboxUIState(sandboxConfig, generateYourTopicSteps); - // Choose active data source - const activeSteps = usingSandbox ? sandbox.generatedSteps! : STEPS; - const activeCodeLines = usingSandbox ? sandbox.generatedCodeLines! : CODE_LINES; + // Choose the active data source + const activeSteps = usingSandbox && sandbox.generatedSteps ? sandbox.generatedSteps : STEPS; + const activeCodeLines = + usingSandbox && sandbox.generatedCodeLines ? sandbox.generatedCodeLines : CODE_LINES; + const isEditingSandbox = sandbox.isSandboxActive && isEditing; - // Pass activeSteps to useStepPlayback - const { currentStepIndex, ... } = useStepPlayback({ + const playback = useStepPlayback({ totalSteps: activeSteps.length, - initialStep: -1, resetKey: usingSandbox ? `sandbox-${sandbox.generationId}` : "default", }); + const currentStep = + playback.currentStepIndex >= 0 ? activeSteps[playback.currentStepIndex] : null; ``` -### 3. Add Toolbar Buttons +### 3. Toolbar + +`VisualizationToolbar` takes the sandbox buttons as `leading`, hides the transport while the editor owns the stage, and swaps the step description for the editing hint: ```tsx - -
- {/* Sandbox toggle */} - : undefined} + leading={ + - - {/* Editing mode: Generate + Reset */} - {sandbox.isSandboxActive && isEditing && ( - <> - - - - - - - - )} - - {/* View mode: Edit button */} - {sandbox.isSandboxActive && !isEditing && ( - - - - )} - - {/* Transport controls (shown when not editing) */} - {(showHighlightedCode || !sandbox.isSandboxActive) && ( - - )} -
-
+ } +/> ``` -### 4. Swap Code Panel +### 4. Swap the Code Panel ```tsx -{/* Editor (editing mode) */} -{sandbox.isSandboxActive && isEditing ? ( - - + ) : ( + - {sandbox.error && ( - - )} -

- Limited to {sandboxConfig.maxCodeLines} lines · {sandboxConfig.maxCodeLength} characters -

-
-) : ( - /* Highlighted code (view mode or sandbox off) */ - - - -)} + ); +} ``` +`SandboxPanel` renders the editor, the error display, and the line and character limits. + --- ## Shared Utilities `src/lib/sandbox/generatorUtils.ts` provides helpers every generator can use: -| Function | Description | -|----------|-------------| -| `getLineRange(node)` | Returns all line numbers a node spans (1-based) | -| `expressionToLabel(node, source)` | Short label from source (truncated to 45 chars) | -| `callbackLabel(bodySource)` | Label like `() => console.log('x')` | -| `getCallbackBody(node)` | Extract body from arrow/function expressions | -| `extractCallbackFromArg(arg, source)` | Full callback info: label, body, lines | -| `extractConsoleLogArg(node, source, scope?)` | Get `console.log()` arg text with optional scope resolution | -| `resolveNodeToString(node, source, scope?)` | Resolve literals/identifiers/template literals to display text | -| `escapeHtml(value)` | Escape untrusted text before interpolating into HTML | -| `createError(type, message, line?)` | Factory for `SandboxError` objects | +| Function | Description | +| -------------------------------------------- | -------------------------------------------------------------- | +| `getLineRange(node)` | Returns all line numbers a node spans (1-based) | +| `expressionToLabel(node, source)` | Short label from source (truncated to 45 chars) | +| `callbackLabel(bodySource)` | Label like `() => console.log('x')` | +| `getCallbackBody(node)` | Extract body from arrow/function expressions | +| `extractCallbackFromArg(arg, source)` | Full callback info: label, body, lines | +| `extractConsoleLogArg(node, source, scope?)` | Get `console.log()` arg text with optional scope resolution | +| `resolveNodeToString(node, source, scope?)` | Resolve literals/identifiers/template literals to display text | +| `escapeHtml(value)` | Escape untrusted text before interpolating into HTML | +| `createError(type, message, line?)` | Factory for `SandboxError` objects | Usage: @@ -372,12 +356,12 @@ The CodeMirror editor (`SandboxEditor`) provides out of the box: The generator can return four error types: -| Type | When | Display | -|------|------|---------| -| `parse-error` | Acorn can't parse the code | Red banner with line/column | +| Type | When | Display | +| --------------------- | --------------------------------------------- | ----------------------------------------- | +| `parse-error` | Acorn can't parse the code | Red banner with line/column | | `unsupported-pattern` | Valid JS but your generator doesn't handle it | Amber banner with supported patterns list | -| `code-too-long` | Exceeds `maxCodeLines` | Auto-handled by `useSandboxMode` | -| `generation-error` | Other generation failure (empty code, etc.) | Red banner | +| `code-too-long` | Exceeds `maxCodeLines` | Auto-handled by `useSandboxMode` | +| `generation-error` | Other generation failure (empty code, etc.) | Red banner | When returning `unsupported-pattern`, provide a helpful message: @@ -386,7 +370,8 @@ return { success: false, error: { type: "unsupported-pattern", - message: "for loops are not supported yet. Use simple statements and function calls.", + message: + "for loops are not supported yet. Use simple statements and function calls.", line: node.loc?.start.line, }, }; @@ -418,15 +403,13 @@ Before shipping a new sandbox topic: - [ ] `defaultCode` is valid and demonstrates the topic well - [ ] `supportedPatterns` list is accurate and matches what the generator handles - [ ] Visualization component uses `useSandboxUIState()` hook -- [ ] `SandboxEditor` rendered with `code`, `codeVersion`, `onChange`, `onGenerate`, `maxLines` -- [ ] `SandboxErrorDisplay` rendered when `sandbox.error` exists -- [ ] `SandboxToggle` in toolbar with `handleToggleSandbox` -- [ ] Generate / Reset / Edit buttons in toolbar with correct conditional rendering -- [ ] `TransportControls` hidden during editing mode +- [ ] `SandboxPanel` replaces `SourceCodePanel` while editing +- [ ] `SandboxControls` passed as `leading` to `VisualizationToolbar` with `align="center"` +- [ ] `hideTransport` and `descriptionOverride` set on `VisualizationToolbar` for editing mode - [ ] `resetKey` passed to `useStepPlayback` includes `sandbox.generationId` - [ ] `activeSteps` switches between generated and hardcoded based on `usingSandbox` - [ ] Any user-derived text interpolated into HTML is escaped via `escapeHtml` - [ ] Tested with valid code, invalid code, empty code, and unsupported patterns - [ ] Tested with malicious payload sample (e.g. ``) to verify text rendering only - [ ] Queue item pills handle long text (overflow-hidden, break-all) -- [ ] Build passes (`npm run build`) +- [ ] `npm run verify` passes diff --git a/docs/seo.md b/docs/seo.md index 70d6fb2..9e972cf 100644 --- a/docs/seo.md +++ b/docs/seo.md @@ -29,22 +29,21 @@ This document describes the SEO work currently implemented in VisualizeJS. - canonical URL - robots directives - Open Graph + Twitter cards (including `images`) -- Social preview image routes are generated by Next metadata files: - - `src/app/opengraph-image.tsx` - - `src/app/twitter-image.tsx` - - URLs are absolute via shared constants in `src/lib/constants.ts` +- Social preview images are generated per route by `opengraph-image.tsx` files, all drawing through `src/lib/socialImage.tsx`: + - `src/app/opengraph-image.tsx` (home) + - `src/app/[category]/opengraph-image.tsx` (category colour, topic count) + - `src/app/[category]/[topic]/opengraph-image.tsx` (topic title, difficulty, description) + - Twitter cards reuse the Open Graph image; there is no separate twitter-image route - Category pages define targeted metadata: - - `src/app/javascript/page.tsx` - - `src/app/react/page.tsx` + - `src/app/[category]/page.tsx` (one generated page per category) ### 2) Keyword Strategy - Centralized keyword composition in `src/lib/metadata.ts`: - - `GLOBAL_KEYWORDS` - - `CATEGORY_KEYWORDS` - - `TOPIC_KEYWORDS` + - `CATEGORY_KEYWORDS` (from the category registry) + - `TOPIC_KEYWORDS` (curated, 4 to 8 per topic, typed by `TopicId`) - dedupe helper -- `getTopicKeywords(topic)` builds final topic keywords from all groups. +- `getTopicKeywords(topic)` merges the topic title, the curated list, and the category list. Search engines ignore the meta keywords tag, so the list is kept short; its real use is the `keywords` field of the TechArticle structured data. - Includes intent-style query coverage, for example: - `var vs let vs const` - `execution context javascript` @@ -79,11 +78,12 @@ This document describes the SEO work currently implemented in VisualizeJS. - `src/lib/metadata.ts` - `src/app/layout.tsx` - `src/app/opengraph-image.tsx` -- `src/app/twitter-image.tsx` +- `src/app/[category]/opengraph-image.tsx` +- `src/app/[category]/[topic]/opengraph-image.tsx` +- `src/lib/socialImage.tsx` - `src/components/layout/VisualizationPageShell.tsx` - `src/components/layout/CategoryTopicsPage.tsx` -- `src/app/javascript/page.tsx` -- `src/app/react/page.tsx` +- `src/app/[category]/page.tsx` - `src/app/robots.ts` - `src/app/sitemap.ts` @@ -101,6 +101,7 @@ Recommended for production: - Optional. Notes: + - `NEXT_PUBLIC_*` variables are exposed to the browser, so do not place secrets there. - Configure Production, Preview, and Development values separately in Vercel as needed. @@ -111,7 +112,8 @@ Notes: 3. Inspect rendered `` on a topic page for canonical, OG, Twitter, and keywords. 4. Verify `og:image` and `twitter:image` are absolute and return `200`: - `https://visualizejs.com/opengraph-image` - - `https://visualizejs.com/twitter-image` + - `https://visualizejs.com/backend/opengraph-image` + - `https://visualizejs.com/javascript/closures/opengraph-image` 5. Validate JSON-LD in Google Rich Results Test or Schema Markup Validator. 6. Add and verify domain in Google Search Console, then submit sitemap. 7. Wait for crawl/indexing and monitor query impressions. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..b61951b --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,68 @@ +# Testing + +The project uses [Vitest](https://vitest.dev) with a jsdom environment and +React Testing Library. Tests run in CI before the step audit and the build. + +## Commands + +| Command | What it does | +| ----------------------- | ------------------------------------------------------- | +| `npm run test` | Run the whole suite once | +| `npm run test:watch` | Re-run affected tests as files change | +| `npm run test:coverage` | Run once and write a V8 coverage report | +| `npx vitest run ` | Run a single file, for example `src/lib/topics.test.ts` | + +Configuration lives in `vitest.config.mts`. The `@/` alias resolves to `src/`, +and `src/test/setup.ts` runs before every file: it loads the jest-dom +matchers, bridges jsdom's `localStorage` onto the global (newer Node +versions ship their own experimental global that would otherwise shadow it), +stubs `matchMedia` as a non-hover device, and cleans up rendered components +and storage after each test. + +## Where tests live + +Unit and component tests sit next to the code they cover as `*.test.ts` or +`*.test.tsx`. Cross-cutting checks that sweep every topic live in +`src/test/conventions/`. The pure rules behind `npm run audit:steps` are in +`scripts/lib/stepAudit.ts` and are tested there too. + +## What is covered + +- **Registries**: `src/lib/topics.ts`, `src/lib/categories.ts`, the theory + registry, `src/lib/metadata.ts`, and the sitemap and robots routes. These + tests check ids, derived routes, keyword and description generation, + structured data, and that every topic links three to five real related + topics. +- **Content conventions** (`src/test/conventions/topicContent.test.ts`): + every topic folder has `index.tsx`, `data.ts`, and `types.ts`, uses + `"use client"` and named exports, keeps files at or under 300 lines, reads + panel titles from `uiCopy.ts`, numbers code lines from 1, passes the step + audit without errors, keeps step copy free of em dashes and emojis, and + (for AI topics) carries `simpleHtml` on every step. Topics that do not drive + a source panel are listed in `NON_SOURCE_TOPICS` in that file. +- **Sandbox**: the acorn parser wrapper, the shared generator utilities, the + event loop step generator (ordering semantics, line tracking, error + messages, HTML escaping), and the sandbox configs. +- **Stores**: topic progress and explanation mode, including cross-tab + `storage` events and storage failures. +- **Hooks**: playback (auto-advance, keyboard shortcuts, reset keys, + progress marking), example selection, change flashes, sandbox mode and UI + state, focus trap, click outside, and hover detection. +- **Shared UI**: `SourceCodePanel` (including the active-over-done guard), + `CodeLine` and `CodeBlock`, `ExplanationToggle`, `TransportControls`, and + `ExampleSelector`. +- **Search**: the ranking in `filterTopics`. + +## Adding tests + +- Prefer testing behaviour through public functions and rendered output. + Query the DOM by role and accessible name rather than by class. +- Hooks are tested with `renderHook` from React Testing Library. Wrap state + changes in `act`, and use `vi.useFakeTimers()` for anything on a timer. +- Modules that cache in module scope (the stores) are loaded fresh with + `vi.resetModules()` followed by a dynamic import. +- A new topic needs no test of its own: the convention suite picks it up + from the registry. If its data does not use numbered code lines, add its + id to `NON_SOURCE_TOPICS`. +- New library code, hooks, and shared primitives should ship with tests in + the same change. diff --git a/docs/theory-authoring.md b/docs/theory-authoring.md index 1b0e35f..7e2dd92 100644 --- a/docs/theory-authoring.md +++ b/docs/theory-authoring.md @@ -9,8 +9,7 @@ How to add theory content for visualization topics. Theory sections render on th - Content model types: `src/content/theory/types.ts` - Content registry: `src/content/theory/index.ts` - Topic content files: - - `src/content/theory/javascript/.ts` - - `src/content/theory/react/.ts` + - `src/content/theory//.ts` (one folder per category: `javascript`, `react`, `frameworks`, `backend`, `ai`) - Shared theory renderer: `src/components/theory/TopicTheorySections.tsx` - Rendered by `src/components/layout/VisualizationPageShell.tsx`, which pulls content from the registry and places it below the visualization. There is no @@ -21,6 +20,7 @@ How to add theory content for visualization topics. Theory sections render on th ## Naming Convention File name must match topic id exactly: + - JS example: topic id `promises` -> `src/content/theory/javascript/promises.ts` - React example: topic id `reconciliation` -> `src/content/theory/react/reconciliation.ts` @@ -35,6 +35,7 @@ File name must match topic id exactly: 5. Run `npm run lint` and `npm run build`. After registration: + - The theory sections render on `//`, below the visualization. - The topic page header shows the `Theory` button, which scrolls to those sections. - No sitemap change is needed, since the content shares the topic's URL. @@ -88,6 +89,7 @@ Example: "Reconciliation is the algorithm React uses to diff two virtual DOM tre Array of 3+ paragraphs (strings). Progressively deeper explanation of the concept. Start accessible and build toward technical detail. This is not a reproduction of MDN or React docs. Focus on the "why" and mental model, not exhaustive API coverage. Guidelines: + - First paragraph: accessible explanation anyone can understand - Second paragraph: key technical insight or mechanism - Third paragraph: how it connects to the runtime or broader system @@ -98,6 +100,7 @@ Guidelines: Array of ordered steps (strings). Each step starts with "Step N:" prefix. Describes the runtime or engine behavior in sequence. Focus on what happens internally, not how to use the API. Guidelines: + - 3-6 steps - Each step is 1-2 sentences - Steps should follow a logical execution order @@ -107,6 +110,7 @@ Guidelines: Array of `TheoryMistake` objects. Each has `title`, `explanation`, and `fix`. Guidelines: + - 2-4 mistakes per topic - `title`: short label (3-8 words) - `explanation`: why this mistake happens (1-2 sentences) @@ -117,6 +121,7 @@ Guidelines: Array of `TheoryInterviewQuestion` objects. At least 3 Q&A pairs. Guidelines: + - Questions should cover different difficulty levels - Answers should be concise but complete (2-4 sentences) - Optional `codeExample` with `code` string and `language` field (defaults to `"javascript"`) @@ -127,6 +132,7 @@ Guidelines: Array of 3-5 topic IDs from `src/lib/topics.ts`. **Never leave this empty.** Cross-category links are allowed (e.g., a React topic can link to JS topics). Guidelines: + - Choose topics that share conceptual overlap - Prefer topics that help build understanding (prerequisites or next steps) - Verify each ID exists in `src/lib/topics.ts` diff --git a/docs/topic-authoring.md b/docs/topic-authoring.md index b064d89..5792b50 100644 --- a/docs/topic-authoring.md +++ b/docs/topic-authoring.md @@ -9,7 +9,7 @@ How to wire a new JavaScript visualization topic into VisualizeJS. For React-spe 1. [Architecture Overview](#architecture-overview) 2. [Directory Structure](#directory-structure) 3. [Step 1 - Register the Topic](#step-1--register-the-topic) -4. [Step 2 - Create the Route Page](#step-2--create-the-route-page) +4. [Step 2 - Register the Component](#step-2--register-the-component) 5. [Step 3 - Build the Visualization Component](#step-3--build-the-visualization-component) 6. [Toolbar Portal Pattern](#toolbar-portal-pattern) 7. [Step Data Modeling](#step-data-modeling) @@ -37,9 +37,11 @@ Every visualization is a self-contained `"use client"` component that receives n The page title is followed by a small external-link icon that opens the topic's authoritative documentation (e.g., MDN). A thin divider line separates the header from the content below. -Transport controls and the step explanation pill are rendered *outside* the surface card via the `ToolbarPortal` pattern. This frees vertical space inside the card for the actual visualization panels. +Transport controls and the step explanation pill are rendered _outside_ the surface card via the `ToolbarPortal` pattern. This frees vertical space inside the card for the actual visualization panels. -Data flow: **hardcoded STEPS array** -> **useStepPlayback hook** -> **currentStepIndex** -> **conditional rendering**. +Data flow: **hardcoded EXAMPLES or STEPS array** -> **useExampleTopic / useStepPlayback** -> **currentStep** -> **conditional rendering**. + +Routing is generated: `src/app/[category]/[topic]/page.tsx` looks the topic up in the registry and renders the component from `VISUALIZATIONS`. There is no per-topic page file. --- @@ -48,15 +50,14 @@ Data flow: **hardcoded STEPS array** -> **useStepPlayback hook** -> **currentSte ``` src/ app/ - javascript/ - your-topic/page.tsx # Route page (Step 2) - react/ - your-topic/page.tsx # React topic route - layout.tsx # React category layout + [category]/ + page.tsx # Category index (generated for every category) + layout.tsx # Mounts the onboarding tour + [topic]/page.tsx # Topic page (generated for every topic) components/ visualizations/ - YourTopic.tsx # Single-file visualization (JS topics) - your-topic/ # Folder-based visualization (React/complex topics) + registry.tsx # VISUALIZATIONS: topic id -> lazy component (Step 2) + your-topic/ # One folder per topic index.tsx # Main component types.ts # Step, Example, Kind types helpers.ts # Badge classes, labels, operation styles @@ -71,7 +72,16 @@ src/ styles.ts # Highlight, label, and connector style maps ConsoleOutput.tsx # Shared console panel ExampleSelector.tsx # Dropdown for switching sub-examples + ExamplePicker.tsx # ExampleSelector + active kind badge + SourceCodePanel.tsx # Source Code panel with active/done line states + VisualizationToolbar.tsx # Transport controls + step pill, portaled above the card + VisualizationLayout.tsx # VisualizationSection, SourceGrid, WaitingPlaceholder NeonPanel.tsx # Themed container with tones + PipelineDiagram.tsx # Ordered stages with an active one + TokenChips.tsx # Row of labelled chips + HeatmapGrid.tsx # Matrix with colour intensity + MetricBars.tsx # Labelled horizontal bars + MessageFlow.tsx # Actors and ordered messages TopicLink.tsx # Cross-topic navigation link TransportControls/ # Playback buttons + speed dropdown index.tsx # Main component @@ -85,10 +95,12 @@ src/ Sidebar.tsx # Navigation sidebar AppTheme.tsx # Global theme wrapper hooks/ - useStepPlayback.ts # Shared playback engine + useStepPlayback.ts # Shared playback engine (keyboard shortcuts, progress) + useTopicProgress.ts # Completed-topic state from localStorage useChangeFlash.ts # Detects per-channel data changes between steps useClickOutside.ts # Outside-click + Escape dismiss hook lib/ + categories.ts # Category registry (labels, routes, colours) topics.ts # Topic registry constants.ts # Site-wide constants metadata.ts # SEO metadata factory @@ -107,94 +119,69 @@ src/ **File:** `src/lib/topics.ts` -Add an entry to the `topics` array: +Add an entry to `TOPIC_DEFINITIONS`: ```typescript { id: "closures", title: "Closures", category: "javascript", - route: "/javascript/closures", description: "See how closures capture variables from their lexical scope.", difficulty: "intermediate", docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures", + toolbar: "simple", // only for topics without an example picker } ``` +The array is `as const`, so the new id joins the `TopicId` union. From that point `npm run typecheck` fails until the component, theory, keyword, and description registries all have an entry for it. + **Fields:** -| Field | Type | Notes | -|---|---|---| -| `id` | `string` | URL slug, used by `getTopicOrThrow()` | -| `title` | `string` | Shown in sidebar + page heading | -| `category` | `"javascript" \| "react"` | Determines sidebar group | -| `route` | `string` | Must match `src/app///page.tsx` | -| `description` | `string` | Used for SEO metadata and category landing page cards (not shown on visualization page) | -| `difficulty` | `"beginner" \| "intermediate" \| "advanced"` | Badge color in sidebar | -| `docsUrl` | `string` | External URL to authoritative documentation (MDN, React docs, etc.). Shown as a link icon next to the page title | +| Field | Type | Notes | +| ------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `id` | `string` | URL slug and registry key (`TopicId`) | +| `title` | `string` | Shown in sidebar + page heading | +| `category` | `Category` (`javascript`, `react`, `frameworks`, `backend`, `ai`) | Sidebar group, route prefix, colours. See [categories.md](categories.md) | +| `description` | `string` | Used for SEO metadata and category landing page cards (not shown on visualization page) | +| `difficulty` | `"beginner" \| "intermediate" \| "advanced"` | Badge color in sidebar | +| `docsUrl` | `string` | External URL to authoritative documentation (MDN, React docs, etc.). Shown as a link icon next to the page title | +| `toolbar` | `"selector" \| "simple"` (optional) | Skeleton shown while the toolbar mounts. Defaults to `selector`; set `simple` when there is no example picker | **Types** are defined in `src/types/index.ts`: ```typescript -export type Category = "javascript" | "react"; +export type Category = "javascript" | "react" | "frameworks" | "backend" | "ai"; export type Difficulty = "beginner" | "intermediate" | "advanced"; -export interface Topic { +export interface TopicDefinition { id: string; title: string; category: Category; - route: string; description: string; difficulty: Difficulty; docsUrl: string; + toolbar?: "selector" | "simple"; +} +export interface Topic extends TopicDefinition { + route: string; // derived: `/${category}/${id}` } ``` --- -## Step 2 - Create the Route Page +## Step 2 - Register the Component -**File:** `src/app/javascript/closures/page.tsx` +**File:** `src/components/visualizations/registry.tsx` -Every topic page follows the same template: +Add one entry to `VISUALIZATIONS`, keyed by the topic id: ```typescript -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { ErrorBoundary } from "@/components/ErrorBoundary"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("closures"); - -const ClosuresVisualization = dynamic( - () => - import("@/components/visualizations/closures").then( - (module) => module.Closures - ), - { loading: () => } -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function ClosuresPage() { - return ( - - - - - - ); -} +"closures": dynamic( + () => import("./closures").then((module) => module.Closures), + { loading }, +), ``` -Key points: -- `dynamic()` with named export ensures the client component is code-split. -- `VisualizationLoading` shows a spinner during chunk load. -- `createTopicMetadata()` generates Open Graph and Twitter SEO tags automatically. -- `ErrorBoundary` catches rendering errors gracefully. -- No changes needed here for the toolbar - the shell already includes `ToolbarProvider` + `ToolbarSlot`. +That is the whole routing step. `src/app/[category]/[topic]/page.tsx` calls `generateStaticParams` over the topic registry, builds metadata with `createTopicMetadata`, and renders `VISUALIZATIONS[topic.id]` inside `VisualizationPageShell`. `dynamic()` keeps each topic in its own chunk. --- @@ -204,210 +191,173 @@ Key points: ### Skeleton -```typescript -"use client"; - -import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; -import { CodeBlock, type CodeBlockLine } from "@/components/visualization-ui/CodeBlock"; -import { ConsoleOutput } from "@/components/visualization-ui/ConsoleOutput"; -import { TransportControls } from "@/components/visualization-ui/TransportControls"; -import { ExampleSelector } from "@/components/visualization-ui/ExampleSelector"; // if multi-example -import { ToolbarPortal } from "@/components/layout/ToolbarPortal"; -import { cn } from "@/lib/utils"; -import { - VISUALIZATION_PANEL_TITLES, - VISUALIZATION_EMPTY_STATES, -} from "@/lib/visualization/uiCopy"; -import { useStepPlayback } from "@/hooks/useStepPlayback"; -import { useChangeFlash } from "@/hooks/useChangeFlash"; +Most topics have several examples and use `useExampleTopic`. Split the pieces across `types.ts`, `data.ts`, `helpers.ts`, and `index.tsx` as in the directory structure above. -// 1. Define your source code lines -interface SourceLine { - num: number; - text: string; -} +```typescript +// types.ts +import type { BaseStep, SourceExample } from "@/types/visualization"; -const CODE_LINES: SourceLine[] = [ - { num: 1, text: "function outer() {" }, - { num: 2, text: " let count = 0;" }, - // ... -]; +export type ClosureKind = "counter" | "loop"; -// 2. Define the step interface -interface ClosureStep { - descriptionHtml: string; - activeLine: number | null; - doneLines: number[]; +export interface ClosureStep extends BaseStep { consoleOutput: string[]; + scope: ScopeEntry[]; // ... topic-specific fields } -// 3. Hardcode the STEPS array -const STEPS: ClosureStep[] = [ +export interface ClosureExample extends SourceExample { + kind: ClosureKind; +} +``` + +```typescript +// data.ts +export const EXAMPLES: ClosureExample[] = [ { - descriptionHtml: `Step 1 explanation with inline code and colored terms.`, - activeLine: 1, - doneLines: [], - consoleOutput: [], + id: "counter", + title: "Counter Factory", + description: "An inner function keeps a private count alive.", + kind: "counter", + codeLines: [ + { num: 1, text: "function makeCounter() {" }, + // ... + ], + steps: [ + { + descriptionHtml: `Step 1 explanation with inline code and colored terms.`, + activeLine: 1, + doneLines: [], + consoleOutput: [], + scope: [], + }, + // ... one object per step + ], }, - // ... one object per step ]; +``` -// 4. Export the named component -export function Closures() { - const { - currentStepIndex, - isPlaying, - speedLevel, - speedLabel, - canStep, - canStepBack, - togglePlay, - step: handleStep, - stepBack: handleStepBack, - reset: handleReset, - setSpeedLevel, - } = useStepPlayback({ totalSteps: STEPS.length, initialStep: -1 }); - - const currentStep = currentStepIndex >= 0 ? STEPS[currentStepIndex] : null; +```typescript +// index.tsx +"use client"; + +import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; +import { ConsoleOutput } from "@/components/visualization-ui/ConsoleOutput"; +import { ExamplePicker, KindBadge } from "@/components/visualization-ui/ExamplePicker"; +import { SourceCodePanel } from "@/components/visualization-ui/SourceCodePanel"; +import { SourceGrid, VisualizationSection, WaitingPlaceholder } from "@/components/visualization-ui/VisualizationLayout"; +import { VisualizationToolbar } from "@/components/visualization-ui/VisualizationToolbar"; +import { useExampleTopic } from "@/hooks/useExampleTopic"; +import { useChangeFlash } from "@/hooks/useChangeFlash"; +import { EXAMPLES } from "./data"; +import { kindBadgeClass, kindLabel } from "./helpers"; +import { ScopePanel } from "./components/ScopePanel"; + +export const Closures = () => { + const { example, activeExampleId, handleExampleChange, playback, currentStep } = + useExampleTopic(EXAMPLES); + const { currentStepIndex } = playback; const flashes = useChangeFlash( { description: currentStep?.descriptionHtml, + scope: currentStep?.scope, console: currentStep?.consoleOutput, - // ... add topic-specific channels (heap, roots, scope, etc.) }, currentStepIndex, ); return ( <> - {/* Toolbar: portaled above the surface card */} - -
-
- -
- -
( + {kindLabel(ex.kind)} )} - > - {currentStep ? ( -

- ) : ( -

- {VISUALIZATION_EMPTY_STATES.stepDescription} -

- )} -
-
-
- - {/* Main visualization (inside the surface card) */} -
-
- - { - const isActive = currentStep?.activeLine === line.num; - const isDone = currentStep?.doneLines.includes(line.num) ?? false; - return { - key: line.num, - lineNumber: line.num, - text: line.text, - className: cn( - isActive && "is-active", - isDone && !isActive && "is-done", - ), - }; - })} - /> - + /> + } + /> + + + +
- {/* Topic-specific panels go here */} + + {currentStep ? : } + +
-
-
+ + ); -} +}; ``` +A topic with a single example skips the picker: call `useStepPlayback({ totalSteps: STEPS.length })` directly, omit `leading`, and set `toolbar: "simple"` in the registry. See `src/components/visualizations/closures/index.tsx`. + +What the shared pieces do for you: + +- `useExampleTopic` owns the active example, restarts playback when it changes, and returns the current step typed from your example's `steps`. +- `VisualizationToolbar` renders the transport controls (scrubber included) and the step description pill, and portals both above the card. `hideTransport`, `align`, and `descriptionOverride` cover the sandbox case. +- `SourceCodePanel` derives line classes once per step and never fades the active line. +- `WaitingPlaceholder` is the empty state for a panel before playback starts. + ### Layout Pattern Each visualization returns a **fragment** with two parts: ``` <> - (portaled above the surface card) - controls-row (transport controls, optional selector/badges) - explanation-pill (centered, rounded-full, max-w-4xl) - - -
(inside the surface card) - main-grid (responsive grid with panels) - source-code-panel - topic-specific-panels + (portaled above the surface card) + controls-row (transport controls, optional picker/badges in `leading`) + step-description-pill (centered, rounded-full, max-w-4xl) + + (inside the surface card) + (source on the left, panels on the right at xl) + + topic-specific panels console-output -
+ + ``` -Row alignment convention: -- Without selector: center the controls row (`justify-center`) so controls stay visually anchored. -- With selector: use a two-side row (`justify-between`) with selector/badges on the left and transport controls on the right. +Row alignment is automatic: with `leading` the controls row is `justify-between`, without it the transport is centered. Pass `align="center"` to keep `leading` and the transport together (sandbox topics do this). --- ## Toolbar Portal Pattern -**Files:** `src/components/layout/ToolbarPortal.tsx`, `src/components/layout/VisualizationPageShell.tsx` +**Files:** `src/components/layout/ToolbarPortal.tsx`, `src/components/layout/VisualizationPageShell.tsx`, `src/components/visualization-ui/VisualizationToolbar.tsx` -The toolbar (transport controls + step explanation) is rendered *outside* the surface card to maximize vertical space for visualization panels. This is achieved via a React context + portal pattern: +The toolbar (transport controls + step description) is rendered _outside_ the surface card to maximize vertical space for visualization panels. This is achieved via a React context + portal pattern: 1. **`ToolbarProvider`** - wraps the shell and stores the mount node + toolbar readiness state in context. -2. **`ToolbarSlot`** - placed between header and card in the shell. Renders a skeleton (`simple` or `selector`) until portaled content mounts. +2. **`ToolbarSlot`** - placed between header and card in the shell. Renders a skeleton (`simple` or `selector`, from `topic.toolbar`) until portaled content mounts. 3. **`ToolbarPortal`** - uses `createPortal` to render children into the `ToolbarSlot` from anywhere in the tree and toggles slot readiness. -The shell sets up the provider and slot automatically. Visualizations just wrap their toolbar JSX in ``: - -```typescript -import { ToolbarPortal } from "@/components/layout/ToolbarPortal"; - -// Inside your visualization component: - -
- {/* controls row + explanation pill */} -
-
-``` +`VisualizationToolbar` wraps all of this. Topics do not import `ToolbarPortal` directly. --- @@ -427,22 +377,39 @@ Every visualization is driven by a hardcoded array of step objects. Each step re 5. **No emojis in descriptions or UI text.** Use `` for emphasis instead. +### Simple and Detailed text + +`BaseStep` has an optional `simpleHtml`. When any step of an example sets it, the toolbar shows a Simple / Detailed toggle and readers who pick Simple see `simpleHtml` in the step pill instead of `descriptionHtml`. The choice is stored in the browser and follows the reader across topics. + +Write `simpleHtml` for topics whose detailed text assumes background knowledge (the AI topics all have it). Rules for the simple version: + +- Same step, same numbers, same code line. It is a plainer telling, not a different story. +- Short sentences, everyday words, one analogy at most. "Dial" for a weight, "question" for a query, "budget" for attention weights. +- No jargon without an explanation in the same sentence. +- `` for literal values is fine; the highlight spans are optional. + +Selector topics pass it through with `simpleHtml={currentStep?.simpleHtml}` and `hasSimpleText={hasSimpleText}` from `useExampleTopic`. + +### Checking steps against code + +`npm run audit:steps -- ` loads the topic's data and reports, per example, any step whose `activeLine` is out of range or blank, any step whose active line is also in `doneLines`, and softer hints: active lines on a closing brace or comment, done lines that shrink (normal when a callback body re-runs), three steps in a row with no active line, and code lines that never light up. Errors fail CI; hints are for you to judge. Add `--strict` to also see identifiers named in `` that live on a different line than the active one. + ### Description HTML Highlight Classes Use these inside `descriptionHtml` strings to colorize terms: -| Class | Color | Meaning | -|---|---|---| -| `.hl-stack` | amber | Call stack references | -| `.hl-api` | cyan | Web API references | -| `.hl-task` | green | Task queue references | +| Class | Color | Meaning | +| ----------- | ------ | -------------------------- | +| `.hl-stack` | amber | Call stack references | +| `.hl-api` | cyan | Web API references | +| `.hl-task` | green | Task queue references | | `.hl-micro` | violet | Microtask queue references | -| `.hl-loop` | pink | Event loop references | +| `.hl-loop` | pink | Event loop references | Example: ```typescript -descriptionHtml: `Call Stack receives console.log('Start').` +descriptionHtml: `Call Stack receives console.log('Start').`; ``` ### Inline code in descriptions @@ -479,35 +446,30 @@ When adding a new topic, verify every item: ### All Topics -- [ ] Topic registered in `src/lib/topics.ts` (including `docsUrl`) +- [ ] Topic registered in `src/lib/topics.ts` (including `docsUrl`; `toolbar: "simple"` if there is no example picker) +- [ ] Component registered in `VISUALIZATIONS` (`src/components/visualizations/registry.tsx`) - [ ] Keywords added in `src/lib/metadata.ts` (`TOPIC_KEYWORDS`) -- [ ] Route page created at `src/app///page.tsx` -- [ ] Visualization component created (single file or folder-based) +- [ ] Visualization folder created under `src/components/visualizations//` - [ ] Component uses `"use client"` directive -- [ ] Component is exported as a named export (not default) -- [ ] Component uses `useStepPlayback` hook for playback -- [ ] Toolbar (controls + explanation) wrapped in `` (renders outside surface card) -- [ ] Main visualization in a `
` (renders inside surface card) -- [ ] Component renders `TransportControls` with all required props (`stepIndex` + `totalSteps` for inline step pill) -- [ ] If topic has multiple examples, uses shared `ExampleSelector` (not a custom dropdown) -- [ ] If using `ExampleSelector`, passes `resetKey: activeExampleId` to `useStepPlayback` -- [ ] If using `ExampleSelector`, wraps `setActiveExampleId` in a `handleExampleChange` callback -- [ ] If using `ExampleSelector`, topic added to `SELECTOR_TOOLBAR_TOPIC_IDS` in `VisualizationPageShell.tsx` -- [ ] Source code displayed via `CodeBlock` or `CodeLine` (not raw HTML) +- [ ] Component is exported as a named export (not default), in const arrow form +- [ ] Step type extends `BaseStep`; example type extends `SourceExample` +- [ ] Multi-example topics use `useExampleTopic(EXAMPLES)` and `ExamplePicker` (not a custom dropdown) +- [ ] Toolbar rendered with `VisualizationToolbar` (never hand-rolled) +- [ ] Main visualization in `VisualizationSection` + `SourceGrid` +- [ ] Source code displayed via `SourceCodePanel` - [ ] Panel titles use `VISUALIZATION_PANEL_TITLES` constants (not hardcoded strings) -- [ ] Panels wrapped in `NeonPanel` with appropriate tones +- [ ] Panels wrapped in `NeonPanel` with appropriate tones; empty state via `WaitingPlaceholder` - [ ] Step descriptions use HTML with semantic highlight classes -- [ ] Explanation pill visible at all times (empty state via `VISUALIZATION_EMPTY_STATES.stepDescription`) -- [ ] Code line states use `isDone && !isActive` guard (never fade active lines) - [ ] Active lines removed from `doneLines` when re-executed by callbacks/event loop - [ ] `useChangeFlash` hook called with channels for each panel -- [ ] `viz-change-flash-pill` applied to explanation pill wrapper via `cn()` +- [ ] `descriptionFlash={flashes.description}` passed to `VisualizationToolbar` - [ ] `viz-change-flash` applied to NeonPanels via `className` (no flash-based `key` props) - [ ] Child items use data fingerprints in keys (not step index) for selective `viz-slide-in` re-triggers - [ ] If related topics exist, `TopicLink` shown conditionally on last step of relevant example - [ ] No emojis in step descriptions or UI text - [ ] No em dashes or AI-sounding language in user-facing content -- [ ] `npm run build` passes +- [ ] `npm run audit:steps -- ` reports no errors and the hints make sense +- [ ] `npm run verify` passes (lint, typecheck, format, step audit, build) - [ ] Visual check on desktop and mobile - [ ] All transport controls work: play, pause, step forward, step back, reset, speed change diff --git a/eslint.config.mjs b/eslint.config.mjs index 0644715..0d0b2f2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,6 +9,7 @@ const eslintConfig = defineConfig([ ".next/**", "out/**", "build/**", + "coverage/**", ".claude/**", "next-env.d.ts", ]), diff --git a/next.config.ts b/next.config.ts index 7ea6ee1..6e3d6c9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -15,15 +15,11 @@ const withAnalyzer = withBundleAnalyzer({ }); const nextConfig: NextConfig = { + // Theory used to live on its own page; it now sits under the visualization. redirects: async () => [ { - source: "/javascript/:topic/theory", - destination: "/javascript/:topic", - permanent: true, - }, - { - source: "/react/:topic/theory", - destination: "/react/:topic", + source: "/:category/:topic/theory", + destination: "/:category/:topic", permanent: true, }, ], diff --git a/package-lock.json b/package-lock.json index f598961..ca28b5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,11 +7,19 @@ "": { "name": "visualizejs", "version": "0.1.0", + "license": "MIT", "dependencies": { + "@codemirror/autocomplete": "^6.20.0", "@codemirror/commands": "^6.10.2", "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/language": "^6.12.1", "@codemirror/state": "^6.5.4", "@codemirror/view": "^6.39.14", + "@lezer/highlight": "^1.2.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", "@vercel/analytics": "^1.6.1", "@vercel/speed-insights": "^1.3.1", "acorn": "^8.15.0", @@ -20,7 +28,6 @@ "lucide-react": "^0.564.0", "next": "16.1.6", "prettier": "^3.8.1", - "radix-ui": "^1.4.3", "react": "19.2.3", "react-dom": "19.2.3", "tailwind-merge": "^3.4.0" @@ -28,17 +35,42 @@ "devDependencies": { "@next/bundle-analyzer": "^16.1.6", "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.2", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^9", "eslint-config-next": "16.1.6", + "jsdom": "^27.4.0", "shadcn": "^3.8.4", "tailwindcss": "^4", + "tsx": "^4.23.13", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.11" + }, + "engines": { + "node": ">=20" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -74,6 +106,61 @@ "nup": "bin/nup.mjs" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -311,9 +398,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -321,9 +408,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -355,13 +442,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -459,6 +546,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -494,19 +591,29 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@codemirror/autocomplete": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", @@ -592,6 +699,146 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.0.tgz", + "integrity": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.3.tgz", + "integrity": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.4.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", + "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.1.tgz", + "integrity": "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -847,610 +1094,696 @@ "tslib": "^2.4.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", - "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.5" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "netbsd" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "netbsd" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "openbsd" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "openbsd" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "sunos" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ - "x64" + "ia32" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ - "arm" + "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/eslint" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -1459,20 +1792,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, - "node_modules/@img/sharp-linuxmusl-x64": { + "node_modules/@img/sharp-darwin-x64": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -1481,87 +1814,423 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ - "wasm32" + "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "LGPL-3.0-or-later", "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, + "os": [ + "darwin" + ], "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ - "arm64" + "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "darwin" ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ - "ia32" + "arm" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "linux" ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ - "x64" + "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "linux" ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", "dev": true, @@ -2114,6 +2783,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@oxc-project/types": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -2133,72 +2812,56 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, - "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", - "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", - "dependencies": { - "@radix-ui/react-visually-hidden": "1.2.3" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", - "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-alert-dialog": { + "node_modules/@radix-ui/react-dialog": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", - "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -2215,36 +2878,32 @@ } } }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", - "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2261,47 +2920,30 @@ } } }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", - "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2318,46 +2960,32 @@ } } }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2374,48 +3002,14 @@ } } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", - "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2432,26 +3026,13 @@ } } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -2468,32 +3049,21 @@ } } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", "license": "MIT", "dependencies": { + "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2510,19 +3080,13 @@ } } }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -2539,11 +3103,14 @@ } } }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2554,98 +3121,81 @@ } } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-form": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", - "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", - "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-id": { + "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2656,684 +3206,684 @@ } } }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", + "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", + "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", + "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", + "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", + "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", + "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", + "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", + "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", + "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", + "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", + "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", + "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", + "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", + "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", - "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", + "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", - "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=18" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", - "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", - "license": "MIT", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "tslib": "^2.8.0" } }, - "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", - "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">= 10" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" } }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18" } }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" }, "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { + "vitest": { "optional": true } } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3344,378 +3894,506 @@ } } }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "node_modules/@testing-library/user-event": { + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" + "engines": { + "node": ">=12", + "npm": ">=6" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@ts-morph/common": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", + "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.3", + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/balanced-match": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", + "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jackspeak": "^4.2.3" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "20 || >=22" } }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", - "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-toggle-group": "1.1.11" + "balanced-match": "^4.0.2" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@ts-morph/common/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=8.6.0" } }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "node_modules/@ts-morph/common/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", + "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validate-npm-package-name": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", + "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", + "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/type-utils": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@typescript-eslint/parser": "^8.55.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">= 4" } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "node_modules/@typescript-eslint/parser": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", + "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", + "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" + "@typescript-eslint/tsconfig-utils": "^8.55.0", + "@typescript-eslint/types": "^8.55.0", + "debug": "^4.4.3" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", + "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", + "dev": true, "license": "MIT", "dependencies": { - "use-sync-external-store": "^1.5.0" + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", + "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", + "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.1" + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "node_modules/@typescript-eslint/types": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", + "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", + "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@typescript-eslint/project-service": "8.55.0", + "@typescript-eslint/tsconfig-utils": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "node_modules/@typescript-eslint/typescript-estree/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, - "license": "MIT" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "node_modules/@typescript-eslint/typescript-estree/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, - "license": "MIT", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", + "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.1", - "lightningcss": "1.30.2", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", + "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "eslint-visitor-keys": "^4.2.1" + }, "engines": { - "node": ">= 10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" ], - "engines": { - "node": ">= 10" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", "cpu": [ "arm64" ], @@ -3724,15 +4402,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", "cpu": [ "x64" ], @@ -3741,15 +4416,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", "cpu": [ "x64" ], @@ -3758,15 +4430,26 @@ "optional": true, "os": [ "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" ], - "engines": { - "node": ">= 10" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", "cpu": [ "arm" ], @@ -3775,49 +4458,96 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", "cpu": [ - "arm64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", "cpu": [ "x64" ], @@ -3826,15 +4556,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", "cpu": [ "x64" ], @@ -3843,23 +4570,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", "cpu": [ "wasm32" ], @@ -3867,21 +4583,16 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "@napi-rs/wasm-runtime": "^0.2.11" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", "cpu": [ "arm64" ], @@ -3890,15 +4601,26 @@ "optional": true, "os": [ "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" ], - "engines": { - "node": ">= 10" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", "cpu": [ "x64" ], @@ -3907,1056 +4629,1330 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">= 10" + ] + }, + "node_modules/@vercel/analytics": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", + "integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==", + "license": "MPL-2.0", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } } }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "node_modules/@vercel/speed-insights": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.3.1.tgz", + "integrity": "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "postcss": "^8.4.41", - "tailwindcss": "4.1.18" + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.11", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@ts-morph/common": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", - "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "fast-glob": "^3.3.3", - "minimatch": "^10.0.1", - "path-browserify": "^1.0.1" + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@ts-morph/common/node_modules/balanced-match": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", - "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", "dependencies": { - "jackspeak": "^4.2.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": "20 || >=22" + "node": ">= 0.6" } }, - "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": "20 || >=22" + "node": ">=0.4.0" } }, - "node_modules/@ts-morph/common/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "acorn": "^8.11.0" }, "engines": { - "node": ">=8.6.0" + "node": ">=0.4.0" } }, - "node_modules/@ts-morph/common/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==", + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, + "license": "MIT", "engines": { - "node": ">= 6" + "node": ">= 14" } }, - "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", - "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "20 || >=22" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "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, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } }, - "node_modules/@types/node": { - "version": "20.19.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", - "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", "license": "MIT", "dependencies": { - "csstype": "^3.2.2" + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, - "node_modules/@types/statuses": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", - "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@types/validate-npm-package-name": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", - "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", - "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/type-utils": "8.55.0", - "@typescript-eslint/utils": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.55.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, "engines": { - "node": ">= 4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", - "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", - "debug": "^4.4.3" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", - "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.55.0", - "@typescript-eslint/types": "^8.55.0", - "debug": "^4.4.3" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", - "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0" + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", - "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "engines": { + "node": ">=4" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", - "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.6.tgz", + "integrity": "sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0", - "@typescript-eslint/utils": "8.55.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", - "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "dependencies": { + "require-from-string": "^2.0.2" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", - "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.55.0", - "@typescript-eslint/tsconfig-utils": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", + "bytes": "^3.1.2", + "content-type": "^1.0.5", "debug": "^4.4.3", - "minimatch": "^9.0.5", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/express" } }, - "node_modules/@typescript-eslint/typescript-estree/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==", + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, - "license": "ISC", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, "bin": { - "semver": "bin/semver.js" + "browserslist": "cli.js" }, "engines": { - "node": ">=10" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", - "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0" + "run-applescript": "^7.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", - "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "eslint-visitor-keys": "^4.2.1" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": ">=6" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "ISC", + "engines": { + "node": ">= 12" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=8" + } }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">=6" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vercel/analytics": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", - "integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==", - "license": "MPL-2.0", - "peerDependencies": { - "@remix-run/react": "^2", - "@sveltejs/kit": "^1 || ^2", - "next": ">= 13", - "react": "^18 || ^19 || ^19.0.0-rc", - "svelte": ">= 4", - "vue": "^3", - "vue-router": "^4" + "dependencies": { + "color-name": "~1.1.4" }, - "peerDependenciesMeta": { - "@remix-run/react": { - "optional": true - }, - "@sveltejs/kit": { - "optional": true - }, - "next": { - "optional": true - }, - "react": { - "optional": true - }, - "svelte": { - "optional": true - }, - "vue": { - "optional": true - }, - "vue-router": { - "optional": true - } + "engines": { + "node": ">=7.0.0" } }, - "node_modules/@vercel/speed-insights": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.3.1.tgz", - "integrity": "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==", - "license": "Apache-2.0", - "peerDependencies": { - "@sveltejs/kit": "^1 || ^2", - "next": ">= 13", - "react": "^18 || ^19 || ^19.0.0-rc", - "svelte": ">= 4", - "vue": "^3", - "vue-router": "^4" - }, - "peerDependenciesMeta": { - "@sveltejs/kit": { - "optional": true - }, - "next": { - "optional": true - }, - "react": { - "optional": true - }, - "svelte": { - "optional": true - }, - "vue": { - "optional": true - }, - "vue-router": { - "optional": true - } - } + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, "engines": { - "node": ">= 0.6" + "node": ">=20" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.6" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14" + "node": ">=6.6.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "ajv": "^8.0.0" + "typescript": ">=4.9.5" }, "peerDependenciesMeta": { - "ajv": { + "typescript": { "optional": true } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">= 8" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/ansis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", - "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, "engines": { - "node": ">=14" + "node": ">=4" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", "license": "MIT", "dependencies": { - "tslib": "^2.0.0" + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" }, "engines": { - "node": ">=10" + "node": ">=20" } }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, - "license": "Apache-2.0", + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 0.4" + "node": "20 || >=22" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 12" } }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=20" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=20" } }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4965,36 +5961,34 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -5003,1332 +5997,1294 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.4" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/ast-types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" }, - "engines": { - "node": ">=4" + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } } }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { - "possible-typed-array-names": "^1.0.0" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">= 0.8" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", "engines": { "node": ">=8" } }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, + "license": "BSD-3-Clause", "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=0.3.1" } }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "run-applescript": "^7.0.0" + "esutils": "^2.0.2" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } + "license": "MIT" }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://dotenvx.com" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "gopd": "^1.2.0" }, "engines": { "node": ">= 0.4" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eciesjs": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.17.tgz", + "integrity": "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "@ecies/ciphers": "^0.2.5", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bun": ">=1", + "deno": ">=2", + "node": ">=16" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } + "license": "MIT" }, - "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" + "node": ">= 0.8" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^5.0.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10.13.0" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=6" + "node": ">=20.19.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=6" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" + "is-arrayish": "^0.2.1" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/code-block-writer": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", - "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "es-errors": "^1.3.0" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.4" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, "engines": { - "node": ">=20" + "node": ">= 0.4" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } + "license": "MIT" }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, "engines": { - "node": ">= 0.10" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=14" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" + "url": "https://eslint.org/donate" }, "peerDependencies": { - "typescript": ">=4.9.5" + "jiti": "*" }, "peerDependenciesMeta": { - "typescript": { + "jiti": { "optional": true } } }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/eslint-config-next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", + "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@next/eslint-plugin-next": "16.1.6", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "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==", + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 12" + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "ms": "^2.1.1" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" }, "engines": { - "node": ">= 0.4" + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/inspect-js" + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "debug": "^3.2.7" }, "engines": { - "node": ">= 0.4" + "node": ">=4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" }, "engines": { - "node": ">=6.0" + "node": ">=4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/dedent": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", - "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } + "dependencies": { + "ms": "^2.1.1" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">=4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" }, - "engines": { - "node": ">= 0.4" + "bin": { + "resolve": "bin/resolve" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "license": "Apache-2.0", + "license": "BSD-2-Clause", "dependencies": { - "esutils": "^2.0.2" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", - "engines": { - "node": ">=12" + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, - "funding": { - "url": "https://dotenvx.com" + "engines": { + "node": ">=4" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eciesjs": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.17.tgz", - "integrity": "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@ecies/ciphers": "^0.2.5", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.9.7", - "@noble/hashes": "^1.8.0" + "estraverse": "^5.2.0" }, "engines": { - "bun": ">=1", - "deno": ">=2", - "node": ">=16" + "node": ">=4.0" } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", - "dev": true, - "license": "ISC" - }, - "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, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.8" + "node": ">=4.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" + "@types/estree": "^1.0.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" + "engines": { + "node": ">= 0.6" } }, - "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" + "eventsource-parser": "^3.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18.0.0" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18.0.0" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", "dev": true, "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.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.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, "engines": { - "node": ">= 0.4" + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/es-iterator-helpers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "safe-array-concat": "^1.1.3" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" + "node": ">=12.0.0" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.4" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "ip-address": "10.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" } }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "engines": { - "node": ">= 0.4" + "node": ">=8.6.0" } }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "is-glob": "^4.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } + "license": "MIT" }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "reusify": "^1.0.4" } }, - "node_modules/eslint-config-next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", - "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.1.6", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^7.0.0", - "globals": "16.4.0", - "typescript-eslint": "^8.46.0" - }, - "peerDependencies": { - "eslint": ">=9.0.0", - "typescript": ">=3.3.1" + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^12.20 || >= 14.13" } }, - "node_modules/eslint-config-next/node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", "dev": true, "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, "engines": { "node": ">=18" }, @@ -6336,850 +7292,790 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": ">= 18.0.0" }, "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^3.2.7" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" } }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" + "is-callable": "^1.2.7" }, "engines": { - "node": ">=4" + "node": ">= 0.4" }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" } }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + "node": ">= 0.6" } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + "node": ">= 0.8" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "node": ">=14.14" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fzf": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", + "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, + "license": "ISC", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "node_modules/get-own-enumerable-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", + "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" + "node": ">= 0.4" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", "dev": true, "license": "MIT", "dependencies": { - "eventsource-parser": "^3.0.1" + "resolve-pkg-maps": "^1.0.0" }, - "engines": { - "node": ">=18.0.0" + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, "engines": { - "node": ">=18.0.0" + "node": ">=10.13.0" } }, - "node_modules/execa": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.6", - "figures": "^6.1.0", - "get-stream": "^9.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.2.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.1.1" - }, "engines": { - "node": "^18.19.0 || >=20.5.0" + "node": ">=18" }, "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { - "node": ">= 18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "dependencies": { - "ip-address": "10.0.1" - }, "engines": { - "node": ">= 16" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "node_modules/graphql": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, "engines": { - "node": ">=8.6.0" + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "duplexer": "^0.1.2" }, "engines": { - "node": ">= 6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "reusify": "^1.0.4" + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], "license": "MIT", "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" + "dunder-proto": "^1.0.0" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0" - }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">=16.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "hermes-estree": "0.25.1" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/hono": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "dev": true, "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=16.9.0" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=16" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.2.7" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { - "fetch-blob": "^3.1.2" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=12.20.0" + "node": ">= 14" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, "engines": { - "node": ">= 0.6" + "node": ">= 14" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">= 0.8" + "node": ">=18.18.0" } }, - "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=14.14" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 4" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.8.19" } }, - "node_modules/fuzzysort": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", - "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/fzf": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", - "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "ISC" }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, "engines": { "node": ">= 0.4" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">= 12" } }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.10" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -7188,69 +8084,58 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-own-enumerable-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", - "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" + "has-bigints": "^1.0.2" }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7259,54 +8144,50 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "semver": "^7.7.1" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=10.13.0" + "node": ">=10" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -7315,12 +8196,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, "engines": { "node": ">= 0.4" }, @@ -7328,83 +8214,57 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { - "duplexer": "^0.1.2" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, "engines": { - "node": ">= 0.4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.0" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -7413,27 +8273,28 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -7442,211 +8303,227 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/headers-polyfill": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", "dependencies": { - "hermes-estree": "0.25.1" + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16.9.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, "engines": { - "node": ">= 14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/human-signals": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", - "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=18.18.0" + "node": ">=0.12.0" } }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "dev": true, "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, "engines": { - "node": ">=6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": ">=0.10.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "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", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -7655,41 +8532,28 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "dev": true, "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { - "has-bigints": "^1.0.2" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7698,15 +8562,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -7715,51 +8580,41 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", - "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-bun-module/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "which-typed-array": "^1.1.16" }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, "engines": { "node": ">= 0.4" }, @@ -7767,16 +8622,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -7785,15 +8638,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -7802,2113 +8655,2334 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "license": "MIT", - "bin": { - "is-docker": "cli.js" + "dependencies": { + "is-inside-container": "^1.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "call-bound": "^1.0.3" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "is-extglob": "^2.1.1" + "@isaacs/cliui": "^9.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/is-in-ssh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", - "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=20" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/panva" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { - "is-docker": "^3.0.0" + "argparse": "^2.0.1" }, "bin": { - "is-inside-container": "cli.js" + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14.16" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "node_modules/jsdom/node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "BSD-2-Clause" }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": ">=0.12.0" + "node": ">=6" } }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" + "universalify": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/is-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", - "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=4.0" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "json-buffer": "3.0.1" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", "dev": true, - "license": "MIT" + "license": "CC0-1.0" }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "language-subtag-registry": "^0.3.20" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10" } }, - "node_modules/is-regexp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", - "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8.0" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", "dependencies": { - "call-bound": "^1.0.3" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" } }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "20 || >=22" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } + "license": "MIT" }, - "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, "funding": { - "url": "https://github.com/sponsors/panva" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/log-symbols/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, "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, "engines": { - "node": ">=6" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", "dependencies": { - "universalify": "^2.0.0" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" + "yallist": "^3.0.2" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "node_modules/lucide-react": { + "version": "0.564.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.564.0.tgz", + "integrity": "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "bin": { + "lz-string": "bin/bin.js" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" } }, - "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" + "semver": "^7.5.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">= 12.0.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=10" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 0.4" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 0.8" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=8.6" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], + "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, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 0.6" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=18" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/express" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=6" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": "*" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "node_modules/msw": { + "version": "2.12.10", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.10.tgz", + "integrity": "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.41.2", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.0.2", + "graphql": "^16.12.0", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.10.1", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^5.2.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" }, "engines": { "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/log-symbols/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==", + "node_modules/msw/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "dev": true, "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, "engines": { - "node": ">=12" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/napi-postinstall" } }, - "node_modules/loose-envify": { + "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "license": "MIT", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "@next/env": "16.1.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" }, "bin": { - "loose-envify": "cli.js" + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "yallist": "^3.0.2" + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "node_modules/lucide-react": { - "version": "0.564.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.564.0.tgz", - "integrity": "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", "dev": true, "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, "engines": { - "node": ">=8.6" + "node": ">= 0.4" } }, - "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==", + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 10" } }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" }, "engines": { - "node": "*" + "node": ">= 0.4" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12.20.0" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/msw": { - "version": "2.12.10", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.10.tgz", - "integrity": "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==", + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { - "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.41.2", - "@open-draft/deferred-promise": "^2.2.0", - "@types/statuses": "^2.0.6", - "cookie": "^1.0.2", - "graphql": "^16.12.0", - "headers-polyfill": "^4.0.2", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "path-to-regexp": "^6.3.0", - "picocolors": "^1.1.1", - "rettime": "^0.10.1", - "statuses": "^2.0.2", - "strict-event-emitter": "^0.5.1", - "tough-cookie": "^6.0.0", - "type-fest": "^5.2.0", - "until-async": "^3.0.2", - "yargs": "^17.7.2" - }, - "bin": { - "msw": "cli/index.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mswjs" - }, - "peerDependencies": { - "typescript": ">= 4.8.x" + "ee-first": "1.1.1" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/msw/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.8" } - }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" + "dependencies": { + "wrappy": "1" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "mimic-function": "^5.0.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", "dev": true, "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": ">=20" }, "funding": { - "url": "https://opencollective.com/napi-postinstall" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true, - "license": "MIT" + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8.0" } }, - "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, "license": "MIT", "dependencies": { - "@next/env": "16.1.6", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", - "sharp": "^0.34.4" + "node": ">=18" }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, "license": "MIT", "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, "engines": { - "node": ">=10.5.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "p-limit": "^3.0.2" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } }, - "node_modules/npm-run-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^4.0.0", - "unicorn-magic": "^0.3.0" + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-run-path/node_modules/path-key": { + "node_modules/parse-ms": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "entities": "^8.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 0.8" } }, - "node_modules/object-treeify": { - "version": "1.1.33", - "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", - "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } + "license": "MIT" }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=16.20.0" } }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "ee-first": "1.1.1" + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">= 0.8" + "node": "^10 || ^12 || >=14" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "wrappy": "1" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", "dev": true, "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", - "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "dependencies": { - "default-browser": "^5.4.0", - "define-lazy-prop": "^3.0.0", - "is-in-ssh": "^1.0.0", - "is-inside-container": "^1.0.0", - "powershell-utils": "^0.1.0", - "wsl-utils": "^0.3.0" + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=20" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true, - "license": "(WTFPL OR MIT)", - "bin": { - "opener": "bin/opener-bin.js" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/ora/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==", + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/outvariant": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, "license": "MIT" }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "node_modules/pretty-ms": { + "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, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" + "parse-ms": "^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 6" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, - "node_modules/package-manager-detector": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", - "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" }, "engines": { - "node": ">=6" + "node": ">= 0.10" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "side-channel": "^1.1.0" }, "engines": { - "node": ">=8" + "node": ">=0.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true, + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, "engines": { - "node": ">=8.6" + "node": ">=10" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", "dev": true, "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, "engines": { - "node": ">=16.20.0" + "node": ">= 4" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/powershell-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", - "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, "bin": { - "prettier": "bin/prettier.cjs" + "resolve": "bin/resolve" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, "engines": { - "node": ">=18" - }, + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">= 6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/prompts/node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "node_modules/rettime": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", + "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", + "dev": true, + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "node_modules/rolldown": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", + "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", "dev": true, "license": "MIT", "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" + "@oxc-project/types": "=0.150.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.9", + "@rolldown/binding-android-arm64": "1.2.9", + "@rolldown/binding-darwin-arm64": "1.2.9", + "@rolldown/binding-darwin-x64": "1.2.9", + "@rolldown/binding-freebsd-x64": "1.2.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", + "@rolldown/binding-linux-arm64-gnu": "1.2.9", + "@rolldown/binding-linux-arm64-musl": "1.2.9", + "@rolldown/binding-linux-ppc64-gnu": "1.2.9", + "@rolldown/binding-linux-s390x-gnu": "1.2.9", + "@rolldown/binding-linux-x64-gnu": "1.2.9", + "@rolldown/binding-linux-x64-musl": "1.2.9", + "@rolldown/binding-openharmony-arm64": "1.2.9", + "@rolldown/binding-win32-arm64-msvc": "1.2.9", + "@rolldown/binding-win32-x64-msvc": "1.2.9" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, "license": "MIT", "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 18" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, + "license": "MIT", "engines": { - "node": ">=0.6" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -9924,459 +10998,456 @@ "url": "https://feross.org/support" } ], - "license": "MIT" - }, - "node_modules/radix-ui": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", - "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-accessible-icon": "1.1.7", - "@radix-ui/react-accordion": "1.2.12", - "@radix-ui/react-alert-dialog": "1.1.15", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-aspect-ratio": "1.1.7", - "@radix-ui/react-avatar": "1.1.10", - "@radix-ui/react-checkbox": "1.3.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-context-menu": "2.2.16", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-form": "0.1.8", - "@radix-ui/react-hover-card": "1.1.15", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-menubar": "1.1.16", - "@radix-ui/react-navigation-menu": "1.2.14", - "@radix-ui/react-one-time-password-field": "0.1.8", - "@radix-ui/react-password-toggle-field": "0.1.3", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-progress": "1.1.7", - "@radix-ui/react-radio-group": "1.3.8", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-scroll-area": "1.2.10", - "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-slider": "1.3.6", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-switch": "1.2.6", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toast": "1.2.15", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-toggle-group": "1.1.11", - "@radix-ui/react-toolbar": "1.1.11", - "@radix-ui/react-tooltip": "1.2.8", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-escape-keydown": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "queue-microtask": "^1.2.2" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, "engines": { - "node": ">= 0.6" + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" + "es-errors": "^1.3.0", + "isarray": "^2.0.5" }, "engines": { - "node": ">= 0.10" - } - }, - "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, - "peerDependencies": { - "react": "^19.2.3" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" + "xmlchars": "^2.2.0" }, "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">=v12.22.7" } }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, "license": "MIT", "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node": ">= 18" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, "license": "MIT", "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + "node": ">= 18" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/recast": { - "version": "0.23.11", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", - "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">= 4" + "node": ">= 0.4" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", + "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shadcn": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-3.8.4.tgz", + "integrity": "sha512-pSad/m1+PGzB0aLsRBV0EkyGg9al1nJqYUuucg6d8v8xZspPZ5/ehGNEp5M4b1KQYqdO5/gGPbkhVbgmXqG9Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/ni": "^25.0.0", + "@babel/core": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-typescript": "^7.27.1", + "@dotenvx/dotenvx": "^1.48.4", + "@modelcontextprotocol/sdk": "^1.26.0", + "@types/validate-npm-package-name": "^4.0.2", + "browserslist": "^4.26.2", + "commander": "^14.0.0", + "cosmiconfig": "^9.0.0", + "dedent": "^1.6.0", + "deepmerge": "^4.3.1", + "diff": "^8.0.2", + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.1", + "fuzzysort": "^3.1.0", + "https-proxy-agent": "^7.0.6", + "kleur": "^4.1.5", + "msw": "^2.10.4", + "node-fetch": "^3.3.2", + "open": "^11.0.0", + "ora": "^8.2.0", + "postcss": "^8.5.6", + "postcss-selector-parser": "^7.1.0", + "prompts": "^2.4.2", + "recast": "^0.23.11", + "stringify-object": "^5.0.0", + "tailwind-merge": "^3.0.1", + "ts-morph": "^26.0.0", + "tsconfig-paths": "^4.2.0", + "validate-npm-package-name": "^7.0.1", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "shadcn": "dist/index.js" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "node_modules/shadcn/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8.6.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "node_modules/shadcn/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "node_modules/shadcn/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "node_modules/shadcn/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, "engines": { - "node": ">=4" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "node_modules/sharp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/rettime": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", - "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", - "dev": true, - "license": "MIT" - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", + "node": ">= 0.4" + }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=0.4" + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "isarray": "^2.0.5" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -10385,402 +11456,401 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 10" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true, "license": "MIT" }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, "license": "MIT" }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, "engines": { - "node": ">= 18" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" }, "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.4" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" }, "engines": { "node": ">= 0.4" } }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shadcn": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-3.8.4.tgz", - "integrity": "sha512-pSad/m1+PGzB0aLsRBV0EkyGg9al1nJqYUuucg6d8v8xZspPZ5/ehGNEp5M4b1KQYqdO5/gGPbkhVbgmXqG9Pw==", + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", "dependencies": { - "@antfu/ni": "^25.0.0", - "@babel/core": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/plugin-transform-typescript": "^7.28.0", - "@babel/preset-typescript": "^7.27.1", - "@dotenvx/dotenvx": "^1.48.4", - "@modelcontextprotocol/sdk": "^1.26.0", - "@types/validate-npm-package-name": "^4.0.2", - "browserslist": "^4.26.2", - "commander": "^14.0.0", - "cosmiconfig": "^9.0.0", - "dedent": "^1.6.0", - "deepmerge": "^4.3.1", - "diff": "^8.0.2", - "execa": "^9.6.0", - "fast-glob": "^3.3.3", - "fs-extra": "^11.3.1", - "fuzzysort": "^3.1.0", - "https-proxy-agent": "^7.0.6", - "kleur": "^4.1.5", - "msw": "^2.10.4", - "node-fetch": "^3.3.2", - "open": "^11.0.0", - "ora": "^8.2.0", - "postcss": "^8.5.6", - "postcss-selector-parser": "^7.1.0", - "prompts": "^2.4.2", - "recast": "^0.23.11", - "stringify-object": "^5.0.0", - "tailwind-merge": "^3.0.1", - "ts-morph": "^26.0.0", - "tsconfig-paths": "^4.2.0", - "validate-npm-package-name": "^7.0.1", - "zod": "^3.24.1", - "zod-to-json-schema": "^3.24.6" - }, - "bin": { - "shadcn": "dist/index.js" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, - "node_modules/shadcn/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/shadcn/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" + "node": ">= 0.4" }, - "engines": { - "node": ">= 6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shadcn/node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shadcn/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sharp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" + "node_modules/stringify-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", + "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" }, "engines": { - "node": ">=10" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/stringify-object?sponsor=1" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "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, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/shebang-regex": { + "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "min-indent": "^1.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" + "client-only": "0.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } } }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" + "has-flag": "^4.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -10788,380 +11858,325 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=20" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "dev": true, "license": "MIT" }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=18" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=18" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, "engines": { - "node": ">=18" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, "engines": { - "node": ">= 0.4" + "node": ">=14.0.0" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "node_modules/tldts": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" + "tldts-core": "^7.0.23" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "tldts": "bin/cli.js" } }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "node_modules/tldts-core": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } + "license": "MIT" }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "is-number": "^7.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8.0" } }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.6" } }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/stringify-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", - "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", "dev": true, - "license": "BSD-2-Clause", + "license": "BSD-3-Clause", "dependencies": { - "get-own-enumerable-keys": "^1.0.0", - "is-obj": "^3.0.0", - "is-regexp": "^3.1.0" + "tldts": "^7.0.5" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/yeoman/stringify-object?sponsor=1" + "node": ">=16" } }, - "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/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=20" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/strip-final-newline": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "node_modules/ts-morph": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", + "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@ts-morph/common": "~0.27.0", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "minimist": "^1.2.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "json5": "lib/cli.js" } }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "license": "MIT" + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, "license": "MIT", "dependencies": { - "client-only": "0.0.1" + "esbuild": "~0.28.0" }, - "engines": { - "node": ">= 12.0.0" + "bin": { + "tsx": "dist/cli.mjs" }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + "engines": { + "node": ">=18.0.0" }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "prelude-ls": "^1.2.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", "dev": true, - "license": "MIT", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { "node": ">=20" }, @@ -11169,616 +12184,817 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "engines": { + "node": ">= 0.6" } }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, "engines": { - "node": ">=18" + "node": ">= 0.4" } }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { - "node": ">=12.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.23" - }, + "license": "Apache-2.0", "bin": { - "tldts": "bin/cli.js" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } }, - "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "node_modules/typescript-eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz", + "integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.55.0", + "@typescript-eslint/parser": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" }, "engines": { - "node": ">=8.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } + "license": "MIT" }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">= 10.0.0" } }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" + "node": ">= 0.8" } }, - "node_modules/ts-morph": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", - "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", "dev": true, + "hasInstallScript": true, "license": "MIT", "dependencies": { - "@ts-morph/common": "~0.27.0", - "code-block-writer": "^13.0.3" + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", "dev": true, "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "funding": { + "url": "https://github.com/sponsors/kettanaito" } }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "minimist": "^1.2.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { - "json5": "lib/cli.js" + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tw-animate-css": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", - "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Wombosvideo" + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1" + "tslib": "^2.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/type-fest": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", - "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", - "dev": true, - "license": "(MIT OR CC0-1.0)", + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", "dependencies": { - "tagged-tag": "^1.0.0" + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" }, "engines": { - "node": ">=20" + "node": ">=10" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", "engines": { - "node": ">= 0.6" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "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, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8" } }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" }, "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14.17" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/typescript-eslint": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz", - "integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==", + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.55.0", - "@typescript-eslint/parser": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0", - "@typescript-eslint/utils": "8.55.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/parcel" } }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 10.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.8" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/until-async": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", - "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, "funding": { - "url": "https://github.com/sponsors/kettanaito" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" + "engines": { + "node": ">= 12.0.0" }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + "node": ">=12" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, "license": "MIT", "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" }, "engines": { - "node": ">=10" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "@types/react": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/validate-npm-package-name": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", - "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.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==", + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/w3c-keyname": { @@ -11787,6 +13003,19 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -11797,6 +13026,16 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, "node_modules/webpack-bundle-analyzer": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", @@ -11835,6 +13074,30 @@ "node": ">= 10" } }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11940,6 +13203,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -12056,6 +13336,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 4b8b0ae..84acad0 100644 --- a/package.json +++ b/package.json @@ -29,14 +29,27 @@ "build": "next build --webpack", "start": "next start", "lint": "eslint", - "check:registries": "node scripts/check-registries.mjs", - "verify": "npm run lint && npm run check:registries && npm run build" + "verify": "npm run lint && npm run typecheck && npm run format:check && npm run test && npm run audit:steps && npm run build", + "format": "prettier --write .", + "format:check": "prettier --check .", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "audit:steps": "tsx --tsconfig tsconfig.json scripts/audit-steps.mts" }, "dependencies": { + "@codemirror/autocomplete": "^6.20.0", "@codemirror/commands": "^6.10.2", "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/language": "^6.12.1", "@codemirror/state": "^6.5.4", "@codemirror/view": "^6.39.14", + "@lezer/highlight": "^1.2.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", "@vercel/analytics": "^1.6.1", "@vercel/speed-insights": "^1.3.1", "acorn": "^8.15.0", @@ -45,7 +58,6 @@ "lucide-react": "^0.564.0", "next": "16.1.6", "prettier": "^3.8.1", - "radix-ui": "^1.4.3", "react": "19.2.3", "react-dom": "19.2.3", "tailwind-merge": "^3.4.0" @@ -53,14 +65,22 @@ "devDependencies": { "@next/bundle-analyzer": "^16.1.6", "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.2", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^9", "eslint-config-next": "16.1.6", + "jsdom": "^27.4.0", "shadcn": "^3.8.4", "tailwindcss": "^4", + "tsx": "^4.23.13", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.11" } } diff --git a/public/icons/ai.svg b/public/icons/ai.svg new file mode 100644 index 0000000..fa80450 --- /dev/null +++ b/public/icons/ai.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/backend.svg b/public/icons/backend.svg new file mode 100644 index 0000000..e402776 --- /dev/null +++ b/public/icons/backend.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/frameworks.svg b/public/icons/frameworks.svg new file mode 100644 index 0000000..77802a7 --- /dev/null +++ b/public/icons/frameworks.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/audit-steps.mts b/scripts/audit-steps.mts new file mode 100644 index 0000000..3387d5a --- /dev/null +++ b/scripts/audit-steps.mts @@ -0,0 +1,50 @@ +/** + * Checks that every step's activeLine and doneLines make sense against the + * topic's code lines. Catches lines out of range, active lines that sit on a + * brace or comment, done lines that shrink between steps, long runs with no + * active line, and code lines that never light up. + * + * Run: npm run audit:steps (all topics) + * npm run audit:steps -- attention closures + * npm run audit:steps -- --strict (also flag identifiers named in the + * description that live on another line) + * + * Exit code is 1 when any topic has errors, so it can run in CI. + * The rules live in scripts/lib/stepAudit.ts so they can be unit tested. + */ +import { existsSync, readdirSync } from "node:fs"; +import { resolve } from "node:path"; +import { + auditExample, + flattenExamples, + type TopicDataModule, +} from "./lib/stepAudit"; + +const ROOT = resolve(process.cwd(), "src/components/visualizations"); +const args = process.argv.slice(2); +const strict = args.includes("--strict"); +const only = new Set(args.filter((arg) => !arg.startsWith("--"))); + +const loadExamples = async (dir: string) => { + const file = resolve(ROOT, dir, "data.ts"); + if (!existsSync(file)) return []; + const mod = (await import(file)) as TopicDataModule; + return flattenExamples(mod); +}; + +let failed = false; +for (const dir of readdirSync(ROOT).sort()) { + if (only.size && !only.has(dir)) continue; + const examples = await loadExamples(dir); + if (!examples.length) continue; + const findings = examples.map((example) => auditExample(example, { strict })); + const errors = findings.flatMap((f) => f.errors); + const warnings = findings.flatMap((f) => f.warnings); + const steps = examples.reduce((n, e) => n + e.steps.length, 0); + const status = errors.length ? "!!" : warnings.length ? "~ " : "ok"; + console.log(`${status} ${dir} (${examples.length} examples, ${steps} steps)`); + for (const error of errors) console.log(` error: ${error}`); + for (const warning of warnings) console.log(` warn: ${warning}`); + if (errors.length) failed = true; +} +process.exitCode = failed ? 1 : 0; diff --git a/scripts/check-registries.mjs b/scripts/check-registries.mjs deleted file mode 100644 index 9724681..0000000 --- a/scripts/check-registries.mjs +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env node -/** - * Verifies that every topic is registered everywhere it needs to be. - * - * Adding a topic touches eight places. Missing one produces a page that half - * works: a route with no theory link, a theory page absent from the sitemap, a - * loading skeleton of the wrong shape. TypeScript cannot catch these because - * every registry is independently valid on its own. - * - * Run with: node scripts/check-registries.mjs - */ - -import { readFileSync, readdirSync, existsSync } from "node:fs"; -import { join } from "node:path"; - -const problems = []; -const fail = (message) => problems.push(message); - -const read = (path) => readFileSync(path, "utf8"); -const exists = (path) => existsSync(path); - -/* ── Parse the topic registry ── */ - -const topicsSource = read("src/lib/topics.ts"); -const topics = [ - ...topicsSource.matchAll( - /id: "([^"]+)",\s*\n\s*title: "([^"]+)",\s*\n\s*category: "([^"]+)",\s*\n\s*route: "([^"]+)"/g, - ), -].map(([, id, title, category, route]) => ({ id, title, category, route })); - -if (topics.length === 0) { - fail("Could not parse any topics from src/lib/topics.ts"); - report(); -} - -const topicIds = new Set(topics.map((t) => t.id)); - -/* ── Read the registries that reference topics ── */ - -const theoryIndex = read("src/content/theory/index.ts"); -const metadata = read("src/lib/metadata.ts"); -const pageShell = read("src/components/layout/VisualizationPageShell.tsx"); - -const mapKeys = (source, mapName) => { - const block = source.match( - new RegExp(`${mapName}[^=]*=\\s*\\{([\\s\\S]*?)\\n\\};`), - ); - if (!block) { - fail(`Could not locate ${mapName}`); - return new Set(); - } - return new Set( - [...block[1].matchAll(/^\s{2}"?([a-z0-9-]+)"?:/gm)].map((m) => m[1]), - ); -}; - -const theoryRegistry = mapKeys(theoryIndex, "THEORY_CONTENT_BY_TOPIC_ID"); -const keywordRegistry = mapKeys(metadata, "TOPIC_KEYWORDS"); -const descriptionRegistry = mapKeys(metadata, "TOPIC_THEORY_DESCRIPTIONS"); - -const selectorBlock = pageShell.match( - /SELECTOR_TOOLBAR_TOPIC_IDS = new Set\(\[([\s\S]*?)\]\)/, -); -const selectorRegistry = new Set( - selectorBlock - ? [...selectorBlock[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]) - : [], -); -if (!selectorBlock) fail("Could not locate SELECTOR_TOOLBAR_TOPIC_IDS"); - -/* ── Per-topic checks ── */ - -const inbound = new Map(topics.map((t) => [t.id, 0])); - -for (const topic of topics) { - const { id, category, route } = topic; - - const routeFile = `src/app${route}/page.tsx`; - if (!exists(routeFile)) fail(`${id}: missing route at ${routeFile}`); - - const componentDir = `src/components/visualizations/${id}`; - if (!exists(componentDir)) - fail(`${id}: missing visualization at ${componentDir}/`); - - const theoryFile = `src/content/theory/${category}/${id}.ts`; - if (!exists(theoryFile)) { - fail(`${id}: missing theory content at ${theoryFile}`); - continue; - } - - if (!theoryRegistry.has(id)) - fail(`${id}: not registered in THEORY_CONTENT_BY_TOPIC_ID (absent from sitemap)`); - if (!keywordRegistry.has(id)) fail(`${id}: missing from TOPIC_KEYWORDS`); - if (!descriptionRegistry.has(id)) - fail(`${id}: missing from TOPIC_THEORY_DESCRIPTIONS`); - - // ExampleSelector topics need a matching toolbar skeleton variant. - const usesSelector = readdirSync(componentDir, { recursive: true }) - .filter((f) => typeof f === "string" && /\.tsx?$/.test(f)) - .some((f) => read(join(componentDir, f)).includes("ExampleSelector")); - - if (usesSelector && !selectorRegistry.has(id)) - fail(`${id}: uses ExampleSelector but is absent from SELECTOR_TOOLBAR_TOPIC_IDS`); - if (!usesSelector && selectorRegistry.has(id)) - fail(`${id}: listed in SELECTOR_TOOLBAR_TOPIC_IDS but has no ExampleSelector`); - - // Related topics: 3 to 5 valid IDs, no self-reference, no duplicates. - const theorySource = read(theoryFile); - const relatedBlock = theorySource.match(/relatedTopicIds:\s*\[([^\]]*)\]/); - if (!relatedBlock) { - fail(`${id}: theory content has no relatedTopicIds`); - continue; - } - - const related = [...relatedBlock[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]); - - if (related.length < 3 || related.length > 5) - fail(`${id}: relatedTopicIds has ${related.length} entries, expected 3 to 5`); - if (related.includes(id)) fail(`${id}: relatedTopicIds references itself`); - if (new Set(related).size !== related.length) - fail(`${id}: relatedTopicIds contains duplicates`); - - for (const ref of related) { - if (!topicIds.has(ref)) { - fail(`${id}: relatedTopicIds references unknown topic "${ref}"`); - continue; - } - inbound.set(ref, inbound.get(ref) + 1); - } -} - -/* ── Whole-graph checks ── */ - -for (const [id, count] of inbound) { - if (count === 0) - fail(`${id}: no theory page links to it, leaving it orphaned for readers and search`); -} - -for (const id of theoryRegistry) { - if (!topicIds.has(id)) - fail(`THEORY_CONTENT_BY_TOPIC_ID has "${id}", which is not a registered topic`); -} - -/* ── Content conventions ── */ - -const contentFiles = []; -const walk = (dir) => { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const path = join(dir, entry.name); - if (entry.isDirectory()) walk(path); - else if (/\.tsx?$/.test(entry.name)) contentFiles.push(path); - } -}; -walk("src/content"); -walk("src/components/visualizations"); - -for (const file of contentFiles) { - if (read(file).includes("—")) - fail(`${file}: contains an em dash, which is not allowed in user-facing content`); -} - -/* ── Report ── */ - -function report() { - if (problems.length > 0) { - console.error(`\nRegistry check failed with ${problems.length} problem(s):\n`); - for (const problem of problems) console.error(` - ${problem}`); - console.error(""); - process.exit(1); - } - - console.log( - `Registry check passed. ${topics.length} topics fully registered across all eight registries.`, - ); -} - -report(); diff --git a/scripts/lib/stepAudit.test.ts b/scripts/lib/stepAudit.test.ts new file mode 100644 index 0000000..7bd1533 --- /dev/null +++ b/scripts/lib/stepAudit.test.ts @@ -0,0 +1,240 @@ +import { describe, expect, it } from "vitest"; +import { + auditExample, + flattenExamples, + isBraceOnly, + isComment, + isTrivial, + type AuditExample, +} from "./stepAudit"; + +const lines = (...texts: string[]) => + texts.map((text, index) => ({ num: index + 1, text })); + +const example = (overrides: Partial = {}): AuditExample => ({ + id: "sample", + codeLines: lines( + "function add(a, b) {", + " const sum = a + b;", + " return sum;", + "}", + "add(1, 2);", + ), + steps: [ + { descriptionHtml: "Declare", activeLine: 1, doneLines: [] }, + { descriptionHtml: "Call", activeLine: 5, doneLines: [1] }, + { descriptionHtml: "Sum", activeLine: 2, doneLines: [1, 5] }, + { descriptionHtml: "Return", activeLine: 3, doneLines: [1, 2, 5] }, + ], + ...overrides, +}); + +describe("line classifiers", () => { + it("recognises brace-only and punctuation-only lines", () => { + expect(isBraceOnly("}")).toBe(true); + expect(isBraceOnly(" });")).toBe(true); + expect(isBraceOnly("")).toBe(true); + expect(isBraceOnly("return;")).toBe(false); + }); + + it("recognises comments in JS, SQL, and shell style", () => { + expect(isComment("// note")).toBe(true); + expect(isComment(" -- sql")).toBe(true); + expect(isComment("# shell")).toBe(true); + expect(isComment("const x = 1; // trailing")).toBe(false); + }); + + it("treats braces and comments as trivial", () => { + expect(isTrivial("}")).toBe(true); + expect(isTrivial("// c")).toBe(true); + expect(isTrivial("let a;")).toBe(false); + }); +}); + +describe("auditExample", () => { + it("passes a clean example without errors or warnings", () => { + expect(auditExample(example())).toEqual({ errors: [], warnings: [] }); + }); + + it("flags active lines outside the code", () => { + const { errors } = auditExample( + example({ + steps: [{ descriptionHtml: "x", activeLine: 9, doneLines: [] }], + }), + ); + expect(errors).toContain("sample #1: activeLine 9 is outside 1..5"); + }); + + it("flags done lines outside the code", () => { + const { errors } = auditExample( + example({ + steps: [{ descriptionHtml: "x", activeLine: 1, doneLines: [0, 6] }], + }), + ); + expect(errors).toEqual([ + "sample #1: doneLine 0 is outside 1..5", + "sample #1: doneLine 6 is outside 1..5", + ]); + }); + + it("flags an active line that is blank", () => { + const { errors } = auditExample( + example({ + codeLines: lines("a();", "", "b();"), + steps: [{ descriptionHtml: "x", activeLine: 2, doneLines: [] }], + }), + ); + expect(errors).toContain("sample #1: activeLine 2 is a blank line"); + }); + + it("flags an active line that also appears in doneLines", () => { + const { errors } = auditExample( + example({ + steps: [{ descriptionHtml: "x", activeLine: 2, doneLines: [1, 2] }], + }), + ); + expect(errors).toContain("sample #1: activeLine 2 is also in doneLines"); + }); + + it("warns, rather than errors, for brace-only and comment lines", () => { + const { errors, warnings } = auditExample( + example({ + codeLines: lines("// setup", "run();", "}"), + steps: [ + { descriptionHtml: "x", activeLine: 1, doneLines: [] }, + { descriptionHtml: "x", activeLine: 3, doneLines: [1, 2] }, + ], + }), + ); + expect(errors).toEqual([]); + expect(warnings).toContain( + 'sample #1: activeLine 1 is a comment: "// setup"', + ); + expect(warnings).toContain( + 'sample #2: activeLine 3 is only a brace: "}" (fine for a return step)', + ); + }); + + it("warns after three consecutive steps without an active line", () => { + const blank = { + descriptionHtml: "x", + activeLine: null, + doneLines: [1, 2, 3, 4, 5], + }; + const { warnings } = auditExample( + example({ steps: [blank, blank, blank, blank] }), + ); + expect(warnings.filter((w) => w.includes("three steps in a row"))).toEqual([ + "sample #3: three steps in a row with no active line", + ]); + }); + + it("warns when more than two done lines disappear between steps", () => { + const { warnings } = auditExample( + example({ + steps: [ + { descriptionHtml: "x", activeLine: 5, doneLines: [1, 2, 3, 4] }, + { descriptionHtml: "x", activeLine: 5, doneLines: [] }, + ], + }), + ); + expect(warnings).toContain("sample #2: doneLines dropped 1,2,3,4"); + }); + + it("tolerates up to two done lines disappearing", () => { + const { warnings } = auditExample( + example({ + steps: [ + { descriptionHtml: "x", activeLine: 5, doneLines: [1, 2] }, + { descriptionHtml: "x", activeLine: 5, doneLines: [] }, + ], + }), + ); + expect(warnings.some((w) => w.includes("dropped"))).toBe(false); + }); + + it("warns about non-trivial lines that never light up", () => { + const { warnings } = auditExample( + example({ + steps: [{ descriptionHtml: "x", activeLine: 1, doneLines: [] }], + }), + ); + expect(warnings).toContain("sample: lines never active or done: 2,3,5"); + }); + + it("only checks identifier mentions in strict mode", () => { + const strictExample = example({ + steps: [ + { + descriptionHtml: "Calls add now", + activeLine: 3, + doneLines: [1, 2], + }, + ], + }); + expect( + auditExample(strictExample).warnings.some((w) => w.includes("mentions")), + ).toBe(false); + const strict = auditExample(strictExample, { strict: true }); + expect(strict.warnings).toContain( + "sample #1: mentions add (line 1) but active line is 3", + ); + }); + + it("accepts strict mentions of identifiers on neighbouring lines", () => { + const { warnings } = auditExample( + example({ + steps: [ + { + descriptionHtml: "Adds sum", + activeLine: 3, + doneLines: [1, 2], + }, + ], + }), + { strict: true }, + ); + expect(warnings.some((w) => w.includes("mentions"))).toBe(false); + }); +}); + +describe("flattenExamples", () => { + const codeLines = lines("a();"); + const steps = [{ descriptionHtml: "x", activeLine: 1, doneLines: [] }]; + + it("wraps single-example topics as a default example", () => { + expect(flattenExamples({ STEPS: steps, CODE_LINES: codeLines })).toEqual([ + { id: "default", codeLines, steps }, + ]); + }); + + it("passes selector examples through", () => { + expect( + flattenExamples({ EXAMPLES: [{ id: "one", codeLines, steps }] }), + ).toEqual([{ id: "one", codeLines, steps }]); + }); + + it("flattens variants into method-suffixed examples", () => { + expect( + flattenExamples({ + EXAMPLES: [ + { + id: "deep", + variants: [ + { methodId: "clone", codeLines, steps }, + { methodId: "json", codeLines, steps }, + ], + }, + ], + }), + ).toEqual([ + { id: "deep/clone", codeLines, steps }, + { id: "deep/json", codeLines, steps }, + ]); + }); + + it("skips examples without code and returns nothing for other modules", () => { + expect(flattenExamples({ EXAMPLES: [{ id: "empty" }] })).toEqual([]); + expect(flattenExamples({})).toEqual([]); + }); +}); diff --git a/scripts/lib/stepAudit.ts b/scripts/lib/stepAudit.ts new file mode 100644 index 0000000..2c8bb65 --- /dev/null +++ b/scripts/lib/stepAudit.ts @@ -0,0 +1,171 @@ +/** + * Rules behind `npm run audit:steps`. Kept free of file system access so the + * checks can run against authored examples in tests as well as on disk. + */ + +export interface AuditLine { + num: number; + text: string; +} + +export interface AuditStep { + descriptionHtml: string; + activeLine: number | null; + doneLines: number[]; +} + +export interface AuditExample { + id: string; + codeLines: AuditLine[]; + steps: AuditStep[]; +} + +export interface AuditVariant { + methodId: string; + codeLines: AuditLine[]; + steps: AuditStep[]; +} + +export interface AuditFindings { + errors: string[]; + warnings: string[]; +} + +export interface AuditOptions { + /** Also flag identifiers named in the description that live on another line. */ + strict?: boolean; +} + +/** The exports a topic's data module may expose. */ +export interface TopicDataModule { + STEPS?: AuditStep[]; + CODE_LINES?: AuditLine[]; + EXAMPLES?: (Partial & { + id: string; + variants?: AuditVariant[]; + })[]; +} + +export const isBlank = (text: string) => text.trim() === ""; +export const isBraceOnly = (text: string) => /^\s*[{}()[\];,]*\s*$/.test(text); +export const isComment = (text: string) => /^\s*(\/\/|--|#)/.test(text); +export const isTrivial = (text: string) => isBraceOnly(text) || isComment(text); + +const identifiers = (text: string) => + new Set( + (text.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? []).filter((w) => w.length > 2), + ); + +/** + * Examples as authored, with per-method variants flattened into examples. + * Single-example topics export STEPS and CODE_LINES; selector topics export + * EXAMPLES, where an example may hold variants instead of its own steps. + */ +export const flattenExamples = (mod: TopicDataModule): AuditExample[] => { + if (mod.STEPS && mod.CODE_LINES) { + return [{ id: "default", codeLines: mod.CODE_LINES, steps: mod.STEPS }]; + } + const examples = mod.EXAMPLES ?? []; + return examples.flatMap((example) => { + if (example.variants) { + return example.variants.map((variant) => ({ + id: `${example.id}/${variant.methodId}`, + codeLines: variant.codeLines, + steps: variant.steps, + })); + } + return example.codeLines && example.steps + ? [{ id: example.id, codeLines: example.codeLines, steps: example.steps }] + : []; + }); +}; + +export const auditExample = ( + example: AuditExample, + { strict = false }: AuditOptions = {}, +): AuditFindings => { + const errors: string[] = []; + const warnings: string[] = []; + const max = Math.max(...example.codeLines.map((line) => line.num)); + const textByNum = new Map(example.codeLines.map((l) => [l.num, l.text])); + let previousDone = new Set(); + let nullRun = 0; + + example.steps.forEach((step, index) => { + const tag = `${example.id} #${index + 1}`; + const active = step.activeLine; + + if (active == null) { + nullRun += 1; + if (nullRun === 3) { + warnings.push(`${tag}: three steps in a row with no active line`); + } + } else { + nullRun = 0; + const text = textByNum.get(active); + if (active < 1 || active > max || text === undefined) { + errors.push(`${tag}: activeLine ${active} is outside 1..${max}`); + } else if (isBlank(text)) { + errors.push(`${tag}: activeLine ${active} is a blank line`); + } else if (isBraceOnly(text)) { + warnings.push( + `${tag}: activeLine ${active} is only a brace: "${text.trim()}" (fine for a return step)`, + ); + } else if (isComment(text)) { + warnings.push( + `${tag}: activeLine ${active} is a comment: "${text.trim()}"`, + ); + } else if (strict) { + const nearby = new Set(); + for (let n = active - 1; n <= active + 1; n += 1) { + for (const word of identifiers(textByNum.get(n) ?? "")) + nearby.add(word); + } + for (const [, bit] of step.descriptionHtml.matchAll( + /([^<]+)<\/code>/g, + )) { + const words = [...identifiers(bit)]; + if (!words.length || words.some((w) => nearby.has(w))) continue; + const elsewhere = example.codeLines.find( + (line) => + line.num !== active && identifiers(line.text).has(words[0]), + ); + if (elsewhere) { + warnings.push( + `${tag}: mentions ${bit} (line ${elsewhere.num}) but active line is ${active}`, + ); + } + } + } + } + + const done = new Set(step.doneLines); + for (const d of done) { + if (d < 1 || d > max) + errors.push(`${tag}: doneLine ${d} is outside 1..${max}`); + } + if (active != null && done.has(active)) { + errors.push(`${tag}: activeLine ${active} is also in doneLines`); + } + const dropped = [...previousDone].filter((d) => !done.has(d)); + if (dropped.length > 2) { + warnings.push(`${tag}: doneLines dropped ${dropped.join(",")}`); + } + previousDone = done; + }); + + const litUp = new Set(); + for (const step of example.steps) { + if (step.activeLine != null) litUp.add(step.activeLine); + for (const d of step.doneLines) litUp.add(d); + } + const never = example.codeLines.filter( + (l) => !isTrivial(l.text) && !litUp.has(l.num), + ); + if (never.length) { + warnings.push( + `${example.id}: lines never active or done: ${never.map((l) => l.num).join(",")}`, + ); + } + return { errors, warnings }; +}; diff --git a/src/app/[category]/[topic]/opengraph-image.tsx b/src/app/[category]/[topic]/opengraph-image.tsx new file mode 100644 index 0000000..053216b --- /dev/null +++ b/src/app/[category]/[topic]/opengraph-image.tsx @@ -0,0 +1,30 @@ +import { CATEGORIES } from "@/lib/categories"; +import { SOCIAL_IMAGE_ALT } from "@/lib/constants"; +import { getTopicDescription } from "@/lib/metadata"; +import { renderSocialImage, SOCIAL_IMAGE_SIZE } from "@/lib/socialImage"; +import { getTopicById, topics } from "@/lib/topics"; + +export const alt = SOCIAL_IMAGE_ALT; +export const size = SOCIAL_IMAGE_SIZE; +export const contentType = "image/png"; + +export function generateStaticParams() { + return topics.map((topic) => ({ category: topic.category, topic: topic.id })); +} + +export default async function TopicImage({ + params, +}: { + params: Promise<{ category: string; topic: string }>; +}) { + const { topic: topicId } = await params; + const topic = getTopicById(topicId) ?? topics[0]; + const category = CATEGORIES[topic.category]; + + return renderSocialImage({ + kicker: `${category.label} · ${topic.difficulty}`, + title: topic.title, + subtitle: getTopicDescription(topic), + accentRgb: category.hero.accent, + }); +} diff --git a/src/app/[category]/[topic]/page.tsx b/src/app/[category]/[topic]/page.tsx new file mode 100644 index 0000000..bfa6ff2 --- /dev/null +++ b/src/app/[category]/[topic]/page.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; +import { TopicVisualization } from "@/components/visualizations/registry"; +import { createTopicMetadata } from "@/lib/metadata"; +import { getTopicById, isTopicId, topics } from "@/lib/topics"; +import type { Topic } from "@/types"; + +interface TopicPageProps { + params: Promise<{ category: string; topic: string }>; +} + +export const dynamicParams = false; + +export function generateStaticParams() { + return topics.map((topic) => ({ category: topic.category, topic: topic.id })); +} + +/** The topic for this URL, or a 404 when the id or category does not match. */ +const resolveTopic = async ( + params: TopicPageProps["params"], +): Promise => { + const { category, topic: topicId } = await params; + const topic = getTopicById(topicId); + if (!topic || topic.category !== category) notFound(); + return topic; +}; + +export async function generateMetadata({ + params, +}: TopicPageProps): Promise { + return createTopicMetadata(await resolveTopic(params)); +} + +export default async function TopicPage({ params }: TopicPageProps) { + const topic = await resolveTopic(params); + if (!isTopicId(topic.id)) notFound(); + + return ( + + + + ); +} diff --git a/src/app/react/layout.tsx b/src/app/[category]/layout.tsx similarity index 68% rename from src/app/react/layout.tsx rename to src/app/[category]/layout.tsx index d51efb7..2669d0b 100644 --- a/src/app/react/layout.tsx +++ b/src/app/[category]/layout.tsx @@ -1,11 +1,11 @@ import type { ReactNode } from "react"; import { OnboardingTourModal } from "@/components/onboarding/OnboardingTourModal"; -interface ReactLayoutProps { +interface CategoryLayoutProps { children: ReactNode; } -export default function ReactLayout({ children }: ReactLayoutProps) { +export default function CategoryLayout({ children }: CategoryLayoutProps) { return ( <> {children} diff --git a/src/app/[category]/opengraph-image.tsx b/src/app/[category]/opengraph-image.tsx new file mode 100644 index 0000000..3a19c4c --- /dev/null +++ b/src/app/[category]/opengraph-image.tsx @@ -0,0 +1,31 @@ +import { CATEGORIES, CATEGORY_ORDER, isCategory } from "@/lib/categories"; +import { SOCIAL_IMAGE_ALT } from "@/lib/constants"; +import { renderSocialImage, SOCIAL_IMAGE_SIZE } from "@/lib/socialImage"; +import { getTopicsByCategory } from "@/lib/topics"; + +export const alt = SOCIAL_IMAGE_ALT; +export const size = SOCIAL_IMAGE_SIZE; +export const contentType = "image/png"; + +export function generateStaticParams() { + return CATEGORY_ORDER.map((category) => ({ category })); +} + +export default async function CategoryImage({ + params, +}: { + params: Promise<{ category: string }>; +}) { + const { category } = await params; + const config = isCategory(category) + ? CATEGORIES[category] + : CATEGORIES.javascript; + const count = getTopicsByCategory(config.id).length; + + return renderSocialImage({ + kicker: `${count} interactive topics`, + title: config.indexTitle, + subtitle: config.description, + accentRgb: config.hero.accent, + }); +} diff --git a/src/app/[category]/page.tsx b/src/app/[category]/page.tsx new file mode 100644 index 0000000..8af0402 --- /dev/null +++ b/src/app/[category]/page.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { CategoryTopicsPage } from "@/components/layout/CategoryTopicsPage"; +import { CATEGORIES, CATEGORY_ORDER, isCategory } from "@/lib/categories"; +import { createCategoryMetadata } from "@/lib/metadata"; +import { getTopicsByCategory } from "@/lib/topics"; + +interface CategoryPageProps { + params: Promise<{ category: string }>; +} + +export const dynamicParams = false; + +export function generateStaticParams() { + return CATEGORY_ORDER.map((category) => ({ category })); +} + +export async function generateMetadata({ + params, +}: CategoryPageProps): Promise { + const { category } = await params; + if (!isCategory(category)) notFound(); + return createCategoryMetadata( + CATEGORIES[category], + getTopicsByCategory(category).length, + ); +} + +export default async function CategoryPage({ params }: CategoryPageProps) { + const { category } = await params; + if (!isCategory(category)) notFound(); + return ; +} diff --git a/src/app/globals.css b/src/app/globals.css index 3333c27..0cc4b17 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -8,7 +8,9 @@ --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: "Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - --font-mono: "JetBrains Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --font-mono: + "JetBrains Mono", "SFMono-Regular", Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -164,7 +166,11 @@ pointer-events: none; background-image: radial-gradient(circle at top, rgba(14, 165, 233, 0.22), transparent 42%), - radial-gradient(circle at 18% 88%, rgba(236, 72, 153, 0.14), transparent 44%); + radial-gradient( + circle at 18% 88%, + rgba(236, 72, 153, 0.14), + transparent 44% + ); z-index: 0; } @@ -174,7 +180,10 @@ inset: 0; pointer-events: none; opacity: 0.035; - background-image: radial-gradient(rgba(255, 255, 255, 0.9) 0.5px, transparent 0.6px); + background-image: radial-gradient( + rgba(255, 255, 255, 0.9) 0.5px, + transparent 0.6px + ); background-size: 3px 3px; z-index: 0; } @@ -186,7 +195,11 @@ .app-surface { border: 1px solid var(--app-border); - background: linear-gradient(180deg, rgba(15, 26, 48, 0.96), rgba(10, 18, 35, 0.96)); + background: linear-gradient( + 180deg, + rgba(15, 26, 48, 0.96), + rgba(10, 18, 35, 0.96) + ); box-shadow: 0 14px 32px rgba(2, 6, 23, 0.45), inset 0 1px 0 rgba(148, 163, 184, 0.06); @@ -219,39 +232,72 @@ overflow: hidden; border: 1px solid rgba(var(--hero-accent), 0.24); background: - radial-gradient(circle at 50% 0%, rgba(var(--hero-accent-soft), 0.14), transparent 44%), + radial-gradient( + circle at 50% 0%, + rgba(var(--hero-accent-soft), 0.14), + transparent 44% + ), linear-gradient(180deg, rgba(12, 23, 44, 0.96), rgba(7, 14, 28, 0.97)); box-shadow: 0 20px 36px rgba(2, 6, 23, 0.45), inset 0 1px 0 rgba(148, 163, 184, 0.12), 0 0 0 1px rgba(var(--hero-secondary), 0.09); - transform: perspective(1150px) rotateX(var(--category-hero-tilt-x)) rotateY(var(--category-hero-tilt-y)); - transition: transform 180ms ease-out, box-shadow 180ms ease-out; + transform: perspective(1150px) rotateX(var(--category-hero-tilt-x)) + rotateY(var(--category-hero-tilt-y)); + transition: + transform 180ms ease-out, + box-shadow 180ms ease-out; will-change: transform; } .category-hero-tiles { opacity: 0.45; background-image: - linear-gradient(to right, rgba(var(--hero-secondary), 0.14) 1px, transparent 1px), - linear-gradient(to bottom, rgba(var(--hero-secondary), 0.14) 1px, transparent 1px); + linear-gradient( + to right, + rgba(var(--hero-secondary), 0.14) 1px, + transparent 1px + ), + linear-gradient( + to bottom, + rgba(var(--hero-secondary), 0.14) 1px, + transparent 1px + ); background-size: 36px 36px; - mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.78), transparent 84%); + mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.78), + transparent 84% + ); } .category-hero-tiles-hotspot { opacity: 0.95; background-image: - linear-gradient(to right, rgba(var(--hero-accent), 0.42) 1px, transparent 1px), - linear-gradient(to bottom, rgba(var(--hero-accent), 0.42) 1px, transparent 1px), + linear-gradient( + to right, + rgba(var(--hero-accent), 0.42) 1px, + transparent 1px + ), + linear-gradient( + to bottom, + rgba(var(--hero-accent), 0.42) 1px, + transparent 1px + ), radial-gradient( 260px circle at var(--category-hero-glow-x) var(--category-hero-glow-y), rgba(var(--hero-accent-soft), 0.38), rgba(var(--hero-secondary), 0.16) 34%, transparent 76% ); - background-size: 36px 36px, 36px 36px, auto; - background-position: 0 0, 0 0, center; + background-size: + 36px 36px, + 36px 36px, + auto; + background-position: + 0 0, + 0 0, + center; mask-image: radial-gradient( 220px circle at var(--category-hero-glow-x) var(--category-hero-glow-y), rgba(0, 0, 0, 1), @@ -305,11 +351,19 @@ } .category-hero-orb-primary { - background: radial-gradient(circle, rgba(var(--hero-accent), 0.42), transparent 70%); + background: radial-gradient( + circle, + rgba(var(--hero-accent), 0.42), + transparent 70% + ); } .category-hero-orb-secondary { - background: radial-gradient(circle, rgba(var(--hero-secondary), 0.34), transparent 72%); + background: radial-gradient( + circle, + rgba(var(--hero-secondary), 0.34), + transparent 72% + ); } .category-hero-orb-delayed { @@ -359,14 +413,126 @@ } } + /* ── Topic cards ── */ + .topic-card { + --card-glow: 244 114 182; + border-color: var(--app-border); + } + + .group:hover .topic-card, + .group:focus-visible .topic-card { + border-color: rgba(var(--card-glow), 0.45); + box-shadow: + 0 14px 32px rgba(2, 6, 23, 0.45), + 0 0 24px rgba(var(--card-glow), 0.16); + transform: translateY(-2px); + } + + /* ── Home hero ── */ + .home-hero { + border: 1px solid rgba(71, 85, 105, 0.5); + background: + radial-gradient( + circle at 20% 0%, + rgba(251, 191, 36, 0.14), + transparent 40% + ), + radial-gradient( + circle at 80% 10%, + rgba(34, 211, 238, 0.14), + transparent 42% + ), + radial-gradient( + circle at 50% 100%, + rgba(244, 114, 182, 0.14), + transparent 46% + ), + linear-gradient(180deg, rgba(12, 23, 44, 0.96), rgba(7, 14, 28, 0.97)); + box-shadow: + 0 20px 36px rgba(2, 6, 23, 0.45), + inset 0 1px 0 rgba(148, 163, 184, 0.12); + } + + .home-hero-grid { + opacity: 0.35; + background-image: + linear-gradient(to right, rgba(148, 163, 184, 0.12) 1px, transparent 1px), + linear-gradient(to bottom, rgba(148, 163, 184, 0.12) 1px, transparent 1px); + background-size: 36px 36px; + mask-image: radial-gradient( + ellipse at center, + rgba(0, 0, 0, 0.9), + transparent 75% + ); + } + + /* ── Step scrubber ── */ + .viz-scrubber input[type="range"] { + --scrubber-fill: 0%; + appearance: none; + width: 7.5rem; + height: 6px; + cursor: pointer; + border-radius: 9999px; + background: linear-gradient( + 90deg, + rgba(244, 114, 182, 0.85) var(--scrubber-fill), + rgba(51, 65, 85, 0.85) var(--scrubber-fill) + ); + outline: none; + } + + .viz-scrubber input[type="range"]::-webkit-slider-thumb { + appearance: none; + height: 14px; + width: 14px; + border-radius: 9999px; + border: 2px solid #f9a8d4; + background: #0f1a30; + box-shadow: 0 0 10px rgba(244, 114, 182, 0.5); + transition: transform 0.15s ease; + } + + .viz-scrubber input[type="range"]::-moz-range-thumb { + height: 14px; + width: 14px; + border-radius: 9999px; + border: 2px solid #f9a8d4; + background: #0f1a30; + box-shadow: 0 0 10px rgba(244, 114, 182, 0.5); + } + + .viz-scrubber input[type="range"]:hover::-webkit-slider-thumb, + .viz-scrubber input[type="range"]:focus-visible::-webkit-slider-thumb { + transform: scale(1.15); + } + + .viz-scrubber input[type="range"]:focus-visible { + box-shadow: 0 0 0 2px rgba(244, 114, 182, 0.5); + } + /* ── Syntax highlighting token classes ── */ - .tok-kw { color: #c084fc; } - .tok-builtin { color: #e5c07b; } - .tok-fn { color: #60a5fa; } - .tok-str { color: #34d399; } - .tok-num { color: #fbbf24; } - .tok-comment { color: #64748b; } - .tok-punct { color: #94a3b8; } + .tok-kw { + color: #c084fc; + } + .tok-builtin { + color: #e5c07b; + } + .tok-fn { + color: #60a5fa; + } + .tok-str { + color: #34d399; + } + .tok-num { + color: #fbbf24; + } + .tok-comment { + color: #64748b; + } + .tok-punct { + color: #94a3b8; + } /* ── Shared code-line base styles ── */ .code-line { @@ -413,17 +579,17 @@ } .code-line.is-highlighted { - background: rgba(251, 191, 36, 0.10); + background: rgba(251, 191, 36, 0.1); box-shadow: inset 3px 0 0 rgba(251, 191, 36, 0.5); } .code-line.is-floating { - background: rgba(16, 185, 129, 0.10); + background: rgba(16, 185, 129, 0.1); box-shadow: inset 3px 0 0 #34d399; } .code-line.is-tdz { - background: rgba(239, 68, 68, 0.10); + background: rgba(239, 68, 68, 0.1); box-shadow: inset 3px 0 0 #f87171; } @@ -475,67 +641,260 @@ } } + /* Flow connector - dotted track with glowing dots travelling along it. + Colour comes from currentColor, so tone is set with a text-* class. */ + .viz-flow { + position: relative; + display: block; + flex-shrink: 0; + overflow: visible; + color: #64748b; + } + + .viz-flow--h { + height: 2px; + background-image: radial-gradient( + circle, + currentColor 1px, + transparent 1.5px + ); + background-size: 6px 2px; + background-repeat: repeat-x; + background-position: center; + opacity: 0.55; + } + + .viz-flow--v { + width: 2px; + background-image: radial-gradient( + circle, + currentColor 1px, + transparent 1.5px + ); + background-size: 2px 6px; + background-repeat: repeat-y; + background-position: center; + opacity: 0.55; + } + + .viz-flow--active { + opacity: 1; + } + + .viz-flow-dot { + position: absolute; + height: 6px; + width: 6px; + border-radius: 9999px; + background: currentColor; + box-shadow: 0 0 8px currentColor; + opacity: 0; + } + + .viz-flow--h .viz-flow-dot { + top: -2px; + left: 0; + margin-left: -3px; + } + + .viz-flow--v .viz-flow-dot { + left: -2px; + top: 0; + margin-top: -3px; + } + + .viz-flow--active.viz-flow--h .viz-flow-dot { + animation: viz-flow-h 1.2s linear infinite; + } + + .viz-flow--active.viz-flow--v .viz-flow-dot { + animation: viz-flow-v 1.2s linear infinite; + } + + .viz-flow--reverse.viz-flow--h .viz-flow-dot { + animation-direction: reverse; + } + + .viz-flow--reverse.viz-flow--v .viz-flow-dot { + animation-direction: reverse; + } + + .viz-flow-dot:nth-child(2) { + animation-delay: -0.6s; + } + + @media (prefers-reduced-motion: reduce) { + .viz-flow--active .viz-flow-dot { + animation: none !important; + opacity: 1; + } + + .viz-flow--active.viz-flow--h .viz-flow-dot { + left: 50%; + } + + .viz-flow--active.viz-flow--v .viz-flow-dot { + top: 50%; + } + } + /* Step description - inline code and colored highlights */ .viz-step-desc code { border: 1px solid rgba(71, 85, 105, 0.7); background: rgba(15, 23, 42, 0.5); border-radius: 6px; padding: 2px 6px; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; font-size: 0.78rem; } - .viz-step-desc .hl-stack { color: #fbbf24; font-weight: 600; } - .viz-step-desc .hl-api { color: #22d3ee; font-weight: 600; } - .viz-step-desc .hl-task { color: #34d399; font-weight: 600; } - .viz-step-desc .hl-micro { color: #c4b5fd; font-weight: 600; } - .viz-step-desc .hl-loop { color: #f472b6; font-weight: 600; } + .viz-step-desc .hl-stack { + color: #fbbf24; + font-weight: 600; + } + .viz-step-desc .hl-api { + color: #22d3ee; + font-weight: 600; + } + .viz-step-desc .hl-task { + color: #34d399; + font-weight: 600; + } + .viz-step-desc .hl-micro { + color: #c4b5fd; + font-weight: 600; + } + .viz-step-desc .hl-loop { + color: #f472b6; + font-weight: 600; + } @keyframes console-fade-in { - from { opacity: 0; transform: translateY(2px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(2px); + } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes viz-slide-in { - from { transform: translateX(-16px); opacity: 0; } - to { transform: translateX(0); opacity: 1; } + from { + transform: translateX(-16px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } } @keyframes viz-float-up { - from { opacity: 0.25; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0.25; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes viz-flow-h { + 0% { + left: 0; + opacity: 0; + } + 12% { + opacity: 1; + } + 88% { + opacity: 1; + } + 100% { + left: 100%; + opacity: 0; + } + } + + @keyframes viz-flow-v { + 0% { + top: 0; + opacity: 0; + } + 12% { + opacity: 1; + } + 88% { + opacity: 1; + } + 100% { + top: 100%; + opacity: 0; + } } @keyframes viz-pulse { - 0%, 100% { opacity: 0.55; } - 50% { opacity: 1; } + 0%, + 100% { + opacity: 0.55; + } + 50% { + opacity: 1; + } } @keyframes viz-change-flash { - 0% { box-shadow: 0 12px 30px rgba(2,6,23,0.45), 0 0 0 0 rgba(244,114,182,0); } - 40% { box-shadow: 0 12px 30px rgba(2,6,23,0.45), 0 0 18px 2px rgba(244,114,182,0.25); } - 100% { box-shadow: 0 12px 30px rgba(2,6,23,0.45), 0 0 0 0 rgba(244,114,182,0); } + 0% { + box-shadow: + 0 12px 30px rgba(2, 6, 23, 0.45), + 0 0 0 0 rgba(244, 114, 182, 0); + } + 40% { + box-shadow: + 0 12px 30px rgba(2, 6, 23, 0.45), + 0 0 18px 2px rgba(244, 114, 182, 0.25); + } + 100% { + box-shadow: + 0 12px 30px rgba(2, 6, 23, 0.45), + 0 0 0 0 rgba(244, 114, 182, 0); + } } @keyframes viz-change-flash-pill { - 0% { box-shadow: inset 0 0 0 0 rgba(244,114,182,0); } - 40% { box-shadow: inset 0 0 20px 2px rgba(244,114,182,0.12); } - 100% { box-shadow: inset 0 0 0 0 rgba(244,114,182,0); } + 0% { + box-shadow: inset 0 0 0 0 rgba(244, 114, 182, 0); + } + 40% { + box-shadow: inset 0 0 20px 2px rgba(244, 114, 182, 0.12); + } + 100% { + box-shadow: inset 0 0 0 0 rgba(244, 114, 182, 0); + } } @keyframes category-hero-beam { - 0%, 100% { - transform: translateX(calc(-10% + var(--category-hero-beam-shift))) rotate(-8deg); + 0%, + 100% { + transform: translateX(calc(-10% + var(--category-hero-beam-shift))) + rotate(-8deg); opacity: 0.32; } 50% { - transform: translateX(calc(32% + var(--category-hero-beam-shift))) rotate(-8deg); + transform: translateX(calc(32% + var(--category-hero-beam-shift))) + rotate(-8deg); opacity: 0.62; } } @keyframes category-hero-orb { - 0%, 100% { + 0%, + 100% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.44; } @@ -546,7 +905,8 @@ } @keyframes category-hero-icon { - 0%, 100% { + 0%, + 100% { transform: translateY(0); } 50% { @@ -566,7 +926,8 @@ } @keyframes category-hero-prism { - 0%, 100% { + 0%, + 100% { transform: translateY(0) scaleX(0.95); opacity: 0.26; } @@ -575,5 +936,4 @@ opacity: 0.58; } } - } diff --git a/src/app/javascript/closures/page.tsx b/src/app/javascript/closures/page.tsx deleted file mode 100644 index 8248edd..0000000 --- a/src/app/javascript/closures/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const closuresTopic = getTopicOrThrow("closures"); - -const ClosuresVisualization = dynamic( - () => - import("@/components/visualizations/closures").then( - (module) => module.Closures - ), - { - loading: () => , - } -); - -export const metadata: Metadata = createTopicMetadata(closuresTopic); - -export default function ClosuresPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/destructuring/page.tsx b/src/app/javascript/destructuring/page.tsx deleted file mode 100644 index 3859745..0000000 --- a/src/app/javascript/destructuring/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { ErrorBoundary } from "@/components/ErrorBoundary"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("destructuring"); - -const DestructuringVisualization = dynamic( - () => - import("@/components/visualizations/destructuring").then( - (module) => module.Destructuring, - ), - { loading: () => }, -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function DestructuringPage() { - return ( - - - - - - ); -} diff --git a/src/app/javascript/event-delegation/page.tsx b/src/app/javascript/event-delegation/page.tsx deleted file mode 100644 index cf36a73..0000000 --- a/src/app/javascript/event-delegation/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { ErrorBoundary } from "@/components/ErrorBoundary"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("event-delegation"); - -const EventDelegationVisualization = dynamic( - () => - import("@/components/visualizations/event-delegation").then( - (module) => module.EventDelegation, - ), - { loading: () => }, -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function EventDelegationPage() { - return ( - - - - - - ); -} diff --git a/src/app/javascript/event-loop/page.tsx b/src/app/javascript/event-loop/page.tsx deleted file mode 100644 index 46c4e99..0000000 --- a/src/app/javascript/event-loop/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const eventLoopTopic = getTopicOrThrow("event-loop"); - -const EventLoopVisualization = dynamic( - () => - import("@/components/visualizations/event-loop").then( - (module) => module.EventLoop - ), - { - loading: () => , - } -); - -export const metadata: Metadata = createTopicMetadata(eventLoopTopic); - -export default function EventLoopPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/execution-context/page.tsx b/src/app/javascript/execution-context/page.tsx deleted file mode 100644 index 724cdbc..0000000 --- a/src/app/javascript/execution-context/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("execution-context"); - -const ExecutionContextVisualization = dynamic( - () => - import("@/components/visualizations/execution-context").then( - (module) => module.ExecutionContext - ), - { - loading: () => , - } -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function ExecutionContextPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/garbage-collection/page.tsx b/src/app/javascript/garbage-collection/page.tsx deleted file mode 100644 index 3f66785..0000000 --- a/src/app/javascript/garbage-collection/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("garbage-collection"); - -const GarbageCollectionVisualization = dynamic( - () => - import("@/components/visualizations/garbage-collection").then( - (module) => module.GarbageCollection - ), - { loading: () => } -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function GarbageCollectionPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/generators/page.tsx b/src/app/javascript/generators/page.tsx deleted file mode 100644 index 38eb7b2..0000000 --- a/src/app/javascript/generators/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const generatorsTopic = getTopicOrThrow("generators"); - -const GeneratorsVisualization = dynamic( - () => - import("@/components/visualizations/generators").then( - (module) => module.Generators - ), - { - loading: () => , - } -); - -export const metadata: Metadata = createTopicMetadata(generatorsTopic); - -export default function GeneratorsPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/heap-stack/page.tsx b/src/app/javascript/heap-stack/page.tsx deleted file mode 100644 index 2365f23..0000000 --- a/src/app/javascript/heap-stack/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("heap-stack"); - -const HeapStackVisualization = dynamic( - () => - import("@/components/visualizations/heap-stack").then( - (module) => module.HeapStack - ), - { loading: () => } -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function HeapStackPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/hoisting/page.tsx b/src/app/javascript/hoisting/page.tsx deleted file mode 100644 index 5529cc5..0000000 --- a/src/app/javascript/hoisting/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const hoistingTopic = getTopicOrThrow("hoisting"); - -const HoistingVisualization = dynamic( - () => - import("@/components/visualizations/hoisting").then( - (module) => module.Hoisting - ), - { - loading: () => , - } -); - -export const metadata: Metadata = createTopicMetadata(hoistingTopic); - -export default function HoistingPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/layout.tsx b/src/app/javascript/layout.tsx deleted file mode 100644 index 0aff143..0000000 --- a/src/app/javascript/layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { ReactNode } from "react"; -import { OnboardingTourModal } from "@/components/onboarding/OnboardingTourModal"; - -interface JavaScriptLayoutProps { - children: ReactNode; -} - -export default function JavaScriptLayout({ children }: JavaScriptLayoutProps) { - return ( - <> - {children} - - - ); -} diff --git a/src/app/javascript/modules-imports/page.tsx b/src/app/javascript/modules-imports/page.tsx deleted file mode 100644 index bed5620..0000000 --- a/src/app/javascript/modules-imports/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { ErrorBoundary } from "@/components/ErrorBoundary"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("modules-imports"); - -const ModulesImportsVisualization = dynamic( - () => - import("@/components/visualizations/modules-imports").then( - (module) => module.ModulesImports, - ), - { loading: () => }, -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function ModulesImportsPage() { - return ( - - - - - - ); -} diff --git a/src/app/javascript/page.tsx b/src/app/javascript/page.tsx deleted file mode 100644 index 3f0c1a1..0000000 --- a/src/app/javascript/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { Metadata } from "next"; -import { CategoryTopicsPage } from "@/components/layout/CategoryTopicsPage"; -import { getTopicsByCategory } from "@/lib/topics"; - -const topicCount = getTopicsByCategory("javascript").length; - -export const metadata: Metadata = { - title: "JavaScript Concepts, Visualized", - description: `Explore ${topicCount} interactive JavaScript visualizations: event loop, closures, hoisting, promises, scope chain, and prototypes. Learn JS internals step by step.`, - keywords: [ - "javascript visualizer", - "javascript visualization", - "javascript concepts", - "javascript fundamentals", - "event loop", - "hoisting", - "closures", - "promises", - "prototype chain", - "this keyword", - "scope chain", - "var vs let vs const", - "garbage collection javascript", - "type coercion", - "execution context", - "heap and stack", - "reference vs value", - "generators iterators", - ], - alternates: { - canonical: "/javascript", - }, -}; - -export default function JavaScriptIndexPage() { - return ; -} diff --git a/src/app/javascript/promises/page.tsx b/src/app/javascript/promises/page.tsx deleted file mode 100644 index f3ac8b1..0000000 --- a/src/app/javascript/promises/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const promisesTopic = getTopicOrThrow("promises"); - -const PromisesVisualization = dynamic( - () => - import("@/components/visualizations/promises").then( - (module) => module.Promises - ), - { - loading: () => , - } -); - -export const metadata: Metadata = createTopicMetadata(promisesTopic); - -export default function PromisesPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/prototypal-inheritance/page.tsx b/src/app/javascript/prototypal-inheritance/page.tsx deleted file mode 100644 index 7ca360d..0000000 --- a/src/app/javascript/prototypal-inheritance/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("prototypal-inheritance"); - -const PrototypalInheritanceVisualization = dynamic( - () => - import("@/components/visualizations/prototypal-inheritance").then( - (module) => module.PrototypalInheritance - ), - { - loading: () => , - } -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function PrototypalInheritancePage() { - return ( - - - - ); -} diff --git a/src/app/javascript/reference-value/page.tsx b/src/app/javascript/reference-value/page.tsx deleted file mode 100644 index 23954a2..0000000 --- a/src/app/javascript/reference-value/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("reference-value"); - -const ReferenceValueVisualization = dynamic( - () => - import("@/components/visualizations/reference-value").then( - (module) => module.ReferenceValue - ), - { loading: () => } -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function ReferenceValuePage() { - return ( - - - - ); -} diff --git a/src/app/javascript/scope-chain/page.tsx b/src/app/javascript/scope-chain/page.tsx deleted file mode 100644 index 40e7826..0000000 --- a/src/app/javascript/scope-chain/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("scope-chain"); - -const ScopeChainVisualization = dynamic( - () => - import("@/components/visualizations/scope-chain").then( - (module) => module.ScopeChain - ), - { - loading: () => , - } -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function ScopeChainPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/spread-rest/page.tsx b/src/app/javascript/spread-rest/page.tsx deleted file mode 100644 index a9659ff..0000000 --- a/src/app/javascript/spread-rest/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { ErrorBoundary } from "@/components/ErrorBoundary"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("spread-rest"); - -const SpreadRestVisualization = dynamic( - () => - import("@/components/visualizations/spread-rest").then( - (module) => module.SpreadRest, - ), - { loading: () => }, -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function SpreadRestPage() { - return ( - - - - - - ); -} diff --git a/src/app/javascript/this-keyword/page.tsx b/src/app/javascript/this-keyword/page.tsx deleted file mode 100644 index 2057b58..0000000 --- a/src/app/javascript/this-keyword/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("this-keyword"); - -const ThisKeywordVisualization = dynamic( - () => - import("@/components/visualizations/this-keyword").then( - (module) => module.ThisKeyword - ), - { - loading: () => , - } -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function ThisKeywordPage() { - return ( - - - - ); -} diff --git a/src/app/javascript/type-coercion/page.tsx b/src/app/javascript/type-coercion/page.tsx deleted file mode 100644 index 0d7b7c6..0000000 --- a/src/app/javascript/type-coercion/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Metadata } from "next"; -import dynamic from "next/dynamic"; -import { VisualizationPageShell } from "@/components/layout/VisualizationPageShell"; -import { VisualizationLoading } from "@/components/visualizations/VisualizationLoading"; -import { createTopicMetadata } from "@/lib/metadata"; -import { getTopicOrThrow } from "@/lib/topics"; - -const topic = getTopicOrThrow("type-coercion"); - -const TypeCoercionVisualization = dynamic( - () => - import("@/components/visualizations/type-coercion").then( - (module) => module.TypeCoercion - ), - { loading: () => } -); - -export const metadata: Metadata = createTopicMetadata(topic); - -export default function TypeCoercionPage() { - return ( - - - - ); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4390771..db0ead4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,18 +3,13 @@ import { Analytics } from "@vercel/analytics/next"; import { SpeedInsights } from "@vercel/speed-insights/next"; import { AppTheme } from "@/components/layout/AppTheme"; import { Sidebar } from "@/components/layout/sidebar"; +import { CommandPalette } from "@/components/search/CommandPalette"; import { CREATOR_LINKEDIN_URL, - OPEN_GRAPH_IMAGE_URL, SITE_DESCRIPTION, - SOCIAL_IMAGE_ALT, - SOCIAL_IMAGE_HEIGHT, - SOCIAL_IMAGE_WIDTH, SITE_NAME, SITE_URL, - TWITTER_IMAGE_URL, } from "@/lib/constants"; -import { topics } from "@/lib/topics"; import "./globals.css"; export const metadata: Metadata = { @@ -51,20 +46,13 @@ export const metadata: Metadata = { url: SITE_URL, type: "website", locale: "en_US", - images: [ - { - url: OPEN_GRAPH_IMAGE_URL, - width: SOCIAL_IMAGE_WIDTH, - height: SOCIAL_IMAGE_HEIGHT, - alt: SOCIAL_IMAGE_ALT, - }, - ], }, + // Images come from the opengraph-image.tsx file in each route segment; + // Twitter falls back to the Open Graph image when none is set here. twitter: { card: "summary_large_image", title: SITE_NAME, description: SITE_DESCRIPTION, - images: [TWITTER_IMAGE_URL], }, }; @@ -90,16 +78,6 @@ export default function RootLayout({ sameAs: [CREATOR_LINKEDIN_URL], }; - const navigationSchema = { - "@context": "https://schema.org", - "@type": "SiteNavigationElement", - name: ["JavaScript Concepts", ...topics.map((t) => t.title)], - url: [ - `${SITE_URL}/javascript`, - ...topics.map((t) => `${SITE_URL}${t.route}`), - ], - }; - return ( @@ -109,11 +87,9 @@ export default function RootLayout({ /> " }, + { num: 8, text: "" }, + ], + steps: [ + { + descriptionHtml: + '$state(0) creates a source signal: an object holding the value 0, a version counter, and an empty list of reactions. The compiler rewrites every later read of count to $.get(count) and every write to $.set(count, ...).', + activeLine: 2, + doneLines: [], + signals: [S.count("0")], + dom: [], + consoleOutput: [], + }, + { + descriptionHtml: + '$derived registers a derived signal but does not run count * 2. Deriveds are lazy: the expression runs on the first read, so until then the node has no value and no dependencies.', + activeLine: 3, + doneLines: [2], + signals: [S.count("0"), S.double("unread", "not computed", [])], + dom: [], + consoleOutput: [], + }, + { + descriptionHtml: + '$effect registers an effect and schedules it for after the component mounts. The callback has not run yet, so the effect does not know what it depends on.', + activeLine: 4, + doneLines: [2, 3], + signals: [ + S.count("0"), + S.double("unread", "not computed", []), + S.fx("scheduled", []), + ], + dom: [], + consoleOutput: [], + }, + { + descriptionHtml: + 'The compiler turned the text on line 9 into a template effect. Mounting clones the button and runs the effect: $.get(count) returns 0 and records the template effect as a reaction of count.', + activeLine: 9, + doneLines: [2, 3, 4], + signals: [ + S.count("0"), + S.double("unread", "not computed", []), + S.tpl("running", ["count"]), + S.fx("scheduled", []), + ], + dom: [btn("added"), btnText('""', "added")], + consoleOutput: [], + }, + { + descriptionHtml: + 'The same template effect reads double. The derived is unread, so it pulls now: it runs count * 2, subscribes itself to count, caches 0 and becomes clean. The template effect subscribes to double and writes the text node.', + activeLine: 9, + doneLines: [2, 3, 4], + signals: [ + S.count("0"), + S.double("clean", "0"), + S.tpl("clean", ["count", "double"]), + S.fx("scheduled", []), + ], + dom: [btn(), btnText('"0 x 2 = 0"', "updated")], + consoleOutput: [], + }, + { + descriptionHtml: + 'Effects run in a microtask after the DOM is in place. Reading double inside the callback returns the cached 0 without recomputing and subscribes the effect to double.', + activeLine: 5, + doneLines: [2, 3, 4, 9], + signals: [ + S.count("0"), + S.double("clean", "0"), + S.tpl("clean", ["count", "double"]), + S.fx("running", ["double"]), + ], + dom: [btn(), btnText('"0 x 2 = 0"')], + consoleOutput: ["double is 0"], + }, + { + descriptionHtml: + 'Click. count++ compiles to $.set(count, $.get(count) + 1). The write is the push half of push-pull: it bumps the version, marks double and the template effect dirty, marks $effect maybe-dirty because it only depends on a derived, and queues a flush. Nothing recomputes yet.', + activeLine: 8, + doneLines: [2, 3, 4, 5, 6, 9], + signals: [ + S.count("1"), + S.double("dirty", "0 (stale)"), + S.tpl("dirty", ["count", "double"]), + S.fx("maybe-dirty", ["double"]), + ], + dom: [btn(), btnText('"0 x 2 = 0"')], + consoleOutput: ["double is 0"], + }, + { + descriptionHtml: + 'Flush. Template effects run before user effects. This one re-reads count (1) and pulls double: the derived is dirty, so it recomputes to 2 and bumps its own version. The effect then calls $.set_text on the one text node it owns.', + activeLine: 9, + doneLines: [2, 3, 4, 5, 6, 8], + signals: [ + S.count("1"), + S.double("clean", "2"), + S.tpl("running", ["count", "double"]), + S.fx("maybe-dirty", ["double"]), + ], + dom: [btn(), btnText('"1 x 2 = 2"', "updated")], + consoleOutput: ["double is 0"], + }, + { + descriptionHtml: + "$effect was only maybe-dirty, so the flush checks its dependency first. The version of double changed, so the effect is promoted to dirty and runs. Had double recomputed to an equal value, the effect would have been skipped.", + activeLine: 5, + doneLines: [2, 3, 4, 6, 8, 9], + signals: [ + S.count("1"), + S.double("clean", "2"), + S.tpl("clean", ["count", "double"]), + S.fx("running", ["double"]), + ], + dom: [btn(), btnText('"1 x 2 = 2"')], + consoleOutput: ["double is 0", "double is 2"], + }, + { + descriptionHtml: + "Settled. Every node is clean. No virtual tree was built and nothing was diffed: the write pushed dirty flags along recorded edges, and each reaction pulled fresh values on demand.", + activeLine: null, + doneLines: [2, 3, 4, 5, 6, 8, 9], + signals: [ + S.count("1"), + S.double("clean", "2"), + S.tpl("clean", ["count", "double"]), + S.fx("clean", ["double"]), + ], + dom: [btn(), btnText('"1 x 2 = 2"')], + consoleOutput: ["double is 0", "double is 2"], + }, + ], + }, + { + id: "compiler", + title: "Compiled output", + description: + "The compiler resolves which DOM node depends on which signal at build time, so an update is one set_text call.", + kind: "compiler", + codeLines: [ + { num: 1, text: "" }, + { num: 4, text: "

Counter

" }, + { num: 5, text: "" }, + ], + compiledLines: [ + { num: 1, text: "import * as $ from 'svelte/internal/client';" }, + { + num: 2, + text: "var root = $.template('

Counter

');", + }, + { num: 3, text: "export default function App($$anchor) {" }, + { num: 4, text: " let count = $.state(0);" }, + { num: 5, text: " var fragment = root();" }, + { num: 6, text: " var button = $.sibling($.first_child(fragment), 2);" }, + { num: 7, text: " var text = $.child(button);" }, + { num: 8, text: " button.__click = () => $.update(count);" }, + { + num: 9, + text: " $.template_effect(() => $.set_text(text, `clicks: ${$.get(count)}`));", + }, + { num: 10, text: " $.append($$anchor, fragment);" }, + { num: 11, text: "}" }, + { num: 12, text: "$.delegate(['click']);" }, + ], + steps: [ + { + descriptionHtml: + 'The compiler rewrites $state(0) to $.state(0), which returns a source signal object. count is no longer a plain number, which is why every later read becomes $.get(count).', + activeLine: 2, + doneLines: [], + compiledActiveLine: 4, + compiledDoneLines: [1], + signals: [C.count("0")], + dom: [], + consoleOutput: [], + }, + { + descriptionHtml: + "All of the markup becomes one string passed to $.template. The structure is fixed at build time, so Svelte can clone it as real DOM later. The space inside <button> is a placeholder text node for {count}.", + activeLine: 4, + doneLines: [2], + compiledActiveLine: 2, + compiledDoneLines: [1, 4], + signals: [C.count("0")], + dom: [], + consoleOutput: [], + }, + { + descriptionHtml: + 'root() clones the template with cloneNode(true). This produces real DOM nodes immediately, not a description of DOM to diff later.', + activeLine: 5, + doneLines: [2, 4], + compiledActiveLine: 5, + compiledDoneLines: [1, 2, 4], + signals: [C.count("0")], + dom: [h1("added"), btn("added"), btnText('" "', "added")], + consoleOutput: [], + }, + { + descriptionHtml: + "$.sibling and $.child walk to the text node once and keep a reference. The compiler knew at build time that only this node depends on count, so there is nothing to search for on update.", + activeLine: 6, + doneLines: [2, 4, 5], + compiledActiveLine: 7, + compiledDoneLines: [1, 2, 4, 5, 6], + signals: [C.count("0")], + dom: [h1(), btn(), btnText('" "', "active")], + consoleOutput: [], + }, + { + descriptionHtml: + "onclick={() => count++} becomes a property assignment, button.__click. $.delegate(['click']) on the last line installs one root listener that walks up from the event target looking for __click, so no listener is attached per element.", + activeLine: 5, + doneLines: [2, 4, 6], + compiledActiveLine: 8, + compiledDoneLines: [1, 2, 4, 5, 6, 7], + signals: [C.count("0")], + dom: [h1(), btn("active"), btnText('" "')], + consoleOutput: [], + }, + { + descriptionHtml: + '$.template_effect wraps the {count} expression and runs it right away. $.get(count) returns 0 and records this template effect as a reaction of count. $.set_text writes text.nodeValue.', + activeLine: 6, + doneLines: [2, 4, 5], + compiledActiveLine: 9, + compiledDoneLines: [1, 2, 4, 5, 6, 7, 8], + signals: [C.count("0"), C.tpl("running")], + dom: [h1(), btn(), btnText('"clicks: 0"', "updated")], + consoleOutput: [], + }, + { + descriptionHtml: + "$.append inserts the fragment at the mount point. Setup is done: one signal, one template effect, and one recorded edge between them.", + activeLine: 7, + doneLines: [2, 4, 5, 6], + compiledActiveLine: 10, + compiledDoneLines: [1, 2, 4, 5, 6, 7, 8, 9], + signals: [C.count("0"), C.tpl("clean")], + dom: [h1(), btn(), btnText('"clicks: 0"')], + consoleOutput: [], + }, + { + descriptionHtml: + 'Click. The delegated listener finds __click and calls $.update(count), shorthand for $.set(count, $.get(count) + 1). The write bumps the version, marks the template effect dirty and queues a microtask flush. The DOM is untouched so far.', + activeLine: 5, + doneLines: [2, 4, 6, 7], + compiledActiveLine: 8, + compiledDoneLines: [1, 2, 4, 5, 6, 7, 9, 10, 12], + signals: [C.count("1"), C.tpl("dirty")], + dom: [h1(), btn(), btnText('"clicks: 0"')], + consoleOutput: [], + }, + { + descriptionHtml: + "Flush. The template effect re-runs its closure: $.get(count) now returns 1 and $.set_text assigns the new string to the same text node. One property write on one node.", + activeLine: 6, + doneLines: [2, 4, 5, 7], + compiledActiveLine: 9, + compiledDoneLines: [1, 2, 4, 5, 6, 7, 8, 10, 12], + signals: [C.count("1"), C.tpl("running")], + dom: [h1(), btn(), btnText('"clicks: 1"', "updated")], + consoleOutput: [], + }, + { + descriptionHtml: + "Settled. The <h1> and the <button> were never revisited because no effect reads them. There was no virtual tree to build and no diff to walk: the compiler resolved which node depends on which signal before the code ever ran.", + activeLine: null, + doneLines: [2, 4, 5, 6, 7], + compiledActiveLine: null, + compiledDoneLines: [1, 2, 4, 5, 6, 7, 8, 9, 10, 12], + signals: [C.count("1"), C.tpl("clean")], + dom: [h1(), btn(), btnText('"clicks: 1"')], + consoleOutput: [], + }, + ], + }, + { + id: "deep", + title: "Deep state and proxies", + description: + "$state proxies objects and arrays so nested writes are tracked. $state.raw opts out and only reacts to reassignment.", + kind: "deep", + codeLines: [ + { num: 1, text: "" }, + { num: 12, text: "{#each todos as todo}" }, + { num: 13, text: "
  • {todo.text} {todo.done ? 'done' : 'open'}
  • " }, + { num: 14, text: "{/each}" }, + { num: 15, text: "

    raw: {raw.n}

    " }, + ], + steps: [ + { + descriptionHtml: + '$state receives an array, so it wraps it in a Proxy and stores the proxy in the todos source. No per-property signals exist yet; the proxy creates one for each property the first time something reads it.', + activeLine: 2, + doneLines: [], + signals: [D.todos()], + dom: [], + consoleOutput: [], + }, + { + descriptionHtml: + "$state.raw creates a source that holds the object as-is, with no proxy. Reassigning raw is tracked; writing raw.n is not, because there is no trap to observe it.", + activeLine: 3, + doneLines: [2], + signals: [D.todos(), D.raw("{ n: 0 }")], + dom: [], + consoleOutput: [], + }, + { + descriptionHtml: + '$effect is registered and scheduled for after mount. Its dependencies will be whatever it reads when it runs.', + activeLine: 4, + doneLines: [2, 3], + signals: [D.todos(), D.raw("{ n: 0 }"), D.fx("scheduled", [])], + dom: [], + consoleOutput: [], + }, + { + descriptionHtml: + 'Mount. The each block is a template effect. It reads todos, then length through the proxy get trap, which creates the todos.length source on demand and subscribes the block to it.', + activeLine: 12, + doneLines: [2, 3, 4], + signals: [ + D.todos(), + D.len("1"), + D.raw("{ n: 0 }"), + D.each("running"), + D.fx("scheduled", []), + ], + dom: [], + consoleOutput: [], + }, + { + descriptionHtml: + "The block reads todos[0], which creates an index source and wraps the nested object in its own proxy. The <li> template effect reads done, creating todos[0].done and subscribing to it.", + activeLine: 13, + doneLines: [2, 3, 4, 12], + signals: [ + D.todos(), + D.len("1"), + D.i0(), + D.i0done("false"), + D.raw("{ n: 0 }"), + D.each("clean"), + D.li0("running"), + D.fx("scheduled", []), + ], + dom: [li("li0", '"write open"', "added")], + consoleOutput: [], + }, + { + descriptionHtml: + "The <p> effect reads raw, so it subscribes to that source. The following .n read is a plain property access on a plain object, and the graph does not see it.", + activeLine: 15, + doneLines: [2, 3, 4, 12, 13, 14], + signals: [ + D.todos(), + D.len("1"), + D.i0(), + D.i0done("false"), + D.raw("{ n: 0 }"), + D.each("clean"), + D.li0("clean"), + D.p("running"), + D.fx("scheduled", []), + ], + dom: [li("li0", '"write open"'), pRaw('"raw: 0"', "added")], + consoleOutput: [], + }, + { + descriptionHtml: + 'The effect runs in a microtask after the DOM is ready. filter reads length and each item\'s done, so the effect subscribes to every source it touched.', + activeLine: 5, + doneLines: [2, 3, 4, 12, 13, 14, 15], + signals: [ + D.todos(), + D.len("1"), + D.i0(), + D.i0done("false"), + D.raw("{ n: 0 }"), + D.each("clean"), + D.li0("clean"), + D.p("clean"), + D.fx("running", ["todos.length", "todos[0].done"]), + ], + dom: [li("li0", '"write open"'), pRaw('"raw: 0"')], + consoleOutput: ["open: 1"], + }, + { + descriptionHtml: + 'push goes through the proxy\'s set trap twice: it writes index 1, creating a todos[1] source, then writes length to 2. The reactions of length, the each block and $effect, are marked dirty and a flush is queued. li #0 is untouched.', + activeLine: 7, + doneLines: [2, 3, 4, 5, 6, 12, 13, 14, 15], + signals: [ + D.todos(), + D.len("2"), + D.i0(), + D.i0done("false"), + D.i1(), + D.raw("{ n: 0 }"), + D.each("dirty"), + D.li0("clean"), + D.p("clean"), + D.fx("dirty", ["todos.length", "todos[0].done"]), + ], + dom: [li("li0", '"write open"'), pRaw('"raw: 0"')], + consoleOutput: ["open: 1"], + }, + { + descriptionHtml: + "Flush. The each block sees length 2 and mounts one more <li> with its own template effect, which reads and creates todos[1].done. The first <li> keeps its DOM nodes.", + activeLine: 13, + doneLines: [2, 3, 4, 5, 6, 7, 12, 14, 15], + signals: [ + D.todos(), + D.len("2"), + D.i0(), + D.i0done("false"), + D.i1(), + D.i1done("false"), + D.raw("{ n: 0 }"), + D.each("running"), + D.li0("clean"), + D.li1("clean"), + D.p("clean"), + D.fx("dirty", ["todos.length", "todos[0].done"]), + ], + dom: [ + li("li0", '"write open"'), + li("li1", '"ship open"', "added"), + pRaw('"raw: 0"'), + ], + consoleOutput: ["open: 1"], + }, + { + descriptionHtml: + "The effect re-runs and now also reads todos[1].done, so its dependency list grows. Dependencies are collected fresh on every run, never declared up front.", + activeLine: 5, + doneLines: [2, 3, 4, 7, 12, 13, 14, 15], + signals: [ + D.todos(), + D.len("2"), + D.i0(), + D.i0done("false"), + D.i1(), + D.i1done("false"), + D.raw("{ n: 0 }"), + D.each("clean"), + D.li0("clean"), + D.li1("clean"), + D.p("clean"), + D.fx("running", ["todos.length", "todos[0].done", "todos[1].done"]), + ], + dom: [ + li("li0", '"write open"'), + li("li1", '"ship open"'), + pRaw('"raw: 0"'), + ], + consoleOutput: ["open: 1", "open: 2"], + }, + { + descriptionHtml: + 'todos[0].done = true hits the nested proxy\'s set trap and writes the todos[0].done source. Only its reactions, li #0 and $effect, are marked dirty. The each block and li #1 depend on other sources and stay clean.', + activeLine: 8, + doneLines: [2, 3, 4, 5, 6, 7, 12, 13, 14, 15], + signals: [ + D.todos(), + D.len("2"), + D.i0(), + D.i0done("true"), + D.i1(), + D.i1done("false"), + D.raw("{ n: 0 }"), + D.each("clean"), + D.li0("dirty"), + D.li1("clean"), + D.p("clean"), + D.fx("dirty", ["todos.length", "todos[0].done", "todos[1].done"]), + ], + dom: [ + li("li0", '"write open"'), + li("li1", '"ship open"'), + pRaw('"raw: 0"'), + ], + consoleOutput: ["open: 1", "open: 2"], + }, + { + descriptionHtml: + "Flush. The li #0 template effect re-evaluates its expression and calls $.set_text on its own text node. No list diff ran; the compiler tied this node to todos[0].done when it created it.", + activeLine: 13, + doneLines: [2, 3, 4, 5, 6, 7, 8, 12, 14, 15], + signals: [ + D.todos(), + D.len("2"), + D.i0(), + D.i0done("true"), + D.i1(), + D.i1done("false"), + D.raw("{ n: 0 }"), + D.each("clean"), + D.li0("running"), + D.li1("clean"), + D.p("clean"), + D.fx("dirty", ["todos.length", "todos[0].done", "todos[1].done"]), + ], + dom: [ + li("li0", '"write done"', "updated"), + li("li1", '"ship open"'), + pRaw('"raw: 0"'), + ], + consoleOutput: ["open: 1", "open: 2"], + }, + { + descriptionHtml: + "The effect runs after the DOM update and logs the new count. Both consumers of todos[0].done ran exactly once for a single nested write.", + activeLine: 5, + doneLines: [2, 3, 4, 7, 8, 12, 13, 14, 15], + signals: [ + D.todos(), + D.len("2"), + D.i0(), + D.i0done("true"), + D.i1(), + D.i1done("false"), + D.raw("{ n: 0 }"), + D.each("clean"), + D.li0("clean"), + D.li1("clean"), + D.p("clean"), + D.fx("running", ["todos.length", "todos[0].done", "todos[1].done"]), + ], + dom: [ + li("li0", '"write done"'), + li("li1", '"ship open"'), + pRaw('"raw: 0"'), + ], + consoleOutput: ["open: 1", "open: 2", "open: 1"], + }, + { + descriptionHtml: + "raw.n = 1 is an ordinary property write on an ordinary object. No trap runs, no source is written, nothing is scheduled, and the <p> still shows 0. This is the trade: $state.raw skips proxy overhead for large or immutable data at the cost of tracking only reassignment.", + activeLine: 9, + doneLines: [2, 3, 4, 5, 6, 7, 8, 12, 13, 14, 15], + signals: [ + D.todos(), + D.len("2"), + D.i0(), + D.i0done("true"), + D.i1(), + D.i1done("false"), + D.raw("{ n: 1 }", "write not tracked"), + D.each("clean"), + D.li0("clean"), + D.li1("clean"), + D.p("clean"), + D.fx("clean", ["todos.length", "todos[0].done", "todos[1].done"]), + ], + dom: [ + li("li0", '"write done"'), + li("li1", '"ship open"'), + pRaw('"raw: 0"'), + ], + consoleOutput: ["open: 1", "open: 2", "open: 1"], + }, + { + descriptionHtml: + "raw = { n: raw.n } is a reassignment, so the compiler emitted $.set(raw, ...). That writes the source, marks the <p> effect dirty, and the flush sets the text to 1.", + activeLine: 10, + doneLines: [2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15], + signals: [ + D.todos(), + D.len("2"), + D.i0(), + D.i0done("true"), + D.i1(), + D.i1done("false"), + D.raw("{ n: 1 }", "reassigned"), + D.each("clean"), + D.li0("clean"), + D.li1("clean"), + D.p("running"), + D.fx("clean", ["todos.length", "todos[0].done", "todos[1].done"]), + ], + dom: [ + li("li0", '"write done"'), + li("li1", '"ship open"'), + pRaw('"raw: 1"', "updated"), + ], + consoleOutput: ["open: 1", "open: 2", "open: 1"], + }, + ], + }, +]; diff --git a/src/components/visualizations/svelte-runes/helpers.ts b/src/components/visualizations/svelte-runes/helpers.ts new file mode 100644 index 0000000..c1dc939 --- /dev/null +++ b/src/components/visualizations/svelte-runes/helpers.ts @@ -0,0 +1,69 @@ +import { + createKindBadgeClass, + createKindLabel, +} from "@/lib/visualization-helpers"; +import type { + DomNodeState, + SignalNodeType, + SignalStatus, + SvelteRunesKind, +} from "./types"; + +export const kindBadgeClass = createKindBadgeClass({ + signals: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25", + compiler: "bg-violet-500/15 text-violet-400 border-violet-500/25", + deep: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25", +}); + +export const kindLabel = createKindLabel({ + signals: "signal graph", + compiler: "compiled output", + deep: "deep state", +}); + +export const SIGNAL_TYPE_LABELS: Record = { + source: "src", + derived: "drv", + template: "tpl", + effect: "fx", +}; + +export const SIGNAL_TYPE_STYLES: Record = { + source: "bg-cyan-500/15 text-cyan-300 border-cyan-500/30", + derived: "bg-violet-500/15 text-violet-300 border-violet-500/30", + template: "bg-amber-500/15 text-amber-300 border-amber-500/30", + effect: "bg-emerald-500/15 text-emerald-300 border-emerald-500/30", +}; + +export const SIGNAL_STATUS_STYLES: Record = { + clean: "bg-slate-500/10 text-slate-400 border-slate-500/25", + unread: "bg-slate-500/10 text-slate-400 border-dashed border-slate-500/40", + dirty: "bg-rose-500/15 text-rose-300 border-rose-500/30", + "maybe-dirty": "bg-amber-500/15 text-amber-300 border-amber-500/30", + scheduled: "bg-amber-500/15 text-amber-300 border-amber-500/30", + running: "bg-cyan-500/15 text-cyan-200 border-cyan-400/40", +}; + +export const SIGNAL_ROW_STYLES: Record = { + clean: "border-slate-500/30 bg-slate-800/30", + unread: "border-slate-500/30 bg-slate-800/20", + dirty: "border-rose-400/40 bg-rose-400/8", + "maybe-dirty": "border-amber-400/35 bg-amber-400/8", + scheduled: "border-amber-400/35 bg-amber-400/8", + running: + "border-cyan-300/50 bg-cyan-400/10 shadow-[0_0_16px_rgba(34,211,238,0.14)]", +}; + +export const DOM_STATE_STYLES: Record = { + unchanged: "border-slate-500/30 bg-slate-800/30 text-slate-400", + added: "border-emerald-400/40 bg-emerald-400/8 text-emerald-200", + updated: "border-amber-400/40 bg-amber-400/10 text-amber-200", + active: "border-cyan-300/50 bg-cyan-400/10 text-cyan-100", +}; + +export const DOM_STATE_LABELS: Record = { + unchanged: null, + added: "added", + updated: "set_text", + active: "ref", +}; diff --git a/src/components/visualizations/svelte-runes/index.tsx b/src/components/visualizations/svelte-runes/index.tsx new file mode 100644 index 0000000..85eb785 --- /dev/null +++ b/src/components/visualizations/svelte-runes/index.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; +import { ConsoleOutput } from "@/components/visualization-ui/ConsoleOutput"; +import { + ExamplePicker, + KindBadge, +} from "@/components/visualization-ui/ExamplePicker"; +import { SourceCodePanel } from "@/components/visualization-ui/SourceCodePanel"; +import { + SourceGrid, + VisualizationSection, + WaitingPlaceholder, +} from "@/components/visualization-ui/VisualizationLayout"; +import { VisualizationToolbar } from "@/components/visualization-ui/VisualizationToolbar"; +import { useExampleTopic } from "@/hooks/useExampleTopic"; +import { useChangeFlash } from "@/hooks/useChangeFlash"; +import { EXAMPLES } from "./data"; +import { kindBadgeClass, kindLabel } from "./helpers"; +import { SignalGraphPanel } from "./components/SignalGraphPanel"; +import { DomPanel } from "./components/DomPanel"; + +export const SvelteRunes = () => { + const { + example, + activeExampleId, + handleExampleChange, + playback, + currentStep, + } = useExampleTopic(EXAMPLES); + const { currentStepIndex } = playback; + const showConsole = example.steps.some( + (step) => step.consoleOutput.length > 0, + ); + + const flashes = useChangeFlash( + { + description: currentStep?.descriptionHtml, + signals: currentStep?.signals, + dom: currentStep?.dom, + console: currentStep?.consoleOutput, + compiled: currentStep?.compiledActiveLine, + }, + currentStepIndex, + ); + + return ( + <> + ( + + {kindLabel(ex.kind)} + + )} + /> + } + /> + + + + + +
    + {example.compiledLines && ( + + )} + +
    + + {currentStep ? ( + + ) : ( + + )} + + + + {currentStep ? ( + + ) : ( + + )} + +
    + + {showConsole && ( +
    + +
    + )} +
    +
    +
    + + ); +}; diff --git a/src/components/visualizations/svelte-runes/types.ts b/src/components/visualizations/svelte-runes/types.ts new file mode 100644 index 0000000..c44601f --- /dev/null +++ b/src/components/visualizations/svelte-runes/types.ts @@ -0,0 +1,56 @@ +import type { SourceLine, BaseStep } from "@/types/visualization"; +import type { ExampleOption } from "@/components/visualization-ui/ExampleSelector"; + +export type SvelteRunesKind = "signals" | "compiler" | "deep"; + +/** Node kinds in the reactive graph. Template effects are the compiler's render effects. */ +export type SignalNodeType = "source" | "derived" | "template" | "effect"; + +/** + * Status flags modelled on the Svelte 5 internals: + * unread is a derived that has never been pulled, maybe-dirty is a reaction + * whose only changed dependency is a derived that may recompute to an equal value. + */ +export type SignalStatus = + | "clean" + | "unread" + | "dirty" + | "maybe-dirty" + | "scheduled" + | "running"; + +export interface SignalNode { + id: string; + name: string; + type: SignalNodeType; + status: SignalStatus; + value?: string; + /** Names of the signals this node read on its last run. */ + deps: string[]; + note?: string; +} + +export type DomNodeState = "unchanged" | "added" | "updated" | "active"; + +export interface DomNode { + id: string; + label: string; + text?: string; + state: DomNodeState; + depth?: number; +} + +export interface SvelteRunesStep extends BaseStep { + signals: SignalNode[]; + dom: DomNode[]; + consoleOutput: string[]; + compiledActiveLine?: number | null; + compiledDoneLines?: number[]; +} + +export interface SvelteRunesExample extends ExampleOption { + kind: SvelteRunesKind; + codeLines: SourceLine[]; + compiledLines?: SourceLine[]; + steps: SvelteRunesStep[]; +} diff --git a/src/components/visualizations/this-keyword/components/MemoryObjects.tsx b/src/components/visualizations/this-keyword/components/MemoryObjects.tsx index ba262d2..8e8b45d 100644 --- a/src/components/visualizations/this-keyword/components/MemoryObjects.tsx +++ b/src/components/visualizations/this-keyword/components/MemoryObjects.tsx @@ -1,7 +1,7 @@ import { cn } from "@/lib/utils"; import type { MemoryObject } from "../types"; -export function MemoryObjects({ objects }: { objects: MemoryObject[] }) { +export const MemoryObjects = ({ objects }: { objects: MemoryObject[] }) => { if (objects.length === 0) { return (

    @@ -21,7 +21,7 @@ export function MemoryObjects({ objects }: { objects: MemoryObject[] }) { ? "border-emerald-300/40 bg-emerald-400/8 shadow-[0_0_18px_rgba(52,211,153,0.1)]" : obj.highlight === "active" ? "border-amber-300/40 bg-amber-400/8 shadow-[0_0_18px_rgba(251,191,36,0.1)]" - : "border-slate-500/30 bg-slate-800/30" + : "border-slate-500/30 bg-slate-800/30", )} >

    @@ -32,7 +32,7 @@ export function MemoryObjects({ objects }: { objects: MemoryObject[] }) { ? "text-emerald-300" : obj.highlight === "active" ? "text-amber-300" - : "text-slate-400" + : "text-slate-400", )} > {obj.label} @@ -64,4 +64,4 @@ export function MemoryObjects({ objects }: { objects: MemoryObject[] }) { ))}
    ); -} +}; diff --git a/src/components/visualizations/this-keyword/components/ThisBindingCard.tsx b/src/components/visualizations/this-keyword/components/ThisBindingCard.tsx index 01f55c4..53fb119 100644 --- a/src/components/visualizations/this-keyword/components/ThisBindingCard.tsx +++ b/src/components/visualizations/this-keyword/components/ThisBindingCard.tsx @@ -2,7 +2,11 @@ import { cn } from "@/lib/utils"; import { ruleColorClass } from "../helpers"; import type { ThisBinding } from "../types"; -export function ThisBindingCard({ binding }: { binding: ThisBinding | null }) { +export const ThisBindingCard = ({ + binding, +}: { + binding: ThisBinding | null; +}) => { if (!binding) { return (

    @@ -21,7 +25,7 @@ export function ThisBindingCard({ binding }: { binding: ThisBinding | null }) { {binding.ruleLabel} @@ -60,7 +64,7 @@ export function ThisBindingCard({ binding }: { binding: ThisBinding | null }) { "rounded-lg border px-3 py-1.5 font-mono text-sm font-semibold", binding.value === "undefined" ? "border-rose-300/35 bg-rose-400/10 text-rose-200" - : "border-emerald-300/35 bg-emerald-400/10 text-emerald-200" + : "border-emerald-300/35 bg-emerald-400/10 text-emerald-200", )} > {binding.value} @@ -68,4 +72,4 @@ export function ThisBindingCard({ binding }: { binding: ThisBinding | null }) {

    ); -} +}; diff --git a/src/components/visualizations/this-keyword/data.ts b/src/components/visualizations/this-keyword/data.ts index bb584b0..11189ef 100644 --- a/src/components/visualizations/this-keyword/data.ts +++ b/src/components/visualizations/this-keyword/data.ts @@ -23,7 +23,7 @@ export const EXAMPLES: ThisExample[] = [ steps: [ { descriptionHtml: - 'Create the user object with a name property and a greet method. No function calls yet, so this has no binding.', + "Create the user object with a name property and a greet method. No function calls yet, so this has no binding.", activeLine: 1, doneLines: [], objects: [ @@ -41,7 +41,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'user.greet() is called. The key is the dot before greet - the object to the left of the dot determines this.', + "user.greet() is called. The key is the dot before greet - the object to the left of the dot determines this.", activeLine: 8, doneLines: [1, 2, 3, 4, 5, 6], objects: [ @@ -87,7 +87,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'Now user.greet is extracted into a standalone variable greetFn. The function reference is copied, but the object binding is not carried along.', + "Now user.greet is extracted into a standalone variable greetFn. The function reference is copied, but the object binding is not carried along.", activeLine: 9, doneLines: [1, 2, 3, 4, 5, 6, 8], objects: [ @@ -105,7 +105,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'greetFn() is called - no dot, no object in front. This is a plain function call.', + "greetFn() is called - no dot, no object in front. This is a plain function call.", activeLine: 10, doneLines: [1, 2, 3, 4, 5, 6, 8, 9], objects: [ @@ -128,7 +128,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'Inside greet() again, but now this is undefined (strict mode). The binding was lost when the method was extracted. this.name is undefined.', + "Inside greet() again, but now this is undefined (strict mode). The binding was lost when the method was extracted. this.name is undefined.", activeLine: 4, doneLines: [1, 2, 3, 5, 6, 8, 9, 10], objects: [ @@ -151,7 +151,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'Key takeaway: The value of this depends on how the function is called, not where it is defined. obj.method() binds this to obj. A plain func() call loses the binding.', + "Key takeaway: The value of this depends on how the function is called, not where it is defined. obj.method() binds this to obj. A plain func() call loses the binding.", activeLine: null, doneLines: [1, 2, 3, 4, 5, 6, 8, 9, 10], objects: [ @@ -193,7 +193,7 @@ export const EXAMPLES: ThisExample[] = [ steps: [ { descriptionHtml: - 'Declare a standalone greet function that references this.name. On its own, this would be undefined - we will control it explicitly.', + "Declare a standalone greet function that references this.name. On its own, this would be undefined - we will control it explicitly.", activeLine: 1, doneLines: [], objects: [], @@ -202,7 +202,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'Create two objects: alice and bob. Neither has a greet method - we will borrow the standalone function.', + "Create two objects: alice and bob. Neither has a greet method - we will borrow the standalone function.", activeLine: 5, doneLines: [1, 2, 3], objects: [ @@ -297,7 +297,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'boundGreet() - even though this is a plain call (no dot, no explicit context), this is still alice because bind locks it permanently.', + "boundGreet() - even though this is a plain call (no dot, no explicit context), this is still alice because bind locks it permanently.", activeLine: 11, doneLines: [1, 2, 3, 5, 6, 8, 9, 10], objects: [ @@ -322,7 +322,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'Key takeaway: call and apply invoke immediately with a chosen this. bind returns a new function with this permanently locked - even future plain calls cannot override it.', + "Key takeaway: call and apply invoke immediately with a chosen this. bind returns a new function with this permanently locked - even future plain calls cannot override it.", activeLine: null, doneLines: [1, 2, 3, 5, 6, 8, 9, 10, 11], objects: [ @@ -366,7 +366,7 @@ export const EXAMPLES: ThisExample[] = [ steps: [ { descriptionHtml: - 'Declare User as a constructor function. By convention, constructor names start with a capital letter.', + "Declare User as a constructor function. By convention, constructor names start with a capital letter.", activeLine: 1, doneLines: [], objects: [], @@ -395,7 +395,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'console.log(this) - this is the new empty object. It has no properties yet.', + "console.log(this) - this is the new empty object. It has no properties yet.", activeLine: 2, doneLines: [1, 7, 9], objects: [ @@ -415,7 +415,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'this.name = name - the name property is added to the new object via this. The object is being built up step by step.', + "this.name = name - the name property is added to the new object via this. The object is being built up step by step.", activeLine: 3, doneLines: [1, 2, 7, 9], objects: [ @@ -435,7 +435,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'this.greet = function()... - a greet method is attached to the new object. The constructor finishes, and new returns this object as alice.', + "this.greet = function()... - a greet method is attached to the new object. The constructor finishes, and new returns this object as alice.", activeLine: 4, doneLines: [1, 2, 3, 7, 9], objects: [ @@ -458,7 +458,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'alice.greet() - now this is a regular method call with implicit binding. The dot rule applies: this is alice.', + "alice.greet() - now this is a regular method call with implicit binding. The dot rule applies: this is alice.", activeLine: 10, doneLines: [1, 2, 3, 4, 5, 6, 7, 9], objects: [ @@ -481,7 +481,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'Key takeaway: new does three things: (1) creates a fresh empty object, (2) binds this to it inside the constructor, (3) returns the object. After construction, the dot rule (implicit binding) applies as usual.', + "Key takeaway: new does three things: (1) creates a fresh empty object, (2) binds this to it inside the constructor, (3) returns the object. After construction, the dot rule (implicit binding) applies as usual.", activeLine: 11, doneLines: [1, 2, 3, 4, 5, 6, 7, 9, 10], objects: [ @@ -523,7 +523,7 @@ export const EXAMPLES: ThisExample[] = [ steps: [ { descriptionHtml: - 'Create the team object with a name, a members array, and a show method.', + "Create the team object with a name, a members array, and a show method.", activeLine: 1, doneLines: [], objects: [ @@ -542,7 +542,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'team.show() is called. Implicit binding: the dot rule means this inside show will be team.', + "team.show() is called. Implicit binding: the dot rule means this inside show will be team.", activeLine: 11, doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9], objects: [ @@ -566,7 +566,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'Inside show(), this is team. The forEach callback is an arrow function - it does not create its own this binding.', + "Inside show(), this is team. The forEach callback is an arrow function - it does not create its own this binding.", activeLine: 5, doneLines: [1, 2, 3, 8, 9, 11], objects: [ @@ -590,7 +590,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'The arrow function captures this from its enclosing scope (show). Since show was called as team.show(), the arrow inherits this = team.', + "The arrow function captures this from its enclosing scope (show). Since show was called as team.show(), the arrow inherits this = team.", activeLine: 5, doneLines: [1, 2, 3, 8, 9, 11], objects: [ @@ -662,7 +662,7 @@ export const EXAMPLES: ThisExample[] = [ }, { descriptionHtml: - 'Key takeaway: Arrow functions inherit this from the enclosing function scope at the time they are defined. If forEach used a regular function instead, this would be undefined - the classic gotcha arrow functions were designed to fix.', + "Key takeaway: Arrow functions inherit this from the enclosing function scope at the time they are defined. If forEach used a regular function instead, this would be undefined - the classic gotcha arrow functions were designed to fix.", activeLine: null, doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11], objects: [ diff --git a/src/components/visualizations/this-keyword/helpers.ts b/src/components/visualizations/this-keyword/helpers.ts index accaadb..1782f2c 100644 --- a/src/components/visualizations/this-keyword/helpers.ts +++ b/src/components/visualizations/this-keyword/helpers.ts @@ -1,4 +1,7 @@ -import { createKindBadgeClass, createKindLabel } from "@/lib/visualization-helpers"; +import { + createKindBadgeClass, + createKindLabel, +} from "@/lib/visualization-helpers"; import type { BindingKind, BindingRule } from "./types"; export const kindBadgeClass = createKindBadgeClass({ diff --git a/src/components/visualizations/this-keyword/index.tsx b/src/components/visualizations/this-keyword/index.tsx index 79c43f9..0038eaf 100644 --- a/src/components/visualizations/this-keyword/index.tsx +++ b/src/components/visualizations/this-keyword/index.tsx @@ -1,54 +1,33 @@ "use client"; -import { useState } from "react"; -import { Badge } from "@/components/ui/badge"; import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; -import { - CodeBlock, - type CodeBlockLine, -} from "@/components/visualization-ui/CodeBlock"; import { ConsoleOutput } from "@/components/visualization-ui/ConsoleOutput"; -import { TransportControls } from "@/components/visualization-ui/TransportControls"; -import { ExampleSelector } from "@/components/visualization-ui/ExampleSelector"; -import { ToolbarPortal } from "@/components/layout/ToolbarPortal"; -import { cn } from "@/lib/utils"; import { - VISUALIZATION_PANEL_TITLES, - VISUALIZATION_EMPTY_STATES, -} from "@/lib/visualization/uiCopy"; -import { useStepPlayback } from "@/hooks/useStepPlayback"; + ExamplePicker, + KindBadge, +} from "@/components/visualization-ui/ExamplePicker"; +import { SourceCodePanel } from "@/components/visualization-ui/SourceCodePanel"; +import { + SourceGrid, + VisualizationSection, +} from "@/components/visualization-ui/VisualizationLayout"; +import { VisualizationToolbar } from "@/components/visualization-ui/VisualizationToolbar"; +import { useExampleTopic } from "@/hooks/useExampleTopic"; import { useChangeFlash } from "@/hooks/useChangeFlash"; import { EXAMPLES } from "./data"; import { kindBadgeClass, kindLabel } from "./helpers"; import { ThisBindingCard } from "./components/ThisBindingCard"; import { MemoryObjects } from "./components/MemoryObjects"; -export function ThisKeyword() { - const [activeExampleId, setActiveExampleId] = useState(EXAMPLES[0].id); - - const example = - EXAMPLES.find((e) => e.id === activeExampleId) ?? EXAMPLES[0]; - +export const ThisKeyword = () => { const { - currentStepIndex, - isPlaying, - speedLevel, - speedLabel, - canStep, - canStepBack, - togglePlay, - step: handleStep, - stepBack: handleStepBack, - reset: handleReset, - setSpeedLevel, - } = useStepPlayback({ - totalSteps: example.steps.length, - initialStep: -1, - resetKey: activeExampleId, - }); - - const currentStep = - currentStepIndex >= 0 ? example.steps[currentStepIndex] : null; + example, + activeExampleId, + handleExampleChange, + playback, + currentStep, + } = useExampleTopic(EXAMPLES); + const { currentStepIndex } = playback; const flashes = useChangeFlash( { @@ -60,103 +39,36 @@ export function ThisKeyword() { currentStepIndex, ); - const handleExampleChange = (id: string) => { - setActiveExampleId(id); - }; - return ( <> {/* Toolbar: portaled above the surface card */} - -
    -
    -
    - ( - - {kindLabel(ex.kind)} - - )} - /> - - {kindLabel(example.kind)} - -
    - - -
    - -
    ( + + {kindLabel(ex.kind)} + )} - > - {currentStep ? ( -

    - ) : ( -

    - {VISUALIZATION_EMPTY_STATES.stepDescription} -

    - )} -
    -
    -
    + /> + } + /> {/* Main visualization */} -
    -
    - - { - const isActive = currentStep?.activeLine === line.num; - const isDone = - currentStep?.doneLines.includes(line.num) ?? false; - return { - key: line.num, - lineNumber: line.num, - text: line.text, - className: cn( - isActive && "is-active", - isDone && !isActive && "is-done" - ), - }; - })} - /> - + + +
    - + -
    +
    -
    -
    + + ); -} +}; diff --git a/src/components/visualizations/this-keyword/types.ts b/src/components/visualizations/this-keyword/types.ts index b76c428..8a63812 100644 --- a/src/components/visualizations/this-keyword/types.ts +++ b/src/components/visualizations/this-keyword/types.ts @@ -1,4 +1,4 @@ -import type { SourceLine } from "@/types/visualization"; +import type { SourceLine, BaseStep } from "@/types/visualization"; export type BindingKind = "implicit" | "explicit" | "new" | "arrow"; @@ -25,10 +25,7 @@ export interface ThisBinding { callExpression: string; } -export interface ThisStep { - descriptionHtml: string; - activeLine: number | null; - doneLines: number[]; +export interface ThisStep extends BaseStep { objects: MemoryObject[]; thisBinding: ThisBinding | null; consoleOutput: string[]; diff --git a/src/components/visualizations/tokenization/components/SegmentGroups.tsx b/src/components/visualizations/tokenization/components/SegmentGroups.tsx new file mode 100644 index 0000000..5b67f6a --- /dev/null +++ b/src/components/visualizations/tokenization/components/SegmentGroups.tsx @@ -0,0 +1,36 @@ +import { TokenChips } from "@/components/visualization-ui/TokenChips"; +import type { SegmentGroup } from "../types"; + +interface SegmentGroupsProps { + groups: SegmentGroup[]; +} + +export const SegmentGroups = ({ groups }: SegmentGroupsProps) => { + if (groups.length === 0) { + return ( +

    + waiting +

    + ); + } + + return ( +
    + {groups.map((group) => ( +
    + + {group.label} + + +
    + ))} +
    + ); +}; diff --git a/src/components/visualizations/tokenization/components/StatePanels.tsx b/src/components/visualizations/tokenization/components/StatePanels.tsx new file mode 100644 index 0000000..e5bde73 --- /dev/null +++ b/src/components/visualizations/tokenization/components/StatePanels.tsx @@ -0,0 +1,83 @@ +import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; +import { TokenChips } from "@/components/visualization-ui/TokenChips"; +import { MetricBars } from "@/components/visualization-ui/MetricBars"; +import { panelTitles } from "../helpers"; +import type { TokenizationKind, TokenizationStep } from "../types"; +import { SegmentGroups } from "./SegmentGroups"; +import { StatsGrid } from "./StatsGrid"; + +interface StatePanelsProps { + kind: TokenizationKind; + step: TokenizationStep | null; + flashes: { + segments: boolean; + rules: boolean; + aux: boolean; + stats: boolean; + }; +} + +const Waiting = () => ( +

    + waiting +

    +); + +export const StatePanels = ({ kind, step, flashes }: StatePanelsProps) => { + const titles = panelTitles(kind); + const isTraining = kind === "bpe"; + + return ( +
    + + + + +
    + + {step ? ( + + ) : ( + + )} + + + + {!step ? ( + + ) : isTraining ? ( + + ) : ( + + )} + +
    + + + + +
    + ); +}; diff --git a/src/components/visualizations/tokenization/components/StatsGrid.tsx b/src/components/visualizations/tokenization/components/StatsGrid.tsx new file mode 100644 index 0000000..42fbb4d --- /dev/null +++ b/src/components/visualizations/tokenization/components/StatsGrid.tsx @@ -0,0 +1,38 @@ +import { cn } from "@/lib/utils"; +import { STAT_TONE_STYLES } from "../helpers"; +import type { StatItem } from "../types"; + +interface StatsGridProps { + stats: StatItem[]; +} + +export const StatsGrid = ({ stats }: StatsGridProps) => { + if (stats.length === 0) { + return ( +

    + waiting +

    + ); + } + + return ( +
      + {stats.map((item) => ( +
    • + + {item.label} + + + {item.value} + +
    • + ))} +
    + ); +}; diff --git a/src/components/visualizations/tokenization/data.ts b/src/components/visualizations/tokenization/data.ts new file mode 100644 index 0000000..3f1e243 --- /dev/null +++ b/src/components/visualizations/tokenization/data.ts @@ -0,0 +1,1107 @@ +import type { + TokenChip, + TokenChipTone, +} from "@/components/visualization-ui/TokenChips"; +import type { MetricBar } from "@/components/visualization-ui/MetricBars"; +import type { + SegmentGroup, + StatItem, + StatTone, + TokenizationExample, + TokenizationStep, +} from "./types"; + +/* ------------------------------------------------------------------ */ +/* Builders */ +/* ------------------------------------------------------------------ */ + +type ChipSpec = string | [label: string, value?: string, tone?: TokenChipTone]; + +const chips = (prefix: string, specs: ChipSpec[]): TokenChip[] => + specs.map((spec, index) => { + const label = typeof spec === "string" ? spec : spec[0]; + const value = typeof spec === "string" ? undefined : spec[1]; + const tone = typeof spec === "string" ? undefined : spec[2]; + return { + id: `${prefix}-${index}-${label}-${tone ?? "n"}`, + label, + value, + tone, + }; + }); + +const group = ( + id: string, + label: string, + specs: ChipSpec[], + showIndex = false, +): SegmentGroup => ({ id, label, chips: chips(id, specs), showIndex }); + +const stat = ( + id: string, + label: string, + value: string, + tone?: StatTone, +): StatItem => ({ id, label, value, tone }); + +/** Visible stand-in for the space byte inside a token. */ +const SP = "␣"; + +/* ------------------------------------------------------------------ */ +/* Example 1: BPE training */ +/* ------------------------------------------------------------------ */ + +const CORPUS: [word: string, freq: number][] = [ + ["low", 5], + ["lower", 2], + ["newest", 6], + ["widest", 3], +]; + +const BASE_SYMBOLS = ["l", "o", "w", "e", "r", "n", "s", "t", "i", "d"]; +const MERGED_SYMBOLS = ["es", "est", "lo", "low", "ne"]; + +interface WordMarks { + pair?: [string, string]; + merged?: string; +} + +const wordGroup = ( + word: string, + syms: string[], + marks: WordMarks = {}, +): SegmentGroup => { + const freq = CORPUS.find(([w]) => w === word)?.[1] ?? 1; + const tones: (TokenChipTone | undefined)[] = syms.map((sym) => + marks.merged && sym === marks.merged ? "match" : undefined, + ); + if (marks.pair) { + for (let i = 0; i < syms.length - 1; i += 1) { + if (syms[i] === marks.pair[0] && syms[i + 1] === marks.pair[1]) { + tones[i] = "active"; + tones[i + 1] = "active"; + } + } + } + return group( + `word-${word}`, + `${word} ×${freq}`, + syms.map((sym, i) => [sym, undefined, tones[i]]), + ); +}; + +/** Full corpus segmentation as a list of [word, symbols]. */ +type Splits = Record; + +const corpusGroups = (splits: Splits, marks: Record = {}) => + CORPUS.map(([word]) => wordGroup(word, splits[word], marks[word])); + +const SPLITS_0: Splits = { + low: ["l", "o", "w"], + lower: ["l", "o", "w", "e", "r"], + newest: ["n", "e", "w", "e", "s", "t"], + widest: ["w", "i", "d", "e", "s", "t"], +}; +const SPLITS_1: Splits = { + ...SPLITS_0, + newest: ["n", "e", "w", "es", "t"], + widest: ["w", "i", "d", "es", "t"], +}; +const SPLITS_2: Splits = { + ...SPLITS_1, + newest: ["n", "e", "w", "est"], + widest: ["w", "i", "d", "est"], +}; +const SPLITS_3: Splits = { + ...SPLITS_2, + low: ["lo", "w"], + lower: ["lo", "w", "e", "r"], +}; +const SPLITS_4: Splits = { + ...SPLITS_3, + low: ["low"], + lower: ["low", "e", "r"], +}; +const SPLITS_5: Splits = { + ...SPLITS_4, + newest: ["ne", "w", "est"], +}; + +/** Bars are scaled against the largest count seen in this corpus (9). */ +const pairBars = ( + counts: [pair: string, count: number][], + best?: string, +): MetricBar[] => + counts.map(([pair, count]) => ({ + id: `pair-${pair}`, + label: pair, + value: count / 9, + display: String(count), + tone: pair === best ? "pink" : "violet", + active: pair === best, + })); + +const PAIRS_0: [string, number][] = [ + ["e + s", 9], + ["s + t", 9], + ["w + e", 8], + ["l + o", 7], + ["o + w", 7], + ["n + e", 6], + ["e + w", 6], +]; +const PAIRS_1: [string, number][] = [ + ["es + t", 9], + ["w + e", 8], + ["l + o", 7], + ["o + w", 7], + ["n + e", 6], + ["e + w", 6], + ["w + i", 3], +]; +const PAIRS_2: [string, number][] = [ + ["l + o", 7], + ["o + w", 7], + ["n + e", 6], + ["e + w", 6], + ["w + est", 6], + ["w + i", 3], + ["w + e", 2], +]; +const PAIRS_3: [string, number][] = [ + ["lo + w", 7], + ["n + e", 6], + ["e + w", 6], + ["w + est", 6], + ["w + i", 3], + ["i + d", 3], + ["d + est", 3], +]; +const PAIRS_4: [string, number][] = [ + ["n + e", 6], + ["e + w", 6], + ["w + est", 6], + ["w + i", 3], + ["i + d", 3], + ["d + est", 3], + ["low + e", 2], +]; +const PAIRS_5: [string, number][] = [ + ["ne + w", 6], + ["w + est", 6], + ["w + i", 3], + ["i + d", 3], + ["d + est", 3], + ["low + e", 2], + ["e + r", 2], +]; + +const vocabChips = (mergeCount: number, latest?: string): TokenChip[] => + chips("vocab", [ + ...BASE_SYMBOLS.map((sym, i): ChipSpec => [sym, `#${i}`, "muted"]), + ...MERGED_SYMBOLS.slice(0, mergeCount).map( + (sym, i): ChipSpec => [ + sym, + `#${BASE_SYMBOLS.length + i}`, + sym === latest ? "match" : "violet", + ], + ), + ]); + +const bpeStats = (merges: number, corpusTokens: number): StatItem[] => [ + stat("merges", "merges", `${merges} / 5`, "amber"), + stat("vocab", "vocab size", String(BASE_SYMBOLS.length + merges), "green"), + stat("corpus", "corpus tokens", String(corpusTokens), "cyan"), +]; + +const bpeStep = ( + partial: Omit & { bytes?: TokenChip[] }, +): TokenizationStep => ({ bytes: [], ...partial }); + +const BPE_STEPS: TokenizationStep[] = [ + bpeStep({ + simpleHtml: + "We start with the smallest pieces possible: single letters. There are 10 different letters in our four words, so the starting dictionary has 10 entries.", + descriptionHtml: + 'Training starts from characters. Every distinct character in the corpus becomes a base symbol, so the initial vocabulary has 10 entries and any text made of these letters can already be encoded.', + activeLine: 1, + doneLines: [], + segments: corpusGroups(SPLITS_0), + rules: vocabChips(0), + bars: [], + stats: bpeStats(0, 79), + }), + bpeStep({ + simpleHtml: + "Each word is chopped into its letters. Some words appear more often than others (newest shows up 6 times), and written out letter by letter the whole list takes 79 pieces.", + descriptionHtml: + 'Each word is split into a list of single-character symbols. The corpus carries word frequencies (low 5, lower 2, newest 6, widest 3), so it currently costs 79 symbols to write out.', + activeLine: 2, + doneLines: [1], + segments: corpusGroups(SPLITS_0), + rules: vocabChips(0), + bars: [], + stats: bpeStats(0, 79), + }), + bpeStep({ + simpleHtml: + "The program counts how often every pair of neighbouring letters shows up. e followed by s appears in both newest and widest, 9 times in total, which puts it at the top.", + descriptionHtml: + 'countPairs tallies every adjacent symbol pair, weighted by word frequency. e + s appears in newest (6) and widest (3), so it scores 9. Only the top pairs are shown.', + activeLine: 5, + doneLines: [1, 2, 3, 4], + segments: corpusGroups(SPLITS_0), + rules: vocabChips(0), + bars: pairBars(PAIRS_0), + stats: bpeStats(0, 79), + }), + bpeStep({ + simpleHtml: + "Pick the most common pair. Two pairs are tied at 9, so the one we saw first wins: e and s.", + descriptionHtml: + 'argmax picks the most frequent pair. e + s and s + t tie at 9, and the tie goes to the pair encountered first, so e + s wins.', + activeLine: 6, + doneLines: [1, 2, 3, 4, 5], + segments: corpusGroups(SPLITS_0, { + newest: { pair: ["e", "s"] }, + widest: { pair: ["e", "s"] }, + }), + rules: vocabChips(0), + bars: pairBars(PAIRS_0, "e + s"), + stats: bpeStats(0, 79), + }), + bpeStep({ + simpleHtml: + "The pair is glued into a new piece, es, and added to the dictionary as entry number 10. We also write down the merge itself, because we will replay these merges later in the same order.", + descriptionHtml: + 'The merge is recorded and the new symbol es joins the vocabulary as id 10. Merges are stored in order because encoding later replays them in exactly this sequence.', + activeLine: 9, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8], + segments: corpusGroups(SPLITS_0, { + newest: { pair: ["e", "s"] }, + widest: { pair: ["e", "s"] }, + }), + rules: vocabChips(1, "es"), + bars: pairBars(PAIRS_0, "e + s"), + stats: bpeStats(1, 79), + }), + bpeStep({ + simpleHtml: + "Every word is rewritten so that e s becomes one es piece. The whole list is now 70 pieces instead of 79.", + descriptionHtml: + 'applyMerge rewrites every word: each adjacent e, s becomes one es symbol. The corpus shrinks from 79 to 70 symbols (9 occurrences removed).', + activeLine: 11, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + segments: corpusGroups(SPLITS_1, { + newest: { merged: "es" }, + widest: { merged: "es" }, + }), + rules: vocabChips(1, "es"), + bars: pairBars(PAIRS_0, "e + s"), + stats: bpeStats(1, 70), + }), + bpeStep({ + simpleHtml: + "Count pairs again. The picture changed: s t is gone (the s is now inside es), and es t took its place with the same score of 9.", + descriptionHtml: + 'Second pass. Pairs are recounted over the new segmentation: s + t no longer exists because s was absorbed, and es + t takes its place with the same count of 9.', + activeLine: 5, + doneLines: [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12], + segments: corpusGroups(SPLITS_1), + rules: vocabChips(1), + bars: pairBars(PAIRS_1), + stats: bpeStats(1, 70), + }), + bpeStep({ + simpleHtml: + "Glue es and t into est. Pieces grow by stacking earlier merges, which is why common word endings like est end up as a single piece.", + descriptionHtml: + 'es + t is merged into est (id 11). Subwords grow by stacking earlier merges, which is why frequent suffixes like est end up as single tokens.', + activeLine: 11, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + segments: corpusGroups(SPLITS_2, { + newest: { merged: "est" }, + widest: { merged: "est" }, + }), + rules: vocabChips(2, "est"), + bars: pairBars(PAIRS_1, "es + t"), + stats: bpeStats(2, 61), + }), + bpeStep({ + simpleHtml: + "Third count. w e dropped from 8 to 2 because inside newest the e is now locked inside est. Each merge changes what the next count can see.", + descriptionHtml: + 'Third recount. Note that w + e dropped from 8 to 2: inside newest the e now lives in est, so the pair only survives in lower. Earlier merges change what later merges can see.', + activeLine: 5, + doneLines: [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12], + segments: corpusGroups(SPLITS_2), + rules: vocabChips(2), + bars: pairBars(PAIRS_2), + stats: bpeStats(2, 61), + }), + bpeStep({ + simpleHtml: + "l o and o w are tied at 7. l o came first, so it becomes lo, and both low and lower are rewritten.", + descriptionHtml: + 'l + o and o + w tie at 7. l + o was seen first, so it becomes lo (id 12) and both low and lower are rewritten.', + activeLine: 11, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + segments: corpusGroups(SPLITS_3, { + low: { merged: "lo" }, + lower: { merged: "lo" }, + }), + rules: vocabChips(3, "lo"), + bars: pairBars(PAIRS_2, "l + o"), + stats: bpeStats(3, 54), + }), + bpeStep({ + simpleHtml: + "Fourth merge: lo plus w becomes low. The word low is now one piece, and lower is low e r.", + descriptionHtml: + 'Fourth merge: lo + w becomes low (id 13). The word low is now a single token, and lower is low, e, r.', + activeLine: 11, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + segments: corpusGroups(SPLITS_4, { + low: { merged: "low" }, + lower: { merged: "low" }, + }), + rules: vocabChips(4, "low"), + bars: pairBars(PAIRS_3, "lo + w"), + stats: bpeStats(4, 47), + }), + bpeStep({ + simpleHtml: + "Fifth merge: three pairs tie at 6 and n e wins because it was seen first. The list now costs 41 pieces instead of the 79 we started with.", + descriptionHtml: + 'Fifth merge: three pairs tie at 6 and n + e wins on first occurrence, producing ne (id 14). The corpus now costs 41 symbols instead of 79.', + activeLine: 11, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + segments: corpusGroups(SPLITS_5, { newest: { merged: "ne" } }), + rules: vocabChips(5, "ne"), + bars: pairBars(PAIRS_4, "n + e"), + stats: bpeStats(5, 41), + }), + bpeStep({ + simpleHtml: + "Five merges were the limit, so training stops. The dictionary has 15 entries and 5 merge rules. A word we never saw, like lowest, can still be written as low plus est, two known pieces.", + descriptionHtml: + 'NUM_MERGES is reached and the loop exits with 15 vocabulary entries and 5 ordered merge rules. An unseen word like lowest would encode as low + est, two known tokens, without any special casing.', + activeLine: 13, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + segments: corpusGroups(SPLITS_5), + rules: vocabChips(5), + bars: pairBars(PAIRS_5), + stats: bpeStats(5, 41), + }), +]; + +/* ------------------------------------------------------------------ */ +/* Example 2: encoding a sentence */ +/* ------------------------------------------------------------------ */ + +// Token ids and merge ranks below are illustrative. They follow the shape of +// a GPT-2 style byte-level BPE vocabulary but are not copied from one. + +const SENTENCE = "The tokenizer splits unusual words"; + +const PRE_TOKENS = [ + "The", + `${SP}tokenizer`, + `${SP}splits`, + `${SP}unusual`, + `${SP}words`, +]; + +const OUTPUT_IDS: [string, string][] = [ + ["The", "464"], + [`${SP}token`, "11241"], + ["izer", "7509"], + [`${SP}splits`, "30778"], + [`${SP}unusual`, "8468"], + [`${SP}words`, "2456"], +]; + +const preTokenGroup = (activeIndex: number, doneCount: number): SegmentGroup => + group( + "pre", + "pre-tokens", + PRE_TOKENS.map( + (piece, i): ChipSpec => [ + piece, + undefined, + i === activeIndex ? "active" : i < doneCount ? "match" : undefined, + ], + ), + ); + +const pieceGroup = (syms: ChipSpec[]): SegmentGroup => + group("piece", "current piece", syms); + +const outputGroup = (count: number): SegmentGroup => + group( + "out", + "output ids", + OUTPUT_IDS.slice(0, count).map( + ([label, id]): ChipSpec => [label, id, "amber"], + ), + true, + ); + +const byteChips = (text: string): TokenChip[] => + chips( + "byte", + [...new TextEncoder().encode(text)].map( + (byte, i): ChipSpec => [ + byte.toString(16).toUpperCase().padStart(2, "0"), + text[i] === " " ? SP : text[i], + byte === 0x20 ? "amber" : undefined, + ], + ), + ); + +const rule = (a: string, b: string, rank: number): ChipSpec => [ + `${a} + ${b}`, + `${a}${b} · rank ${rank}`, + "violet", +]; + +const TOKENIZER_RULES: ChipSpec[] = [ + rule(SP, "t", 0), + rule("e", "r", 13), + rule("e", "n", 26), + rule("o", "k", 456), + rule(`${SP}t`, "ok", 1204), + rule("i", "z", 4033), + rule("iz", "er", 7100), + rule(`${SP}tok`, "en", 9870), +]; + +const SPLITS_RULES: ChipSpec[] = [ + rule(SP, "s", 1), + rule("i", "t", 30), + rule("p", "l", 301), + rule("it", "s", 890), + rule(`${SP}s`, "pl", 2210), + rule(`${SP}spl`, "its", 20114), +]; + +const WORDS_RULES: ChipSpec[] = [ + rule(SP, "w", 8), + rule("o", "r", 42), + rule("d", "s", 611), + rule(`${SP}w`, "or", 1503), + rule(`${SP}wor`, "ds", 4890), +]; + +const encodeStats = (tokens: number, pieces?: number): StatItem[] => [ + stat("chars", "characters", "34", "slate"), + stat("words", "words", "5", "slate"), + stat("pieces", "pre-tokens", pieces == null ? "0" : String(pieces), "cyan"), + stat("tokens", "tokens", String(tokens), "amber"), +]; + +const ENCODE_STEPS: TokenizationStep[] = [ + { + simpleHtml: + "The model cannot read letters. It only understands numbers, so this function turns a sentence of 34 characters into as few numbers as the dictionary allows.", + descriptionHtml: + "encode receives the raw string. The model never sees characters directly; it sees a list of integer ids, so everything below is about turning 34 characters into as few ids as the vocabulary allows.", + activeLine: 1, + doneLines: [], + segments: [group("text", "input", [SENTENCE])], + rules: [], + bars: [], + bytes: [], + stats: encodeStats(0), + }, + { + simpleHtml: + "First the text is split into chunks. The space in front of a word stays attached to it, which is why most dictionary entries begin with a space.", + descriptionHtml: + 'preTokenize splits on a regex that keeps each leading space attached to the word after it. That is why most tokens in a vocabulary start with a space: the space is part of the token, not a separator between tokens.', + activeLine: 3, + doneLines: [1, 2], + segments: [preTokenGroup(-1, 0)], + rules: [], + bars: [], + bytes: [], + stats: encodeStats(0, 5), + }, + { + simpleHtml: + "The first chunk, The, is split into single characters. Under the hood each character is really one byte, and for plain English one byte is one letter.", + descriptionHtml: + "First piece, The, is split into single symbols. In a byte-level tokenizer each symbol is really a UTF-8 byte; for ASCII text one byte equals one character, so the byte view lines up with the letters.", + activeLine: 4, + doneLines: [1, 2, 3], + segments: [preTokenGroup(0, 0), pieceGroup(["T", "h", "e"])], + rules: [], + bars: [], + bytes: byteChips("The"), + stats: encodeStats(0, 5), + }, + { + simpleHtml: + "Now the merge rules replay. The most common pair goes first: T and h join, then Th and e, and the chunk collapses into one piece.", + descriptionHtml: + 'The loop applies the lowest-ranked merge present in the piece, then repeats. T + h (rank 60) fires first, then Th + e (rank 145), and the piece collapses to one symbol.', + activeLine: 8, + doneLines: [1, 2, 3, 4, 5, 6, 7], + segments: [preTokenGroup(0, 0), pieceGroup([["The", undefined, "match"]])], + rules: chips("rules", [rule("T", "h", 60), rule("Th", "e", 145)]), + bars: [], + bytes: byteChips("The"), + stats: encodeStats(0, 5), + }, + { + simpleHtml: + "Only one piece is left, so we look it up: The is number 464. One word, one number.", + descriptionHtml: + 'syms.length is 1, so the loop ends and the vocabulary lookup maps The to id 464. One word, one token.', + activeLine: 10, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9], + segments: [ + preTokenGroup(0, 0), + pieceGroup([["The", undefined, "match"]]), + outputGroup(1), + ], + rules: chips("rules", [rule("T", "h", 60), rule("Th", "e", 145)]), + bars: [], + bytes: byteChips("The"), + stats: encodeStats(1, 5), + }, + { + simpleHtml: + "Next chunk: a space followed by tokenizer, 10 characters in total. The space is just another character with its own merge rules.", + descriptionHtml: + 'Second piece, ␣tokenizer, starts as 10 symbols. The first one is the space byte 0x20, an ordinary symbol with its own merge rules.', + activeLine: 4, + doneLines: [1, 2, 3], + segments: [ + preTokenGroup(1, 1), + pieceGroup([SP, "t", "o", "k", "e", "n", "i", "z", "e", "r"]), + outputGroup(1), + ], + rules: [], + bars: [], + bytes: byteChips(" tokenizer"), + stats: encodeStats(1, 5), + }, + { + simpleHtml: + "The program scans every neighbouring pair and picks the one the dictionary likes best. Space plus t is the most common pair of all, so it goes first.", + descriptionHtml: + 'lowestRankPair scans every adjacent pair and returns the one with the smallest rank. ␣ + t is rank 0 (the most frequent pair in the training data), then e + r and e + n follow.', + activeLine: 6, + doneLines: [1, 2, 3, 4, 5], + segments: [ + preTokenGroup(1, 1), + pieceGroup([ + [`${SP}t`, undefined, "match"], + "o", + "k", + ["en", undefined, "match"], + "i", + "z", + ["er", undefined, "match"], + ]), + outputGroup(1), + ], + rules: chips("rules", TOKENIZER_RULES.slice(0, 3)), + bars: [], + bytes: byteChips(" tokenizer"), + stats: encodeStats(1, 5), + }, + { + simpleHtml: + "Merges keep stacking: o k, then ␣t with ok. The order comes from how common each pair was in training, not from left to right.", + descriptionHtml: + "Merges keep stacking: o + k (rank 456), then ␣t + ok (rank 1204). Rank order, not left-to-right order, decides what merges next, so the piece is rewritten in the same way it was during training.", + activeLine: 8, + doneLines: [1, 2, 3, 4, 5, 6, 7], + segments: [ + preTokenGroup(1, 1), + pieceGroup([[`${SP}tok`, undefined, "match"], "en", "i", "z", "er"]), + outputGroup(1), + ], + rules: chips("rules", TOKENIZER_RULES.slice(0, 5)), + bars: [], + bytes: byteChips(" tokenizer"), + stats: encodeStats(1, 5), + }, + { + simpleHtml: + "i z and then iz er form the ending izer. The same ending appears in optimizer and organizer, so the model learns it once and reuses it.", + descriptionHtml: + 'i + z (rank 4033) and iz + er (rank 7100) build the suffix izer. The same suffix token shows up in optimizer and organizer, which is what lets the model generalise across word families.', + activeLine: 8, + doneLines: [1, 2, 3, 4, 5, 6, 7], + segments: [ + preTokenGroup(1, 1), + pieceGroup([`${SP}tok`, "en", ["izer", undefined, "match"]]), + outputGroup(1), + ], + rules: chips("rules", TOKENIZER_RULES.slice(0, 7)), + bars: [], + bytes: byteChips(" tokenizer"), + stats: encodeStats(1, 5), + }, + { + simpleHtml: + "␣tok and en join into ␣token. Two pieces remain: ␣token and izer.", + descriptionHtml: + '␣tok + en (rank 9870) yields ␣token. Two symbols remain: ␣token and izer.', + activeLine: 8, + doneLines: [1, 2, 3, 4, 5, 6, 7], + segments: [ + preTokenGroup(1, 1), + pieceGroup([[`${SP}token`, undefined, "match"], "izer"]), + outputGroup(1), + ], + rules: chips("rules", TOKENIZER_RULES), + bars: [], + bytes: byteChips(" tokenizer"), + stats: encodeStats(1, 5), + }, + { + simpleHtml: + "There is no rule for joining ␣token and izer, so merging stops. The word was too rare in training to earn its own entry, so it costs 2 numbers instead of 1.", + descriptionHtml: + 'No merge rule exists for ␣token + izer, so lowestRankPair returns nothing and the loop breaks. The word was too rare in training to earn its own token, so it costs 2 tokens.', + activeLine: 7, + doneLines: [1, 2, 3, 4, 5, 6, 8], + segments: [ + preTokenGroup(1, 1), + pieceGroup([ + [`${SP}token`, undefined, "active"], + ["izer", undefined, "active"], + ]), + outputGroup(1), + ], + rules: chips("rules", TOKENIZER_RULES), + bars: [], + bytes: byteChips(" tokenizer"), + stats: encodeStats(1, 5), + }, + { + simpleHtml: + "Both pieces are looked up: 11241 and 7509. The model will see them as two separate positions in the sentence.", + descriptionHtml: + 'Both symbols are looked up and pushed: 11241 for ␣token and 7509 for izer. The model will receive these as two separate positions in its input sequence.', + activeLine: 10, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9], + segments: [ + preTokenGroup(1, 1), + pieceGroup([ + [`${SP}token`, undefined, "match"], + ["izer", undefined, "match"], + ]), + outputGroup(3), + ], + rules: chips("rules", TOKENIZER_RULES), + bars: [], + bytes: byteChips(" tokenizer"), + stats: encodeStats(3, 5), + }, + { + simpleHtml: + "The chunk splits goes through 6 merges and becomes a single piece, number 30778. How many merges a word needs varies; what matters is what is left when no rule applies.", + descriptionHtml: + '␣splits goes through 6 merges and ends as a single symbol with id 30778. The merge count per word varies, but the output is always whatever symbols survive when no rule applies.', + activeLine: 8, + doneLines: [1, 2, 3, 4, 5, 6, 7, 9, 10], + segments: [ + preTokenGroup(2, 2), + pieceGroup([[`${SP}splits`, undefined, "match"]]), + outputGroup(4), + ], + rules: chips("rules", SPLITS_RULES), + bars: [], + bytes: byteChips(" splits"), + stats: encodeStats(4, 5), + }, + { + simpleHtml: + "unusual takes 7 merges and words takes 5, and each ends as one piece. Common English words almost always fit in one number because their letter pairs were seen often enough.", + descriptionHtml: + "␣unusual (7 merges) and ␣words (5 merges) each become one token. Common English words almost always fit in a single token because their pairs were frequent enough to be merged all the way up.", + activeLine: 8, + doneLines: [1, 2, 3, 4, 5, 6, 7, 9, 10], + segments: [ + preTokenGroup(4, 4), + pieceGroup([[`${SP}words`, undefined, "match"]]), + outputGroup(6), + ], + rules: chips("rules", WORDS_RULES), + bars: [], + bytes: byteChips(" words"), + stats: encodeStats(6, 5), + }, + { + simpleHtml: + "Result: 6 numbers for 5 words and 34 characters. The model counts numbers, not words, so unusual words and other languages cost more space and more money.", + descriptionHtml: + 'encode returns 6 ids for 5 words and 34 characters, about 5.7 characters per token. Token count, not word count, is what fills the context window and what API pricing meters, so unusual words and non-English text cost more.', + activeLine: 12, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + segments: [preTokenGroup(-1, 5), outputGroup(6)], + rules: [], + bars: [], + bytes: [], + stats: [ + ...encodeStats(6, 5), + stat("ratio", "chars / token", "5.7", "green"), + ], + }, +]; + +/* ------------------------------------------------------------------ */ +/* Example 3: why counts differ */ +/* ------------------------------------------------------------------ */ + +const INPUTS = [ + "hello", + "Hello", + `${SP}hello`, + "12345", + `${SP}${SP}${SP}${SP}return`, + "ხმა", +]; + +const inputsGroup = (activeIndex: number, doneCount: number): SegmentGroup => + group( + "inputs", + "inputs", + INPUTS.map( + (text, i): ChipSpec => [ + text, + undefined, + i === activeIndex ? "active" : i < doneCount ? "match" : undefined, + ], + ), + ); + +const EDGE_OUTPUTS: [string, string][] = [ + ["hello", "31373"], + ["Hello", "15496"], + [`${SP}hello`, "23748"], + ["123", "10163"], + ["45", "2231"], + [`${SP}${SP}${SP}`, "27908"], + [`${SP}return`, "1441"], +]; + +const GEORGIAN_BYTES = ["E1", "83", "AE", "E1", "83", "9B", "E1", "83", "90"]; + +const edgeOutputGroup = (count: number, withBytes = false): SegmentGroup => + group( + "edge-out", + "output ids", + [ + ...EDGE_OUTPUTS.slice(0, count).map( + ([label, id]): ChipSpec => [label, id, "amber"], + ), + ...(withBytes + ? GEORGIAN_BYTES.map( + (hex): ChipSpec => [`0x${hex}`, String(parseInt(hex, 16)), "miss"], + ) + : []), + ], + true, + ); + +/** Rules listed in the order they fire, which is ascending rank. */ +const HELLO_RULES: ChipSpec[] = [ + rule("h", "e", 7), + rule("l", "l", 48), + rule("he", "ll", 2110), + rule("hell", "o", 18300), +]; + +const CAP_HELLO_RULES: ChipSpec[] = [ + rule("l", "l", 48), + rule("H", "e", 191), + rule("He", "ll", 3540), + rule("Hell", "o", 12520), +]; + +const INDENT_RULES: ChipSpec[] = [rule(SP, SP, 2), rule(`${SP}${SP}`, SP, 51)]; + +const RETURN_RULES: ChipSpec[] = [ + rule(SP, "r", 6), + rule("u", "r", 60), + rule(`${SP}r`, "e", 930), + rule("ur", "n", 1210), + rule("t", "urn", 2640), + rule(`${SP}re`, "turn", 4410), +]; + +const georgianBytes = (): TokenChip[] => + chips( + "gbyte", + GEORGIAN_BYTES.map( + (hex, i): ChipSpec => [hex, ["ხ", "მ", "ა"][Math.floor(i / 3)], "miss"], + ), + ); + +const edgeStats = (processed: number, tokens: number): StatItem[] => [ + stat("inputs", "inputs", `${processed} / 6`, "slate"), + stat("chars", "characters", "34", "slate"), + stat("tokens", "tokens", String(tokens), "amber"), +]; + +const EDGE_STEPS: TokenizationStep[] = [ + { + simpleHtml: + "Six short inputs, 34 characters in total, go through the same function. The question is how many numbers each one costs, and the answer depends on bytes and merge rules, not on what a person would call a word.", + descriptionHtml: + "Six short inputs, 34 characters in total, run through the same encodePiece. The question is how many tokens each one costs, and the answer depends on bytes and merge rules rather than on what a human would call a word.", + activeLine: 2, + doneLines: [1], + segments: [inputsGroup(-1, 0)], + rules: [], + bars: [], + bytes: [], + stats: edgeStats(0, 0), + }, + { + simpleHtml: + "hello becomes 5 bytes. Every byte has a placeholder symbol, so the program only ever works with a fixed alphabet of 256 symbols and nothing is ever unknown.", + descriptionHtml: + 'TextEncoder turns hello into 5 UTF-8 bytes. Each byte maps to a printable placeholder symbol, so the merge loop only ever deals with a fixed alphabet of 256 base symbols and nothing is ever out of vocabulary.', + activeLine: 4, + doneLines: [1, 2, 3], + segments: [inputsGroup(0, 0)], + rules: [], + bars: [], + bytes: byteChips("hello"), + stats: edgeStats(0, 0), + }, + { + simpleHtml: + "Four merges turn hello into one piece, number 31373. A five-letter word in one piece is normal for common lowercase English.", + descriptionHtml: + 'Four merges reduce hello to one symbol and the lookup returns 31373. A five-letter word in one token is the normal case for common lowercase English.', + activeLine: 10, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9], + segments: [inputsGroup(0, 0), edgeOutputGroup(1)], + rules: chips("rules", HELLO_RULES), + bars: [], + bytes: byteChips("hello"), + stats: edgeStats(1, 1), + }, + { + simpleHtml: + "Hello with a capital H differs from hello by exactly one byte. The tokenizer does not know about capital letters; to it this is just a different sequence of bytes.", + descriptionHtml: + 'Hello differs from hello in exactly one byte: 0x48 instead of 0x68. BPE has no notion of case, so from here on this is simply a different byte sequence.', + activeLine: 4, + doneLines: [1, 2, 3], + segments: [inputsGroup(1, 1), edgeOutputGroup(1)], + rules: [], + bars: [], + bytes: byteChips("Hello"), + stats: edgeStats(1, 1), + }, + { + simpleHtml: + "The capitalised word also becomes one piece, but a different one: number 15496. The model stores separate knowledge for each and only learns they are related by seeing them used the same way.", + descriptionHtml: + 'The capitalised form still merges to one symbol, but it is a different symbol with id 15496. The model has a separate embedding row for it, and it learns that the two are related only from context during training.', + activeLine: 10, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9], + segments: [inputsGroup(1, 1), edgeOutputGroup(2)], + rules: chips("rules", CAP_HELLO_RULES), + bars: [], + bytes: byteChips("Hello"), + stats: edgeStats(2, 2), + }, + { + simpleHtml: + "A space followed by hello is a third piece, number 23748. In normal text almost every word follows a space, so the space version is the common one.", + descriptionHtml: + '␣hello carries the space byte and encodes to a third id, 23748. In running text almost every word follows a space, so the space-prefixed token is the common one; the bare hello mostly appears at the start of a line.', + activeLine: 10, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9], + segments: [inputsGroup(2, 2), edgeOutputGroup(3)], + rules: chips("rules", [ + rule(SP, "h", 5), + rule("e", "l", 78), + rule(`${SP}h`, "el", 1490), + rule(`${SP}hel`, "lo", 6080), + ]), + bars: [], + bytes: byteChips(" hello"), + stats: edgeStats(3, 3), + }, + { + simpleHtml: + "12345 is 5 digit bytes. Digits merge by frequency like letters do, and the dictionary happens to have 123 and 45 but not the full number.", + descriptionHtml: + "12345 is 5 digit bytes. Digits merge by frequency like everything else, and the merge table happens to contain 123 and 45 but not the full number.", + activeLine: 6, + doneLines: [1, 2, 3, 4, 5], + segments: [inputsGroup(3, 3), edgeOutputGroup(3)], + rules: chips("rules", [ + rule("1", "2", 340), + rule("12", "3", 2870), + rule("4", "5", 3115), + ]), + bars: [], + bytes: byteChips("12345"), + stats: edgeStats(3, 3), + }, + { + simpleHtml: + "The number becomes two pieces, 123 and 45, split by habit rather than by place value. This is one reason language models find arithmetic awkward; newer tokenizers limit digit runs to 3.", + descriptionHtml: + 'The number becomes 2 tokens, 123 and 45, split by merge statistics rather than by place value. This is one reason arithmetic is awkward for language models; newer tokenizers cap digit runs at 3 to make the chunking predictable.', + activeLine: 10, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9], + segments: [inputsGroup(3, 3), edgeOutputGroup(5)], + rules: chips("rules", [ + rule("1", "2", 340), + rule("12", "3", 2870), + rule("4", "5", 3115), + ]), + bars: [], + bytes: byteChips("12345"), + stats: edgeStats(4, 5), + }, + { + simpleHtml: + "Indented code: four spaces plus return become ␣␣␣ and ␣return, because the last space sticks to the word. Indentation gets its own pieces, so deeply indented code costs extra.", + descriptionHtml: + "Indented code: the pre-tokenizer splits four spaces plus return into ␣␣␣ and ␣return, because the last space attaches to the word. Whitespace runs get their own tokens, so deeply indented code burns tokens on indentation alone.", + activeLine: 10, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9], + segments: [inputsGroup(4, 4), edgeOutputGroup(7)], + rules: chips("rules", [...INDENT_RULES, ...RETURN_RULES]), + bars: [], + bytes: byteChips(" return"), + stats: edgeStats(5, 7), + }, + { + simpleHtml: + "ხმა is a 3-letter Georgian word, but it takes 9 bytes because each of these letters needs 3 bytes. The byte view is three times longer than the text.", + descriptionHtml: + 'ხმა (Georgian, 3 letters) is 9 bytes: every letter in the U+10D0 block needs 3 bytes in UTF-8. The byte view is now three times longer than the text.', + activeLine: 4, + doneLines: [1, 2, 3], + segments: [inputsGroup(5, 5), edgeOutputGroup(7)], + rules: [], + bars: [], + bytes: georgianBytes(), + stats: edgeStats(5, 7), + }, + { + simpleHtml: + "This dictionary never learned merges for these bytes, so nothing can be joined and each byte becomes its own piece. This fallback means any text can be encoded, just expensively.", + descriptionHtml: + 'This vocabulary learned no merges for these byte pairs, so lowestRankPair finds nothing on the first pass and the loop breaks immediately. This is the byte fallback: each byte is emitted as its own token from the reserved 0 to 255 range.', + activeLine: 7, + doneLines: [1, 2, 3, 4, 5, 6], + segments: [inputsGroup(5, 5), edgeOutputGroup(7, true)], + rules: chips("rules", [ + ["no rule for E1 + 83", "fallback", "miss"], + ["no rule for 83 + AE", "fallback", "miss"], + ]), + bars: [], + bytes: georgianBytes(), + stats: edgeStats(5, 7), + }, + { + simpleHtml: + "Final count: 34 characters became 16 pieces. The Georgian word alone costs 9, three per letter, while hello costs 1. Cost depends on how much of a language the training data contained, not on how long the text looks.", + descriptionHtml: + 'Final tally: 34 characters became 16 tokens. The Georgian word alone costs 9 of them, three per letter, while hello costs 1. Token cost tracks how well the training data covered a script, not how long the text looks.', + activeLine: 10, + doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9], + segments: [inputsGroup(-1, 6), edgeOutputGroup(7, true)], + rules: [], + bars: [], + bytes: [], + stats: [ + ...edgeStats(6, 16), + stat("cheap", "hello", "1 token", "green"), + stat("expensive", "ხმა", "9 tokens", "rose"), + ], + }, +]; + +/* ------------------------------------------------------------------ */ +/* Examples */ +/* ------------------------------------------------------------------ */ + +export const EXAMPLES: TokenizationExample[] = [ + { + id: "bpe", + title: "BPE merges", + description: + "Train byte pair encoding on a tiny corpus: count adjacent pairs, merge the most frequent one, repeat.", + kind: "bpe", + codeLines: [ + { num: 1, text: "const vocab = new Set(words.flatMap((w) => [...w]));" }, + { + num: 2, + text: "const splits = new Map(words.map((w) => [w, [...w]]));", + }, + { num: 3, text: "const merges = [];" }, + { num: 4, text: "while (merges.length < NUM_MERGES) {" }, + { num: 5, text: " const pairs = countPairs(splits, freq);" }, + { num: 6, text: " const best = argmax(pairs);" }, + { num: 7, text: " if (!best) break;" }, + { num: 8, text: " merges.push(best);" }, + { num: 9, text: " vocab.add(best[0] + best[1]);" }, + { num: 10, text: " for (const [word, syms] of splits) {" }, + { num: 11, text: " splits.set(word, applyMerge(syms, best));" }, + { num: 12, text: " }" }, + { num: 13, text: "}" }, + ], + steps: BPE_STEPS, + }, + { + id: "encode", + title: "Encoding a sentence", + description: + "Apply learned merges to a sentence, piece by piece, and map the surviving symbols to token ids.", + kind: "encode", + codeLines: [ + { num: 1, text: "function encode(text) {" }, + { num: 2, text: " const ids = [];" }, + { num: 3, text: " for (const piece of preTokenize(text)) {" }, + { num: 4, text: " let syms = [...piece];" }, + { num: 5, text: " while (syms.length > 1) {" }, + { num: 6, text: " const pair = lowestRankPair(syms, ranks);" }, + { num: 7, text: " if (!pair) break;" }, + { num: 8, text: " syms = applyMerge(syms, pair);" }, + { num: 9, text: " }" }, + { num: 10, text: " ids.push(...syms.map((s) => vocab.get(s)));" }, + { num: 11, text: " }" }, + { num: 12, text: " return ids;" }, + { num: 13, text: "}" }, + ], + steps: ENCODE_STEPS, + }, + { + id: "edge-cases", + title: "Why counts differ", + description: + "Case, leading spaces, digits, indentation, and non-Latin scripts all change how many tokens a string costs.", + kind: "edge-cases", + codeLines: [ + { num: 1, text: "const encoder = new TextEncoder();" }, + { num: 2, text: "function encodePiece(piece) {" }, + { num: 3, text: " const bytes = [...encoder.encode(piece)];" }, + { num: 4, text: " let syms = bytes.map((b) => byteToUnicode[b]);" }, + { num: 5, text: " while (syms.length > 1) {" }, + { num: 6, text: " const pair = lowestRankPair(syms, ranks);" }, + { num: 7, text: " if (!pair) break;" }, + { num: 8, text: " syms = applyMerge(syms, pair);" }, + { num: 9, text: " }" }, + { num: 10, text: " return syms.map((s) => vocab.get(s));" }, + { num: 11, text: "}" }, + ], + steps: EDGE_STEPS, + }, +]; diff --git a/src/components/visualizations/tokenization/helpers.ts b/src/components/visualizations/tokenization/helpers.ts new file mode 100644 index 0000000..aab52ef --- /dev/null +++ b/src/components/visualizations/tokenization/helpers.ts @@ -0,0 +1,56 @@ +import { + createKindBadgeClass, + createKindLabel, +} from "@/lib/visualization-helpers"; +import type { StatTone, TokenizationKind } from "./types"; + +export const kindBadgeClass = createKindBadgeClass({ + bpe: "bg-amber-500/15 text-amber-400 border-amber-500/25", + encode: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25", + "edge-cases": "bg-rose-500/15 text-rose-400 border-rose-500/25", +}); + +export const kindLabel = createKindLabel({ + bpe: "bpe training", + encode: "encoding", + "edge-cases": "edge cases", +}); + +export interface PanelTitles { + segments: string; + rules: string; + aux: string; + stats: string; +} + +const PANEL_TITLES: Record = { + bpe: { + segments: "Current Segmentation", + rules: "Vocabulary", + aux: "Pair Frequencies", + stats: "Stats", + }, + encode: { + segments: "Tokens", + rules: "Merge Rules Applied", + aux: "Byte View", + stats: "Stats", + }, + "edge-cases": { + segments: "Tokens", + rules: "Vocabulary Lookup", + aux: "Byte View", + stats: "Stats", + }, +}; + +export const panelTitles = (kind: TokenizationKind): PanelTitles => + PANEL_TITLES[kind]; + +export const STAT_TONE_STYLES: Record = { + cyan: "border-cyan-400/30 text-cyan-200", + green: "border-emerald-400/30 text-emerald-200", + amber: "border-amber-400/30 text-amber-200", + rose: "border-rose-400/30 text-rose-200", + slate: "border-slate-600/50 text-slate-200", +}; diff --git a/src/components/visualizations/tokenization/index.tsx b/src/components/visualizations/tokenization/index.tsx new file mode 100644 index 0000000..30b8c68 --- /dev/null +++ b/src/components/visualizations/tokenization/index.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { + ExamplePicker, + KindBadge, +} from "@/components/visualization-ui/ExamplePicker"; +import { SourceCodePanel } from "@/components/visualization-ui/SourceCodePanel"; +import { + SourceGrid, + VisualizationSection, +} from "@/components/visualization-ui/VisualizationLayout"; +import { VisualizationToolbar } from "@/components/visualization-ui/VisualizationToolbar"; +import { useExampleTopic } from "@/hooks/useExampleTopic"; +import { useChangeFlash } from "@/hooks/useChangeFlash"; +import { EXAMPLES } from "./data"; +import { kindBadgeClass, kindLabel } from "./helpers"; +import { StatePanels } from "./components/StatePanels"; + +export const Tokenization = () => { + const { + example, + activeExampleId, + handleExampleChange, + playback, + currentStep, + hasSimpleText, + } = useExampleTopic(EXAMPLES); + const { currentStepIndex } = playback; + + const flashes = useChangeFlash( + { + description: currentStep?.descriptionHtml, + segments: currentStep?.segments, + rules: currentStep?.rules, + aux: example.kind === "bpe" ? currentStep?.bars : currentStep?.bytes, + stats: currentStep?.stats, + }, + currentStepIndex, + ); + + return ( + <> + ( + + {kindLabel(ex.kind)} + + )} + /> + } + /> + + + + + + + + + + ); +}; diff --git a/src/components/visualizations/tokenization/types.ts b/src/components/visualizations/tokenization/types.ts new file mode 100644 index 0000000..1db2afe --- /dev/null +++ b/src/components/visualizations/tokenization/types.ts @@ -0,0 +1,41 @@ +import type { SourceLine, BaseStep } from "@/types/visualization"; +import type { ExampleOption } from "@/components/visualization-ui/ExampleSelector"; +import type { TokenChip } from "@/components/visualization-ui/TokenChips"; +import type { MetricBar } from "@/components/visualization-ui/MetricBars"; + +export type TokenizationKind = "bpe" | "encode" | "edge-cases"; + +export type StatTone = "cyan" | "green" | "amber" | "rose" | "slate"; + +/** A labelled row of chips, for example one corpus word or the output ids. */ +export interface SegmentGroup { + id: string; + label: string; + chips: TokenChip[]; + showIndex?: boolean; +} + +export interface StatItem { + id: string; + label: string; + value: string; + tone?: StatTone; +} + +export interface TokenizationStep extends BaseStep { + /** Tokens or per-word segmentation. */ + segments: SegmentGroup[]; + /** Vocabulary entries or merge rules applied so far. */ + rules: TokenChip[]; + /** Pair frequencies, used by the BPE training example only. */ + bars: MetricBar[]; + /** UTF-8 bytes of the piece being encoded, empty during training. */ + bytes: TokenChip[]; + stats: StatItem[]; +} + +export interface TokenizationExample extends ExampleOption { + kind: TokenizationKind; + codeLines: SourceLine[]; + steps: TokenizationStep[]; +} diff --git a/src/components/visualizations/type-coercion/data.ts b/src/components/visualizations/type-coercion/data.ts index 73013bc..1eb4154 100644 --- a/src/components/visualizations/type-coercion/data.ts +++ b/src/components/visualizations/type-coercion/data.ts @@ -24,7 +24,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [], consoleOutput: ["true"], coercionOps: [ - { label: '"5" (string) converts to 5 (number)', result: "Coerced", color: "amber" }, + { + label: '"5" (string) converts to 5 (number)', + result: "Coerced", + color: "amber", + }, { label: "5 == 5", result: "true", color: "emerald" }, ], }, @@ -35,7 +39,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1], consoleOutput: ["true", "false"], coercionOps: [ - { label: "Types: string vs number", result: "Different types", color: "red" }, + { + label: "Types: string vs number", + result: "Different types", + color: "red", + }, { label: "No coercion performed", result: "false", color: "red" }, ], }, @@ -46,7 +54,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2], consoleOutput: ["true", "false", "true"], coercionOps: [ - { label: "false (boolean) converts to 0 (number)", result: "Coerced", color: "amber" }, + { + label: "false (boolean) converts to 0 (number)", + result: "Coerced", + color: "amber", + }, { label: "0 == 0", result: "true", color: "emerald" }, ], }, @@ -57,7 +69,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3], consoleOutput: ["true", "false", "true", "false"], coercionOps: [ - { label: "Types: number vs boolean", result: "Different types", color: "red" }, + { + label: "Types: number vs boolean", + result: "Different types", + color: "red", + }, { label: "No coercion performed", result: "false", color: "red" }, ], }, @@ -68,8 +84,16 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3, 4], consoleOutput: ["true", "false", "true", "false", "true"], coercionOps: [ - { label: '"" (string) converts to 0 (number)', result: "Coerced", color: "amber" }, - { label: "false (boolean) converts to 0 (number)", result: "Coerced", color: "amber" }, + { + label: '"" (string) converts to 0 (number)', + result: "Coerced", + color: "amber", + }, + { + label: "false (boolean) converts to 0 (number)", + result: "Coerced", + color: "amber", + }, { label: "0 == 0", result: "true", color: "emerald" }, ], }, @@ -80,7 +104,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3, 4, 5], consoleOutput: ["true", "false", "true", "false", "true", "false"], coercionOps: [ - { label: "Types: string vs boolean", result: "Different types", color: "red" }, + { + label: "Types: string vs boolean", + result: "Different types", + color: "red", + }, { label: "No coercion performed", result: "false", color: "red" }, ], }, @@ -112,7 +140,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [], consoleOutput: [], coercionOps: [ - { label: "0 (number) is falsy", result: "Boolean(0) = false", color: "red" }, + { + label: "0 (number) is falsy", + result: "Boolean(0) = false", + color: "red", + }, { label: "if block skipped", result: "not executed", color: "red" }, ], }, @@ -123,7 +155,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1], consoleOutput: [], coercionOps: [ - { label: '"" (empty string) is falsy', result: 'Boolean("") = false', color: "red" }, + { + label: '"" (empty string) is falsy', + result: 'Boolean("") = false', + color: "red", + }, { label: "if block skipped", result: "not executed", color: "red" }, ], }, @@ -134,7 +170,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2], consoleOutput: [], coercionOps: [ - { label: "null is falsy", result: "Boolean(null) = false", color: "red" }, + { + label: "null is falsy", + result: "Boolean(null) = false", + color: "red", + }, { label: "if block skipped", result: "not executed", color: "red" }, ], }, @@ -145,7 +185,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3], consoleOutput: [], coercionOps: [ - { label: "undefined is falsy", result: "Boolean(undefined) = false", color: "red" }, + { + label: "undefined is falsy", + result: "Boolean(undefined) = false", + color: "red", + }, { label: "if block skipped", result: "not executed", color: "red" }, ], }, @@ -156,7 +200,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3, 4], consoleOutput: [], coercionOps: [ - { label: "NaN is falsy", result: "Boolean(NaN) = false", color: "red" }, + { + label: "NaN is falsy", + result: "Boolean(NaN) = false", + color: "red", + }, { label: "if block skipped", result: "not executed", color: "red" }, ], }, @@ -167,7 +215,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3, 4, 5], consoleOutput: ['"0" is truthy'], coercionOps: [ - { label: '"0" (non-empty string) is truthy', result: 'Boolean("0") = true', color: "emerald" }, + { + label: '"0" (non-empty string) is truthy', + result: 'Boolean("0") = true', + color: "emerald", + }, { label: "if block executes", result: "logged", color: "emerald" }, ], }, @@ -178,7 +230,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3, 4, 5, 6], consoleOutput: ['"0" is truthy', "[] is truthy"], coercionOps: [ - { label: "[] (empty array) is truthy", result: "Boolean([]) = true", color: "emerald" }, + { + label: "[] (empty array) is truthy", + result: "Boolean([]) = true", + color: "emerald", + }, { label: "if block executes", result: "logged", color: "emerald" }, ], }, @@ -189,7 +245,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3, 4, 5, 6, 7], consoleOutput: ['"0" is truthy', "[] is truthy", "{} is truthy"], coercionOps: [ - { label: "{} (empty object) is truthy", result: "Boolean({}) = true", color: "emerald" }, + { + label: "{} (empty object) is truthy", + result: "Boolean({}) = true", + color: "emerald", + }, { label: "if block executes", result: "logged", color: "emerald" }, ], }, @@ -219,17 +279,25 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [], consoleOutput: ["true"], coercionOps: [ - { label: "Special rule: null == undefined", result: "Always true", color: "emerald" }, + { + label: "Special rule: null == undefined", + result: "Always true", + color: "emerald", + }, ], }, { descriptionHtml: - "null === undefined is false. Strict equality checks types first: null is type object, undefined is type undefined.", + "null === undefined is false. Strict equality checks types first, and null and undefined are two different types, so the comparison stops there.", activeLine: 2, doneLines: [1], consoleOutput: ["true", "false"], coercionOps: [ - { label: "Types: object vs undefined", result: "Different types", color: "red" }, + { + label: "Types: object vs undefined", + result: "Different types", + color: "red", + }, { label: "No coercion performed", result: "false", color: "red" }, ], }, @@ -240,7 +308,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2], consoleOutput: ["true", "false", "false"], coercionOps: [ - { label: "null is not coerced to a number", result: "No conversion", color: "cyan" }, + { + label: "null is not coerced to a number", + result: "No conversion", + color: "cyan", + }, { label: "null == 0", result: "false", color: "red" }, ], }, @@ -251,7 +323,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3], consoleOutput: ["true", "false", "false", "false"], coercionOps: [ - { label: "null is not coerced to a string", result: "No conversion", color: "cyan" }, + { + label: "null is not coerced to a string", + result: "No conversion", + color: "cyan", + }, { label: 'null == ""', result: "false", color: "red" }, ], }, @@ -263,7 +339,11 @@ export const EXAMPLES: CoercionExample[] = [ consoleOutput: ["true", "false", "false", "false", "object"], coercionOps: [ { label: "typeof null", result: '"object"', color: "amber" }, - { label: "Historical bug since JS 1.0", result: "Never fixed", color: "amber" }, + { + label: "Historical bug since JS 1.0", + result: "Never fixed", + color: "amber", + }, ], }, { @@ -271,10 +351,25 @@ export const EXAMPLES: CoercionExample[] = [ 'typeof undefined returns "undefined". Key takeaway: use == null to check for both null and undefined, or use === to distinguish them.', activeLine: 6, doneLines: [1, 2, 3, 4, 5], - consoleOutput: ["true", "false", "false", "false", "object", "undefined"], + consoleOutput: [ + "true", + "false", + "false", + "false", + "object", + "undefined", + ], coercionOps: [ - { label: "typeof undefined", result: '"undefined"', color: "emerald" }, - { label: "Matches its own type name", result: "Consistent", color: "emerald" }, + { + label: "typeof undefined", + result: '"undefined"', + color: "emerald", + }, + { + label: "Matches its own type name", + result: "Consistent", + color: "emerald", + }, ], }, ], @@ -304,7 +399,11 @@ export const EXAMPLES: CoercionExample[] = [ consoleOutput: ["false"], coercionOps: [ { label: "NaN === NaN", result: "false", color: "red" }, - { label: "IEEE 754: NaN is never equal to anything", result: "Including itself", color: "amber" }, + { + label: "IEEE 754: NaN is never equal to anything", + result: "Including itself", + color: "amber", + }, ], }, { @@ -315,7 +414,11 @@ export const EXAMPLES: CoercionExample[] = [ consoleOutput: ["false", "false"], coercionOps: [ { label: "NaN == NaN", result: "false", color: "red" }, - { label: "Loose equality cannot help", result: "Still not equal", color: "red" }, + { + label: "Loose equality cannot help", + result: "Still not equal", + color: "red", + }, ], }, { @@ -326,7 +429,11 @@ export const EXAMPLES: CoercionExample[] = [ consoleOutput: ["false", "false", "number"], coercionOps: [ { label: "typeof NaN", result: '"number"', color: "amber" }, - { label: '"Not-a-Number" is a number', result: "Ironic but true", color: "amber" }, + { + label: '"Not-a-Number" is a number', + result: "Ironic but true", + color: "amber", + }, ], }, { @@ -336,7 +443,11 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3], consoleOutput: ["false", "false", "number", "true"], coercionOps: [ - { label: 'Number("hello") produces NaN', result: "Coerced first", color: "amber" }, + { + label: 'Number("hello") produces NaN', + result: "Coerced first", + color: "amber", + }, { label: "isNaN(NaN)", result: "true", color: "emerald" }, ], }, @@ -347,8 +458,16 @@ export const EXAMPLES: CoercionExample[] = [ doneLines: [1, 2, 3, 4], consoleOutput: ["false", "false", "number", "true", "false"], coercionOps: [ - { label: "No coercion performed", result: "Strict check", color: "cyan" }, - { label: '"hello" is a string, not NaN', result: "false", color: "red" }, + { + label: "No coercion performed", + result: "Strict check", + color: "cyan", + }, + { + label: '"hello" is a string, not NaN', + result: "false", + color: "red", + }, ], }, { @@ -359,7 +478,11 @@ export const EXAMPLES: CoercionExample[] = [ consoleOutput: ["false", "false", "number", "true", "false", "true"], coercionOps: [ { label: "Value is exactly NaN", result: "true", color: "emerald" }, - { label: "Number.isNaN() is the safe check", result: "Recommended", color: "emerald" }, + { + label: "Number.isNaN() is the safe check", + result: "Recommended", + color: "emerald", + }, ], }, ], diff --git a/src/components/visualizations/type-coercion/helpers.ts b/src/components/visualizations/type-coercion/helpers.ts index e7a8400..455230d 100644 --- a/src/components/visualizations/type-coercion/helpers.ts +++ b/src/components/visualizations/type-coercion/helpers.ts @@ -1,4 +1,7 @@ -import { createKindBadgeClass, createKindLabel } from "@/lib/visualization-helpers"; +import { + createKindBadgeClass, + createKindLabel, +} from "@/lib/visualization-helpers"; import type { CoercionKind } from "./types"; export const kindBadgeClass = createKindBadgeClass({ diff --git a/src/components/visualizations/type-coercion/index.tsx b/src/components/visualizations/type-coercion/index.tsx index a96375d..1956170 100644 --- a/src/components/visualizations/type-coercion/index.tsx +++ b/src/components/visualizations/type-coercion/index.tsx @@ -1,54 +1,32 @@ "use client"; -import { useState } from "react"; -import { Badge } from "@/components/ui/badge"; import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; -import { - CodeBlock, - type CodeBlockLine, -} from "@/components/visualization-ui/CodeBlock"; import { ConsoleOutput } from "@/components/visualization-ui/ConsoleOutput"; -import { TransportControls } from "@/components/visualization-ui/TransportControls"; import { - ExampleSelector, -} from "@/components/visualization-ui/ExampleSelector"; -import { ToolbarPortal } from "@/components/layout/ToolbarPortal"; -import { cn } from "@/lib/utils"; + ExamplePicker, + KindBadge, +} from "@/components/visualization-ui/ExamplePicker"; +import { SourceCodePanel } from "@/components/visualization-ui/SourceCodePanel"; import { - VISUALIZATION_PANEL_TITLES, - VISUALIZATION_EMPTY_STATES, -} from "@/lib/visualization/uiCopy"; -import { useStepPlayback } from "@/hooks/useStepPlayback"; + SourceGrid, + VisualizationSection, +} from "@/components/visualization-ui/VisualizationLayout"; +import { VisualizationToolbar } from "@/components/visualization-ui/VisualizationToolbar"; +import { useExampleTopic } from "@/hooks/useExampleTopic"; +import { cn } from "@/lib/utils"; import { useChangeFlash } from "@/hooks/useChangeFlash"; import { EXAMPLES } from "./data"; import { kindBadgeClass, kindLabel, OP_COLOR_MAP } from "./helpers"; -export function TypeCoercion() { - const [activeExampleId, setActiveExampleId] = useState(EXAMPLES[0].id); - - const example = - EXAMPLES.find((e) => e.id === activeExampleId) ?? EXAMPLES[0]; - +export const TypeCoercion = () => { const { - currentStepIndex, - isPlaying, - speedLevel, - speedLabel, - canStep, - canStepBack, - togglePlay, - step: handleStep, - stepBack: handleStepBack, - reset: handleReset, - setSpeedLevel, - } = useStepPlayback({ - totalSteps: example.steps.length, - initialStep: -1, - resetKey: activeExampleId, - }); - - const currentStep = - currentStepIndex >= 0 ? example.steps[currentStepIndex] : null; + example, + activeExampleId, + handleExampleChange, + playback, + currentStep, + } = useExampleTopic(EXAMPLES); + const { currentStepIndex } = playback; const flashes = useChangeFlash( { @@ -59,103 +37,34 @@ export function TypeCoercion() { currentStepIndex, ); - const handleExampleChange = (id: string) => { - setActiveExampleId(id); - }; - return ( <> - -
    -
    -
    - ( - - {kindLabel(ex.kind)} - - )} - /> - - {kindLabel(example.kind)} - -
    - - -
    - -
    ( + + {kindLabel(ex.kind)} + )} - aria-live="polite" - role="status" - > - {currentStep?.descriptionHtml ? ( -

    - ) : ( -

    - {VISUALIZATION_EMPTY_STATES.stepDescription} -

    - )} -
    -
    -
    + /> + } + /> -
    -
    - - { - const isActive = currentStep?.activeLine === line.num; - const isDone = - currentStep?.doneLines.includes(line.num) ?? false; - return { - key: line.num, - lineNumber: line.num, - text: line.text, - className: cn( - isActive && "is-active", - isDone && !isActive && "is-done" - ), - }; - } - )} - /> - + + +
    {op.label} @@ -184,12 +93,16 @@ export function TypeCoercion() { )} -
    +
    -
    -
    + + ); -} +}; diff --git a/src/components/visualizations/type-coercion/types.ts b/src/components/visualizations/type-coercion/types.ts index eeb3bac..6e9336a 100644 --- a/src/components/visualizations/type-coercion/types.ts +++ b/src/components/visualizations/type-coercion/types.ts @@ -1,5 +1,5 @@ import type { ExampleOption } from "@/components/visualization-ui/ExampleSelector"; -import type { SourceLine } from "@/types/visualization"; +import type { SourceLine, BaseStep } from "@/types/visualization"; export type CoercionKind = "equality" | "truthy" | "null" | "nan"; @@ -9,10 +9,7 @@ export interface CoercionOperation { color: "amber" | "emerald" | "red" | "cyan"; } -export interface CoercionStep { - descriptionHtml: string; - activeLine: number | null; - doneLines: number[]; +export interface CoercionStep extends BaseStep { consoleOutput: string[]; coercionOps: CoercionOperation[]; } diff --git a/src/components/visualizations/use-effect-lifecycle/data.ts b/src/components/visualizations/use-effect-lifecycle/data.ts index c581e1d..b7ed938 100644 --- a/src/components/visualizations/use-effect-lifecycle/data.ts +++ b/src/components/visualizations/use-effect-lifecycle/data.ts @@ -43,12 +43,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Count: 0"' }], }, activeNodeId: "timer", - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[]" }], + effects: [ + { id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[]" }, + ], currentPhase: "render", }, { descriptionHtml: - 'useEffect is registered with an empty dependency array []. React stores the effect but does not run it yet.', + "useEffect is registered with an empty dependency array []. React stores the effect but does not run it yet.", activeLine: 4, doneLines: [1, 2], consoleOutput: [], @@ -58,7 +60,9 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Count: 0"' }], }, activeNodeId: "timer", - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[]" }], + effects: [ + { id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[]" }, + ], currentPhase: "render", }, { @@ -73,7 +77,9 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Count: 0"' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[]" }], + effects: [ + { id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[]" }, + ], currentPhase: "paint", }, { @@ -88,12 +94,19 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Count: 0"' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "effect-run", deps: "[]" }], + effects: [ + { + id: "e1", + label: "useEffect([], ...)", + phase: "effect-run", + deps: "[]", + }, + ], currentPhase: "effect-run", }, { descriptionHtml: - 'The interval fires. setCount triggers a re-render. React calls Timer again, count is now 1. The deps array is [], so the effect is skipped (no cleanup, no re-run).', + "The interval fires. setCount triggers a re-render. React calls Timer again, count is now 1. The deps array is [], so the effect is skipped (no cleanup, no re-run).", activeLine: 7, doneLines: [1, 2, 4, 5, 6, 8, 14, 16], consoleOutput: ["effect: mounted"], @@ -101,10 +114,19 @@ export const EXAMPLES: UseEffectExample[] = [ id: "timer", label: "", highlight: "updated", - children: [{ id: "div", label: '
    "Count: 1"', highlight: "updated" }], + children: [ + { id: "div", label: '
    "Count: 1"', highlight: "updated" }, + ], }, activeNodeId: "timer", - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[] (unchanged)" }], + effects: [ + { + id: "e1", + label: "useEffect([], ...)", + phase: "idle", + deps: "[] (unchanged)", + }, + ], currentPhase: "render", }, { @@ -117,10 +139,19 @@ export const EXAMPLES: UseEffectExample[] = [ id: "timer", label: "", highlight: "removed", - children: [{ id: "div", label: '
    "Count: 1"', highlight: "removed" }], + children: [ + { id: "div", label: '
    "Count: 1"', highlight: "removed" }, + ], }, activeNodeId: "timer", - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "cleanup-run", deps: "[]" }], + effects: [ + { + id: "e1", + label: "useEffect([], ...)", + phase: "cleanup-run", + deps: "[]", + }, + ], currentPhase: "cleanup-run", }, { @@ -176,12 +207,19 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Loading..."' }], }, activeNodeId: "profile", - effects: [{ id: "e1", label: "useEffect([userId], ...)", phase: "idle", deps: '["1"]' }], + effects: [ + { + id: "e1", + label: "useEffect([userId], ...)", + phase: "idle", + deps: '["1"]', + }, + ], currentPhase: "render", }, { descriptionHtml: - 'useEffect is registered with [userId]. This is the first render, so the effect will run after paint.', + "useEffect is registered with [userId]. This is the first render, so the effect will run after paint.", activeLine: 4, doneLines: [1, 2], consoleOutput: [], @@ -192,7 +230,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Loading..."' }], }, activeNodeId: "profile", - effects: [{ id: "e1", label: "useEffect([userId], ...)", phase: "idle", deps: '["1"]' }], + effects: [ + { + id: "e1", + label: "useEffect([userId], ...)", + phase: "idle", + deps: '["1"]', + }, + ], currentPhase: "render", }, { @@ -208,7 +253,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Loading..."' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([userId], ...)", phase: "idle", deps: '["1"]' }], + effects: [ + { + id: "e1", + label: "useEffect([userId], ...)", + phase: "idle", + deps: '["1"]', + }, + ], currentPhase: "paint", }, { @@ -224,7 +276,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Loading..."' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([userId], ...)", phase: "effect-run", deps: '["1"]' }], + effects: [ + { + id: "e1", + label: "useEffect([userId], ...)", + phase: "effect-run", + deps: '["1"]', + }, + ], currentPhase: "effect-run", }, { @@ -238,10 +297,19 @@ export const EXAMPLES: UseEffectExample[] = [ label: "", highlight: "updated", props: [{ key: "userId", value: '"1"' }], - children: [{ id: "div", label: '
    "Alice"', highlight: "updated" }], + children: [ + { id: "div", label: '
    "Alice"', highlight: "updated" }, + ], }, activeNodeId: "profile", - effects: [{ id: "e1", label: "useEffect([userId], ...)", phase: "idle", deps: '["1"] (unchanged)' }], + effects: [ + { + id: "e1", + label: "useEffect([userId], ...)", + phase: "idle", + deps: '["1"] (unchanged)', + }, + ], currentPhase: "render", }, { @@ -258,7 +326,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Loading..."' }], }, activeNodeId: "profile", - effects: [{ id: "e1", label: "useEffect([userId], ...)", phase: "idle", deps: '["2"]' }], + effects: [ + { + id: "e1", + label: "useEffect([userId], ...)", + phase: "idle", + deps: '["2"]', + }, + ], currentPhase: "render", }, { @@ -274,12 +349,19 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Loading..."' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([userId], ...)", phase: "effect-run", deps: '["2"] (changed)' }], + effects: [ + { + id: "e1", + label: "useEffect([userId], ...)", + phase: "effect-run", + deps: '["2"] (changed)', + }, + ], currentPhase: "effect-run", }, { descriptionHtml: - 'React compares each dependency value with Object.is. When any value changes, the previous cleanup runs (if any), then the new effect runs. Primitives compare by value; objects compare by reference.', + "React compares each dependency value with Object.is. When any value changes, the previous cleanup runs (if any), then the new effect runs. Primitives compare by value; objects compare by reference.", activeLine: null, doneLines: [1, 2, 4, 5, 6, 7, 9], consoleOutput: ["fetching user 1", "fetching user 2"], @@ -290,7 +372,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Bob"' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([userId], ...)", phase: "idle", deps: '["2"]' }], + effects: [ + { + id: "e1", + label: "useEffect([userId], ...)", + phase: "idle", + deps: '["2"]', + }, + ], currentPhase: "idle", }, ], @@ -333,7 +422,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Room: general"' }], }, activeNodeId: "chat", - effects: [{ id: "e1", label: "useEffect([roomId])", phase: "idle", deps: '["general"]' }], + effects: [ + { + id: "e1", + label: "useEffect([roomId])", + phase: "idle", + deps: '["general"]', + }, + ], currentPhase: "render", }, { @@ -349,7 +445,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Room: general"' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([roomId])", phase: "effect-run", deps: '["general"]' }], + effects: [ + { + id: "e1", + label: "useEffect([roomId])", + phase: "effect-run", + deps: '["general"]', + }, + ], currentPhase: "effect-run", }, { @@ -366,7 +469,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Room: random"' }], }, activeNodeId: "chat", - effects: [{ id: "e1", label: "useEffect([roomId])", phase: "idle", deps: '["random"]' }], + effects: [ + { + id: "e1", + label: "useEffect([roomId])", + phase: "idle", + deps: '["random"]', + }, + ], currentPhase: "render", }, { @@ -379,10 +489,20 @@ export const EXAMPLES: UseEffectExample[] = [ id: "chat", label: "", props: [{ key: "roomId", value: '"random"' }], - children: [{ id: "div", label: '
    "Room: random"', highlight: "updated" }], + children: [ + { id: "div", label: '
    "Room: random"', highlight: "updated" }, + ], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([roomId])", phase: "idle", deps: '["random"]', cleanupPending: true }], + effects: [ + { + id: "e1", + label: "useEffect([roomId])", + phase: "idle", + deps: '["random"]', + cleanupPending: true, + }, + ], currentPhase: "paint", }, { @@ -398,7 +518,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Room: random"' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([roomId])", phase: "cleanup-run", deps: '["general"] (prev)' }], + effects: [ + { + id: "e1", + label: "useEffect([roomId])", + phase: "cleanup-run", + deps: '["general"] (prev)', + }, + ], currentPhase: "cleanup-run", }, { @@ -406,7 +533,11 @@ export const EXAMPLES: UseEffectExample[] = [ 'Then the new effect runs: logs "connect to random" and opens a fresh connection to the new room.', activeLine: 3, doneLines: [1, 2, 6, 7, 8, 10, 12], - consoleOutput: ["connect to general", "disconnect from general", "connect to random"], + consoleOutput: [ + "connect to general", + "disconnect from general", + "connect to random", + ], componentTree: { id: "chat", label: "", @@ -414,7 +545,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Room: random"' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([roomId])", phase: "effect-run", deps: '["random"]' }], + effects: [ + { + id: "e1", + label: "useEffect([roomId])", + phase: "effect-run", + deps: '["random"]', + }, + ], currentPhase: "effect-run", }, { @@ -422,7 +560,12 @@ export const EXAMPLES: UseEffectExample[] = [ 'On unmount, cleanup runs one last time: "disconnect from random". Cleanup always runs before the next effect and on unmount. Each cleanup closes over the values from the render it was created in.', activeLine: 7, doneLines: [1, 2, 3, 4, 6, 8, 10, 12], - consoleOutput: ["connect to general", "disconnect from general", "connect to random", "disconnect from random"], + consoleOutput: [ + "connect to general", + "disconnect from general", + "connect to random", + "disconnect from random", + ], componentTree: { id: "chat", label: "", @@ -431,7 +574,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [], }, activeNodeId: "chat", - effects: [{ id: "e1", label: "useEffect([roomId])", phase: "cleanup-run", deps: '["random"]' }], + effects: [ + { + id: "e1", + label: "useEffect([roomId])", + phase: "cleanup-run", + deps: '["random"]', + }, + ], currentPhase: "cleanup-run", }, ], @@ -456,7 +606,10 @@ export const EXAMPLES: UseEffectExample[] = [ { num: 9, text: ' window.addEventListener("resize", handleResize);' }, { num: 10, text: "" }, { num: 11, text: " return () => {" }, - { num: 12, text: ' window.removeEventListener("resize", handleResize);' }, + { + num: 12, + text: ' window.removeEventListener("resize", handleResize);', + }, { num: 13, text: " };" }, { num: 14, text: " }, []);" }, { num: 15, text: "" }, @@ -477,7 +630,9 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Width: 1280px"' }], }, activeNodeId: "ws", - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[]" }], + effects: [ + { id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[]" }, + ], currentPhase: "render", }, { @@ -492,7 +647,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [{ id: "div", label: '
    "Width: 1280px"' }], }, activeNodeId: undefined, - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "effect-run", deps: "[]" }], + effects: [ + { + id: "e1", + label: "useEffect([], ...)", + phase: "effect-run", + deps: "[]", + }, + ], currentPhase: "effect-run", }, { @@ -505,15 +667,24 @@ export const EXAMPLES: UseEffectExample[] = [ id: "ws", label: "", highlight: "updated", - children: [{ id: "div", label: '
    "Width: 960px"', highlight: "updated" }], + children: [ + { id: "div", label: '
    "Width: 960px"', highlight: "updated" }, + ], }, activeNodeId: "ws", - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "idle", deps: "[] (unchanged)" }], + effects: [ + { + id: "e1", + label: "useEffect([], ...)", + phase: "idle", + deps: "[] (unchanged)", + }, + ], currentPhase: "render", }, { descriptionHtml: - 'The component unmounts. Cleanup runs: removeEventListener detaches the handler. No dangling listeners remain.', + "The component unmounts. Cleanup runs: removeEventListener detaches the handler. No dangling listeners remain.", activeLine: 12, doneLines: [1, 2, 3, 4, 6, 7, 8, 9, 11, 14, 16], consoleOutput: [], @@ -524,7 +695,14 @@ export const EXAMPLES: UseEffectExample[] = [ children: [], }, activeNodeId: "ws", - effects: [{ id: "e1", label: "useEffect([], ...)", phase: "cleanup-run", deps: "[]" }], + effects: [ + { + id: "e1", + label: "useEffect([], ...)", + phase: "cleanup-run", + deps: "[]", + }, + ], currentPhase: "cleanup-run", }, { diff --git a/src/components/visualizations/use-effect-lifecycle/helpers.ts b/src/components/visualizations/use-effect-lifecycle/helpers.ts index 1900a83..41c7715 100644 --- a/src/components/visualizations/use-effect-lifecycle/helpers.ts +++ b/src/components/visualizations/use-effect-lifecycle/helpers.ts @@ -1,4 +1,7 @@ -import { createKindBadgeClass, createKindLabel } from "@/lib/visualization-helpers"; +import { + createKindBadgeClass, + createKindLabel, +} from "@/lib/visualization-helpers"; import type { EffectPhase, UseEffectKind } from "./types"; /* ── Helpers ── */ diff --git a/src/components/visualizations/use-effect-lifecycle/index.tsx b/src/components/visualizations/use-effect-lifecycle/index.tsx index b3b234d..668a56b 100644 --- a/src/components/visualizations/use-effect-lifecycle/index.tsx +++ b/src/components/visualizations/use-effect-lifecycle/index.tsx @@ -1,23 +1,20 @@ "use client"; -import { useState } from "react"; -import { Badge } from "@/components/ui/badge"; import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; -import { - CodeBlock, - type CodeBlockLine, -} from "@/components/visualization-ui/CodeBlock"; import { ConsoleOutput } from "@/components/visualization-ui/ConsoleOutput"; -import { TransportControls } from "@/components/visualization-ui/TransportControls"; -import { ExampleSelector } from "@/components/visualization-ui/ExampleSelector"; import { ComponentTreeDiagram } from "@/components/visualization-ui/ComponentTreeDiagram"; -import { ToolbarPortal } from "@/components/layout/ToolbarPortal"; -import { cn } from "@/lib/utils"; import { - VISUALIZATION_PANEL_TITLES, - VISUALIZATION_EMPTY_STATES, -} from "@/lib/visualization/uiCopy"; -import { useStepPlayback } from "@/hooks/useStepPlayback"; + ExamplePicker, + KindBadge, +} from "@/components/visualization-ui/ExamplePicker"; +import { SourceCodePanel } from "@/components/visualization-ui/SourceCodePanel"; +import { + SourceGrid, + VisualizationSection, + WaitingPlaceholder, +} from "@/components/visualization-ui/VisualizationLayout"; +import { VisualizationToolbar } from "@/components/visualization-ui/VisualizationToolbar"; +import { useExampleTopic } from "@/hooks/useExampleTopic"; import { useChangeFlash } from "@/hooks/useChangeFlash"; import { EXAMPLES } from "./data"; import { kindBadgeClass, kindLabel } from "./helpers"; @@ -26,35 +23,14 @@ import { EffectTimelinePanel } from "./components/EffectTimelinePanel"; /* ── Main Component ── */ export const UseEffectLifecycle = () => { - const [activeExampleId, setActiveExampleId] = useState(EXAMPLES[0].id); - - const handleExampleChange = (id: string) => { - setActiveExampleId(id); - }; - - const example = - EXAMPLES.find((e) => e.id === activeExampleId) ?? EXAMPLES[0]; - const { - currentStepIndex, - isPlaying, - speedLevel, - speedLabel, - canStep, - canStepBack, - togglePlay, - step: handleStep, - stepBack: handleStepBack, - reset: handleReset, - setSpeedLevel, - } = useStepPlayback({ - totalSteps: example.steps.length, - initialStep: -1, - resetKey: activeExampleId, - }); - - const currentStep = - currentStepIndex >= 0 ? example.steps[currentStepIndex] : null; + example, + activeExampleId, + handleExampleChange, + playback, + currentStep, + } = useExampleTopic(EXAMPLES); + const { currentStepIndex } = playback; const flashes = useChangeFlash( { @@ -68,106 +44,39 @@ export const UseEffectLifecycle = () => { return ( <> - -
    -
    -
    - ( - - {kindLabel(ex.kind)} - - )} - /> - - {kindLabel(example.kind)} - -
    - - -
    - -
    - {currentStep?.descriptionHtml ? ( -

    - ) : ( -

    - {VISUALIZATION_EMPTY_STATES.stepDescription} -

    + ( + + {kindLabel(ex.kind)} + )} -
    -
    -
    + /> + } + /> -
    -
    - - { - const isActive = currentStep?.activeLine === line.num; - const isDone = - currentStep?.doneLines.includes(line.num) ?? false; - return { - key: line.num, - lineNumber: line.num, - text: line.text, - className: cn( - isActive && "is-active", - isDone && !isActive && "is-done", - ), - }; - }, - )} - /> - + + +
    {currentStep ? ( { activeNodeId={currentStep.activeNodeId} /> ) : ( -

    - waiting -

    + )}
    @@ -201,8 +108,8 @@ export const UseEffectLifecycle = () => {
    -
    - + + ); }; diff --git a/src/components/visualizations/use-effect-lifecycle/types.ts b/src/components/visualizations/use-effect-lifecycle/types.ts index 9bcdb65..aca76eb 100644 --- a/src/components/visualizations/use-effect-lifecycle/types.ts +++ b/src/components/visualizations/use-effect-lifecycle/types.ts @@ -1,4 +1,4 @@ -import type { SourceLine, TreeNodeData } from "@/types/visualization"; +import type { SourceLine, TreeNodeData, BaseStep } from "@/types/visualization"; import type { ExampleOption } from "@/components/visualization-ui/ExampleSelector"; /* ── Types ── */ @@ -21,10 +21,7 @@ export interface EffectEntry { cleanupPending?: boolean; } -export interface UseEffectStep { - descriptionHtml: string; - activeLine: number | null; - doneLines: number[]; +export interface UseEffectStep extends BaseStep { consoleOutput: string[]; componentTree: TreeNodeData; activeNodeId?: string; diff --git a/src/components/visualizations/virtual-dom/components/CreateElementPanel.tsx b/src/components/visualizations/virtual-dom/components/CreateElementPanel.tsx index 36fd30c..39027e7 100644 --- a/src/components/visualizations/virtual-dom/components/CreateElementPanel.tsx +++ b/src/components/visualizations/virtual-dom/components/CreateElementPanel.tsx @@ -1,7 +1,11 @@ import { cn } from "@/lib/utils"; import type { CreateElementCall } from "../types"; -export function CreateElementPanel({ calls }: { calls: CreateElementCall[] }) { +export const CreateElementPanel = ({ + calls, +}: { + calls: CreateElementCall[]; +}) => { if (calls.length === 0) { return (

    @@ -27,4 +31,4 @@ export function CreateElementPanel({ calls }: { calls: CreateElementCall[] }) { ))}

    ); -} +}; diff --git a/src/components/visualizations/virtual-dom/components/DomOutputPanel.tsx b/src/components/visualizations/virtual-dom/components/DomOutputPanel.tsx index 6444612..13009df 100644 --- a/src/components/visualizations/virtual-dom/components/DomOutputPanel.tsx +++ b/src/components/visualizations/virtual-dom/components/DomOutputPanel.tsx @@ -1,6 +1,6 @@ import { cn } from "@/lib/utils"; -export function DomOutputPanel({ lines }: { lines: string[] }) { +export const DomOutputPanel = ({ lines }: { lines: string[] }) => { if (lines.length === 0) { return (

    @@ -16,9 +16,7 @@ export function DomOutputPanel({ lines }: { lines: string[] }) { return (

    {line.trim()} @@ -27,4 +25,4 @@ export function DomOutputPanel({ lines }: { lines: string[] }) { })}
    ); -} +}; diff --git a/src/components/visualizations/virtual-dom/data.ts b/src/components/visualizations/virtual-dom/data.ts index 61bab11..2f3a97a 100644 --- a/src/components/visualizations/virtual-dom/data.ts +++ b/src/components/visualizations/virtual-dom/data.ts @@ -171,12 +171,7 @@ export const EXAMPLES: VirtualDomExample[] = [ highlight: false, }, ], - domOutput: [ - "
    ", - "

    Hello

    ", - "

    World

    ", - "
    ", - ], + domOutput: ["
    ", "

    Hello

    ", "

    World

    ", "
    "], }, ], }, @@ -190,7 +185,7 @@ export const EXAMPLES: VirtualDomExample[] = [ kind: "component", codeLines: [ { num: 1, text: "function Header() {" }, - { num: 2, text: ' return

    My App

    ;' }, + { num: 2, text: " return

    My App

    ;" }, { num: 3, text: "}" }, { num: 4, text: "" }, { num: 5, text: "function Content() {" }, @@ -363,7 +358,7 @@ export const EXAMPLES: VirtualDomExample[] = [ }, { descriptionHtml: - "All components are resolved. The virtual DOM tree now contains only host elements (div, h1, p) that map directly to real DOM nodes.", + "All components are resolved. The virtual DOM tree now contains only host elements (main, h1, p) that map directly to real DOM nodes.", activeLine: null, doneLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], vdomTree: { diff --git a/src/components/visualizations/virtual-dom/index.tsx b/src/components/visualizations/virtual-dom/index.tsx index e6d0d73..53f32f4 100644 --- a/src/components/visualizations/virtual-dom/index.tsx +++ b/src/components/visualizations/virtual-dom/index.tsx @@ -1,58 +1,34 @@ "use client"; -import { useState } from "react"; -import { Badge } from "@/components/ui/badge"; import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; -import { - CodeBlock, - type CodeBlockLine, -} from "@/components/visualization-ui/CodeBlock"; -import { TransportControls } from "@/components/visualization-ui/TransportControls"; -import { ExampleSelector } from "@/components/visualization-ui/ExampleSelector"; import { ComponentTreeDiagram } from "@/components/visualization-ui/ComponentTreeDiagram"; -import { ToolbarPortal } from "@/components/layout/ToolbarPortal"; -import { cn } from "@/lib/utils"; import { - VISUALIZATION_PANEL_TITLES, - VISUALIZATION_EMPTY_STATES, -} from "@/lib/visualization/uiCopy"; -import { useStepPlayback } from "@/hooks/useStepPlayback"; + ExamplePicker, + KindBadge, +} from "@/components/visualization-ui/ExamplePicker"; +import { SourceCodePanel } from "@/components/visualization-ui/SourceCodePanel"; +import { + SourceGrid, + VisualizationSection, + WaitingPlaceholder, +} from "@/components/visualization-ui/VisualizationLayout"; +import { VisualizationToolbar } from "@/components/visualization-ui/VisualizationToolbar"; +import { useExampleTopic } from "@/hooks/useExampleTopic"; import { useChangeFlash } from "@/hooks/useChangeFlash"; import { EXAMPLES } from "./data"; import { kindBadgeClass, kindLabel } from "./helpers"; import { CreateElementPanel } from "./components/CreateElementPanel"; import { DomOutputPanel } from "./components/DomOutputPanel"; -export function VirtualDom() { - const [activeExampleId, setActiveExampleId] = useState(EXAMPLES[0].id); - - const handleExampleChange = (id: string) => { - setActiveExampleId(id); - }; - - const example = - EXAMPLES.find((e) => e.id === activeExampleId) ?? EXAMPLES[0]; - +export const VirtualDom = () => { const { - currentStepIndex, - isPlaying, - speedLevel, - speedLabel, - canStep, - canStepBack, - togglePlay, - step: handleStep, - stepBack: handleStepBack, - reset: handleReset, - setSpeedLevel, - } = useStepPlayback({ - totalSteps: example.steps.length, - initialStep: -1, - resetKey: activeExampleId, - }); - - const currentStep = - currentStepIndex >= 0 ? example.steps[currentStepIndex] : null; + example, + activeExampleId, + handleExampleChange, + playback, + currentStep, + } = useExampleTopic(EXAMPLES); + const { currentStepIndex } = playback; const flashes = useChangeFlash( { @@ -66,98 +42,33 @@ export function VirtualDom() { return ( <> - -
    -
    -
    - ( - - {kindLabel(ex.kind)} - - )} - /> - - {kindLabel(example.kind)} - -
    - - -
    - -
    ( + + {kindLabel(ex.kind)} + )} - > - {currentStep?.descriptionHtml ? ( -

    - ) : ( -

    - {VISUALIZATION_EMPTY_STATES.stepDescription} -

    - )} -
    -
    -
    + /> + } + /> -
    -
    + + {/* Source Code */} - - { - const isActive = currentStep?.activeLine === line.num; - const isDone = - currentStep?.doneLines.includes(line.num) ?? false; - return { - key: line.num, - lineNumber: line.num, - text: line.text, - className: cn( - isActive && "is-active", - isDone && !isActive && "is-done", - ), - }; - }, - )} - /> - +
    {/* VDOM Tree + createElement Calls */} @@ -166,9 +77,7 @@ export function VirtualDom() { title="VDOM Tree" tone="cyan" bodyClassName="min-h-[10rem]" - className={ - flashes.vdomTree ? "viz-change-flash" : undefined - } + className={flashes.vdomTree ? "viz-change-flash" : undefined} > {currentStep?.vdomTree ? ( ) : ( -

    - waiting -

    + )} @@ -201,15 +108,13 @@ export function VirtualDom() { title="DOM Output" tone="green" bodyClassName="min-h-[6rem]" - className={ - flashes.domOutput ? "viz-change-flash" : undefined - } + className={flashes.domOutput ? "viz-change-flash" : undefined} >
    -
    -
    + + ); -} +}; diff --git a/src/components/visualizations/virtual-dom/types.ts b/src/components/visualizations/virtual-dom/types.ts index 11a9a2c..3b39cbd 100644 --- a/src/components/visualizations/virtual-dom/types.ts +++ b/src/components/visualizations/virtual-dom/types.ts @@ -1,4 +1,4 @@ -import type { SourceLine, TreeNodeData } from "@/types/visualization"; +import type { SourceLine, TreeNodeData, BaseStep } from "@/types/visualization"; import type { ExampleOption } from "@/components/visualization-ui/ExampleSelector"; export type VirtualDomKind = "element" | "component" | "dynamic"; @@ -9,10 +9,7 @@ export interface CreateElementCall { highlight?: boolean; } -export interface VirtualDomStep { - descriptionHtml: string; - activeLine: number | null; - doneLines: number[]; +export interface VirtualDomStep extends BaseStep { vdomTree: TreeNodeData | null; activeNodeId?: string; createElementCalls: CreateElementCall[]; diff --git a/src/components/visualizations/vue-reactivity/components/BindingsPanel.tsx b/src/components/visualizations/vue-reactivity/components/BindingsPanel.tsx new file mode 100644 index 0000000..0a80e69 --- /dev/null +++ b/src/components/visualizations/vue-reactivity/components/BindingsPanel.tsx @@ -0,0 +1,42 @@ +import { cn } from "@/lib/utils"; +import { BINDING_LABELS, BINDING_STYLES } from "../helpers"; +import type { BindingEntry } from "../types"; + +export const BindingsPanel = ({ + bindings, +}: { + bindings: BindingEntry[] | null; +}) => { + if (!bindings || bindings.length === 0) { + return ( +

    + no bindings yet +

    + ); + } + + return ( +
      + {bindings.map((binding) => ( +
    • +
      + {binding.name} + = {binding.value} + + {BINDING_LABELS[binding.kind]} + +
      +

      + {binding.note} +

      +
    • + ))} +
    + ); +}; diff --git a/src/components/visualizations/vue-reactivity/components/ComputedPanel.tsx b/src/components/visualizations/vue-reactivity/components/ComputedPanel.tsx new file mode 100644 index 0000000..1bd361b --- /dev/null +++ b/src/components/visualizations/vue-reactivity/components/ComputedPanel.tsx @@ -0,0 +1,75 @@ +import { cn } from "@/lib/utils"; +import type { ComputedState } from "../types"; + +const Field = ({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) => ( +
    + + {label} + + {children} +
    +); + +export const ComputedPanel = ({ + computed, +}: { + computed: ComputedState | null; +}) => { + if (!computed) { + return ( +

    + no computed yet +

    + ); + } + + return ( +
    +
    + ComputedRefImpl{" "} + {computed.name} +
    +
    + + + {computed.dirty ? "true" : "false"} + + + + + {computed.cached} + + + + {computed.evaluations} + + + {computed.subscribers.length === 0 ? ( + none + ) : ( + + {computed.subscribers.join(", ")} + + )} + +
    +
    + ); +}; diff --git a/src/components/visualizations/vue-reactivity/components/DepMap.tsx b/src/components/visualizations/vue-reactivity/components/DepMap.tsx new file mode 100644 index 0000000..3a4b4ac --- /dev/null +++ b/src/components/visualizations/vue-reactivity/components/DepMap.tsx @@ -0,0 +1,70 @@ +import { cn } from "@/lib/utils"; +import { DEP_STATUS_STYLES } from "../helpers"; +import type { DepEntry } from "../types"; + +export const DepMap = ({ deps }: { deps: DepEntry[] }) => { + if (deps.length === 0) { + return ( +

    + targetMap is empty +

    + ); + } + + const targets = Array.from(new Set(deps.map((dep) => dep.target))); + + return ( +
    + {targets.map((target) => ( +
    +
    + + targetMap + + -> + toRaw({target}) + -> depsMap +
    +
      + {deps + .filter((dep) => dep.target === target) + .map((dep) => ( +
    • + "{dep.key}" + -> Dep + {dep.effects.length === 0 ? ( + {"{ }"} + ) : ( + dep.effects.map((effect) => ( + + {effect} + + )) + )} + {dep.status !== "idle" && ( + + {dep.status} + + )} +
    • + ))} +
    +
    + ))} +
    + ); +}; diff --git a/src/components/visualizations/vue-reactivity/components/SchedulerPanel.tsx b/src/components/visualizations/vue-reactivity/components/SchedulerPanel.tsx new file mode 100644 index 0000000..3d8925d --- /dev/null +++ b/src/components/visualizations/vue-reactivity/components/SchedulerPanel.tsx @@ -0,0 +1,86 @@ +import { cn } from "@/lib/utils"; +import type { SchedulerState } from "../types"; + +const Row = ({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) => ( +
    + + {label} + + {children} +
    +); + +export const SchedulerPanel = ({ + scheduler, +}: { + scheduler: SchedulerState | null; +}) => { + if (!scheduler) { + return ( +

    + waiting +

    + ); + } + + return ( +
    + + {scheduler.activeEffect ? ( + + {scheduler.activeEffect} + + ) : ( + undefined + )} + + + + {scheduler.queue.length === 0 ? ( + [ ] + ) : ( + scheduler.queue.map((job) => ( + + {job} + + )) + )} + + + + + {scheduler.flushPending ? "flushJobs queued" : "idle"} + + + + {scheduler.ranNow.length > 0 && ( + + {scheduler.ranNow.map((effect) => ( + + {effect} + + ))} + + )} +
    + ); +}; diff --git a/src/components/visualizations/vue-reactivity/components/StatePanels.tsx b/src/components/visualizations/vue-reactivity/components/StatePanels.tsx new file mode 100644 index 0000000..f719e9f --- /dev/null +++ b/src/components/visualizations/vue-reactivity/components/StatePanels.tsx @@ -0,0 +1,53 @@ +import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; +import { cn } from "@/lib/utils"; +import type { VueReactivityKind, VueReactivityStep } from "../types"; +import { ComputedPanel } from "./ComputedPanel"; +import { BindingsPanel } from "./BindingsPanel"; +import { SchedulerPanel } from "./SchedulerPanel"; + +interface StatePanelsProps { + kind: VueReactivityKind; + step: VueReactivityStep | null; + flashes: { computed?: boolean; bindings?: boolean; scheduler?: boolean }; +} + +export const StatePanels = ({ kind, step, flashes }: StatePanelsProps) => { + const showComputed = kind === "computed"; + const showBindings = kind === "pitfalls"; + + return ( +
    + {showComputed && ( + + + + )} + {showBindings && ( + + + + )} + + + +
    + ); +}; diff --git a/src/components/visualizations/vue-reactivity/components/TrapLog.tsx b/src/components/visualizations/vue-reactivity/components/TrapLog.tsx new file mode 100644 index 0000000..43086a5 --- /dev/null +++ b/src/components/visualizations/vue-reactivity/components/TrapLog.tsx @@ -0,0 +1,54 @@ +import { cn } from "@/lib/utils"; +import { TRAP_RESULT_LABELS, TRAP_RESULT_STYLES } from "../helpers"; +import type { TrapEntry } from "../types"; + +export const TrapLog = ({ traps }: { traps: TrapEntry[] }) => { + if (traps.length === 0) { + return ( +

    + no traps fired +

    + ); + } + + return ( +
      + {traps.map((trap, index) => { + const isLatest = index === traps.length - 1; + return ( +
    1. +
      + + {trap.op} + + + {trap.target}.{trap.key} + + + {trap.op === "set" ? "=" : "->"} {trap.value} + + + {TRAP_RESULT_LABELS[trap.result]} + +
      +

      + {trap.isRef && ( + + ref accessor + + )} + {trap.note} +

      +
    2. + ); + })} +
    + ); +}; diff --git a/src/components/visualizations/vue-reactivity/data.ts b/src/components/visualizations/vue-reactivity/data.ts new file mode 100644 index 0000000..5efa388 --- /dev/null +++ b/src/components/visualizations/vue-reactivity/data.ts @@ -0,0 +1,786 @@ +import type { + BindingEntry, + DepEntry, + SchedulerState, + TrapEntry, + VueReactivityExample, +} from "./types"; + +const IDLE: SchedulerState = { + activeEffect: null, + queue: [], + flushPending: false, + ranNow: [], +}; + +const sched = (patch: Partial): SchedulerState => ({ + ...IDLE, + ...patch, +}); + +const dep = ( + target: string, + key: string, + effects: string[], + status: DepEntry["status"] = "idle", +): DepEntry => ({ target, key, effects, status }); + +/* ------------------------------------------------------------------ */ +/* Example 1: track and trigger */ +/* ------------------------------------------------------------------ */ + +const t1GetCount: TrapEntry = { + id: "t1-get-count", + op: "get", + target: "state", + key: "count", + value: "0", + result: "track", + note: "activeEffect is effect#1, so track() adds it to the Dep for count.", +}; +const t1SetName: TrapEntry = { + id: "t1-set-name", + op: "set", + target: "state", + key: "name", + value: "'Bob'", + result: "miss", + note: "trigger() finds no Dep for name. The raw object is updated, nothing is scheduled.", +}; +const t1GetCount2: TrapEntry = { + id: "t1-get-count-2", + op: "get", + target: "state", + key: "count", + value: "0", + result: "noop", + note: "count++ reads first. activeEffect is undefined, so track() returns early.", +}; +const t1SetCount1: TrapEntry = { + id: "t1-set-count-1", + op: "set", + target: "state", + key: "count", + value: "1", + result: "trigger", + note: "hasChanged(0, 1) is true. trigger() schedules every effect in the Dep for count.", +}; +const t1GetCount3: TrapEntry = { + id: "t1-get-count-3", + op: "get", + target: "state", + key: "count", + value: "1", + result: "noop", + note: "Second count++ reads outside any effect. Nothing tracked.", +}; +const t1SetCount2: TrapEntry = { + id: "t1-set-count-2", + op: "set", + target: "state", + key: "count", + value: "2", + result: "trigger", + note: "trigger() runs again, but queueJob() skips effect#1 because it is already queued.", +}; +const t1GetCountFlush: TrapEntry = { + id: "t1-get-count-flush", + op: "get", + target: "state", + key: "count", + value: "2", + result: "track", + note: "Re-run inside flushJobs. activeEffect is effect#1 again, so the Dep is refreshed.", +}; + +const trackExample: VueReactivityExample = { + id: "track-trigger", + title: "Track and Trigger", + description: + "A Proxy get trap records which effect read a property, and the set trap re-runs only those effects.", + kind: "track", + codeLines: [ + { num: 1, text: "import { reactive, watchEffect, nextTick } from 'vue'" }, + { num: 2, text: "" }, + { num: 3, text: "const state = reactive({ count: 0, name: 'Ada' })" }, + { num: 4, text: "" }, + { num: 5, text: "watchEffect(() => {" }, + { num: 6, text: " console.log('double', state.count * 2)" }, + { num: 7, text: "})" }, + { num: 8, text: "" }, + { num: 9, text: "state.name = 'Bob'" }, + { num: 10, text: "state.count++" }, + { num: 11, text: "state.count++" }, + { num: 12, text: "await nextTick()" }, + ], + steps: [ + { + descriptionHtml: + 'reactive() wraps the plain object in a Proxy with get, set, has, and deleteProperty handlers, and caches the pair in a WeakMap so the same object always yields the same proxy. No trap has fired yet, so targetMap is empty.', + activeLine: 3, + doneLines: [1], + traps: [], + deps: [], + scheduler: IDLE, + computed: null, + bindings: null, + consoleOutput: [], + }, + { + descriptionHtml: + 'watchEffect() creates a ReactiveEffect around the callback and runs it immediately. Before the callback body executes, effect.run() sets the global activeEffect to effect#1 so any trap that fires knows who is reading.', + activeLine: 5, + doneLines: [1, 3], + traps: [], + deps: [], + scheduler: sched({ activeEffect: "effect#1" }), + computed: null, + bindings: null, + consoleOutput: [], + }, + { + descriptionHtml: + 'Reading state.count hits the get trap. It calls track(target, "count"), which looks up targetMap.get(raw), then depsMap.get("count"), creates the missing Dep, and adds effect#1 to it. This is the only place a dependency is ever recorded.', + activeLine: 6, + doneLines: [1, 3, 5], + traps: [t1GetCount], + deps: [dep("state", "count", ["effect#1"], "tracking")], + scheduler: sched({ activeEffect: "effect#1" }), + computed: null, + bindings: null, + consoleOutput: [], + }, + { + descriptionHtml: + "The callback logs double 0 and returns. run() restores activeEffect to undefined. Note what is missing: state.name was never read, so there is no Dep for name and no effect will ever react to it.", + activeLine: 7, + doneLines: [1, 3, 5, 6], + traps: [t1GetCount], + deps: [dep("state", "count", ["effect#1"])], + scheduler: IDLE, + computed: null, + bindings: null, + consoleOutput: ["double 0"], + }, + { + descriptionHtml: + 'state.name = \'Bob\' hits the set trap. The write lands on the raw object through Reflect.set, then trigger(target, "name") looks for a Dep and finds none. Nothing is scheduled: an untracked write is free.', + activeLine: 9, + doneLines: [1, 3, 5, 6, 7], + traps: [t1GetCount, t1SetName], + deps: [dep("state", "count", ["effect#1"])], + scheduler: IDLE, + computed: null, + bindings: null, + consoleOutput: ["double 0"], + }, + { + descriptionHtml: + "state.count++ is a read followed by a write. The get trap fires first, but activeEffect is undefined because this runs in module scope, so track() returns without touching the map.", + activeLine: 10, + doneLines: [1, 3, 5, 6, 7, 9], + traps: [t1GetCount, t1SetName, t1GetCount2], + deps: [dep("state", "count", ["effect#1"])], + scheduler: IDLE, + computed: null, + bindings: null, + consoleOutput: ["double 0"], + }, + { + descriptionHtml: + 'The set trap writes 1, sees the value changed, and calls trigger(target, "count"). The Dep contains effect#1. Because watchEffect attaches a scheduler, the effect is not run inline: queueJob(effect#1) pushes it into the pre-flush queue and queues a single flushJobs microtask.', + activeLine: 10, + doneLines: [1, 3, 5, 6, 7, 9], + traps: [t1GetCount, t1SetName, t1GetCount2, t1SetCount1], + deps: [dep("state", "count", ["effect#1"], "triggering")], + scheduler: sched({ queue: ["effect#1 (pre)"], flushPending: true }), + computed: null, + bindings: null, + consoleOutput: ["double 0"], + }, + { + descriptionHtml: + "The second state.count++ writes 2 and triggers again, but queueJob deduplicates: effect#1 is already in the queue, so it is not added twice. Two writes, one pending run. This is how Vue batches synchronous mutations.", + activeLine: 11, + doneLines: [1, 3, 5, 6, 7, 9, 10], + traps: [ + t1GetCount, + t1SetName, + t1GetCount2, + t1SetCount1, + t1GetCount3, + t1SetCount2, + ], + deps: [dep("state", "count", ["effect#1"], "triggering")], + scheduler: sched({ queue: ["effect#1 (pre)"], flushPending: true }), + computed: null, + bindings: null, + consoleOutput: ["double 0"], + }, + { + descriptionHtml: + 'await nextTick() yields to the microtask queue. nextTick returns the same promise the scheduler chained flushJobs onto, so the flush runs before this line resumes. The effect has still not re-run at this point.', + activeLine: 12, + doneLines: [1, 3, 5, 6, 7, 9, 10, 11], + traps: [ + t1GetCount, + t1SetName, + t1GetCount2, + t1SetCount1, + t1GetCount3, + t1SetCount2, + ], + deps: [dep("state", "count", ["effect#1"])], + scheduler: sched({ queue: ["effect#1 (pre)"], flushPending: true }), + computed: null, + bindings: null, + consoleOutput: ["double 0"], + }, + { + descriptionHtml: + 'flushJobs drains the queue and calls effect#1.run(). activeEffect is set again, the callback reads state.count, the get trap tracks it (Vue 3.4 compares Dep versions instead of clearing and re-adding), and the log shows double 4. One run for two writes.', + activeLine: 6, + doneLines: [1, 3, 5, 7, 9, 10, 11], + traps: [ + t1GetCount, + t1SetName, + t1GetCount2, + t1SetCount1, + t1GetCount3, + t1SetCount2, + t1GetCountFlush, + ], + deps: [dep("state", "count", ["effect#1"], "tracking")], + scheduler: sched({ activeEffect: "effect#1", ranNow: ["effect#1"] }), + computed: null, + bindings: null, + consoleOutput: ["double 0", "double 4"], + }, + { + descriptionHtml: + 'The flush finishes, the nextTick promise resolves, and line 12 continues. A component template is compiled into exactly this kind of effect, which is why a Vue component re-renders only when a property it actually read changes. React, by contrast, re-runs the whole component function on any state update it owns.', + activeLine: 12, + doneLines: [1, 3, 5, 6, 7, 9, 10, 11], + traps: [ + t1GetCount, + t1SetName, + t1GetCount2, + t1SetCount1, + t1GetCount3, + t1SetCount2, + t1GetCountFlush, + ], + deps: [dep("state", "count", ["effect#1"])], + scheduler: IDLE, + computed: null, + bindings: null, + consoleOutput: ["double 0", "double 4"], + }, + ], +}; + +/* ------------------------------------------------------------------ */ +/* Example 2: computed caching */ +/* ------------------------------------------------------------------ */ + +const t2GetCount: TrapEntry = { + id: "t2-get-count", + op: "get", + target: "state", + key: "count", + value: "1", + result: "track", + note: "activeEffect is the computed's own effect, so the Dep for count now holds computed(double).", +}; +const t2SetCount: TrapEntry = { + id: "t2-set-count", + op: "set", + target: "state", + key: "count", + value: "5", + result: "trigger", + note: "trigger() notifies computed(double). A computed is only marked dirty, its getter does not run here.", +}; +const t2GetCount2: TrapEntry = { + id: "t2-get-count-2", + op: "get", + target: "state", + key: "count", + value: "5", + result: "track", + note: "Recompute on demand. The Dep already holds computed(double); its version is refreshed.", +}; + +const computedExample: VueReactivityExample = { + id: "computed-caching", + title: "Computed Caching", + description: + "A computed runs its getter lazily, caches the result, and only recomputes after a dependency changed.", + kind: "computed", + codeLines: [ + { num: 1, text: "import { reactive, computed } from 'vue'" }, + { num: 2, text: "" }, + { num: 3, text: "const state = reactive({ count: 1 })" }, + { num: 4, text: "" }, + { num: 5, text: "const double = computed(() => {" }, + { num: 6, text: " console.log('getter ran')" }, + { num: 7, text: " return state.count * 2" }, + { num: 8, text: "})" }, + { num: 9, text: "" }, + { num: 10, text: "console.log(double.value)" }, + { num: 11, text: "console.log(double.value)" }, + { num: 12, text: "state.count = 5" }, + { num: 13, text: "console.log('after write')" }, + { num: 14, text: "console.log(double.value)" }, + ], + steps: [ + { + descriptionHtml: + 'reactive() returns a Proxy around { count: 1 }. As before, nothing is tracked until an effect reads through it.', + activeLine: 3, + doneLines: [1], + traps: [], + deps: [], + scheduler: IDLE, + computed: null, + bindings: null, + consoleOutput: [], + }, + { + descriptionHtml: + 'computed() builds a ComputedRefImpl holding a ReactiveEffect around the getter. The getter is not called yet: a computed is lazy, so the instance starts dirty with no cached value and zero runs.', + activeLine: 5, + doneLines: [1, 3], + traps: [], + deps: [], + scheduler: IDLE, + computed: { + name: "double", + dirty: true, + cached: "undefined", + evaluations: 0, + subscribers: [], + }, + bindings: null, + consoleOutput: [], + }, + { + descriptionHtml: + 'Reading double.value enters the class getter on ComputedRefImpl. This read happens at module scope, so there is no outer effect to subscribe to the computed. The instance is dirty, so it calls effect.run() and activeEffect becomes the computed itself.', + activeLine: 10, + doneLines: [1, 3, 5, 8], + traps: [], + deps: [], + scheduler: sched({ activeEffect: "computed(double)" }), + computed: { + name: "double", + dirty: true, + cached: "undefined", + evaluations: 0, + subscribers: [], + }, + bindings: null, + consoleOutput: [], + }, + { + descriptionHtml: + 'The getter logs, then reads state.count. The get trap fires with activeEffect set to the computed, so track() records computed(double) in the Dep for count. A computed is a dependency consumer like any other effect.', + activeLine: 7, + doneLines: [1, 3, 5, 6, 8, 10], + traps: [t2GetCount], + deps: [dep("state", "count", ["computed(double)"], "tracking")], + scheduler: sched({ activeEffect: "computed(double)" }), + computed: { + name: "double", + dirty: true, + cached: "undefined", + evaluations: 0, + subscribers: [], + }, + bindings: null, + consoleOutput: ["getter ran"], + }, + { + descriptionHtml: + "The getter returns 2. The computed stores it as _value, clears dirty, and the outer console.log prints it. In Vue 3.4+ the instance also records the global version counter so later reads can tell whether anything reactive changed since.", + activeLine: 10, + doneLines: [1, 3, 5, 6, 7, 8], + traps: [t2GetCount], + deps: [dep("state", "count", ["computed(double)"])], + scheduler: IDLE, + computed: { + name: "double", + dirty: false, + cached: "2", + evaluations: 1, + subscribers: [], + }, + bindings: null, + consoleOutput: ["getter ran", "2"], + }, + { + descriptionHtml: + 'The second read sees dirty === false. Vue 3.4 goes further: it compares the stored global version with the current one, they match, so it does not even walk the Dep list. The cached 2 is returned and the getter does not run. No trap fires because double is a class instance, not a Proxy.', + activeLine: 11, + doneLines: [1, 3, 5, 6, 7, 8, 10], + traps: [t2GetCount], + deps: [dep("state", "count", ["computed(double)"])], + scheduler: IDLE, + computed: { + name: "double", + dirty: false, + cached: "2", + evaluations: 1, + subscribers: [], + }, + bindings: null, + consoleOutput: ["getter ran", "2", "2"], + }, + { + descriptionHtml: + 'state.count = 5 hits the set trap and trigger() walks the Dep for count. For a computed subscriber, trigger does not re-run the getter. It bumps the Dep version, flags the computed dirty, and would notify the computed\'s own subscribers, of which there are none. The stale 2 stays in memory.', + activeLine: 12, + doneLines: [1, 3, 5, 6, 7, 8, 10, 11], + traps: [t2GetCount, t2SetCount], + deps: [dep("state", "count", ["computed(double)"], "triggering")], + scheduler: IDLE, + computed: { + name: "double", + dirty: true, + cached: "2", + evaluations: 1, + subscribers: [], + }, + bindings: null, + consoleOutput: ["getter ran", "2", "2"], + }, + { + descriptionHtml: + "after write is logged and the getter count is still 1. Invalidation is eager, recomputation is deferred: a computed that nobody reads after a change costs nothing beyond flipping a flag.", + activeLine: 13, + doneLines: [1, 3, 5, 6, 7, 8, 10, 11, 12], + traps: [t2GetCount, t2SetCount], + deps: [dep("state", "count", ["computed(double)"])], + scheduler: IDLE, + computed: { + name: "double", + dirty: true, + cached: "2", + evaluations: 1, + subscribers: [], + }, + bindings: null, + consoleOutput: ["getter ran", "2", "2", "after write"], + }, + { + descriptionHtml: + "The third read finds dirty === true (in 3.4 terms, the Dep version moved), so it runs the getter again. activeEffect is the computed, state.count is tracked once more, and getter ran prints a second time.", + activeLine: 7, + doneLines: [1, 3, 5, 6, 8, 10, 11, 12, 13], + traps: [t2GetCount, t2SetCount, t2GetCount2], + deps: [dep("state", "count", ["computed(double)"], "tracking")], + scheduler: sched({ activeEffect: "computed(double)" }), + computed: { + name: "double", + dirty: true, + cached: "2", + evaluations: 1, + subscribers: [], + }, + bindings: null, + consoleOutput: ["getter ran", "2", "2", "after write", "getter ran"], + }, + { + descriptionHtml: + "The getter returns 10, the cache is replaced, and dirty clears. Two evaluations for three reads and one write. If a template read double.value, the render effect would appear under subscribers, and the write on line 12 would schedule that render, still without running the getter until the template asked for the value.", + activeLine: 14, + doneLines: [1, 3, 5, 6, 7, 8, 10, 11, 12, 13], + traps: [t2GetCount, t2SetCount, t2GetCount2], + deps: [dep("state", "count", ["computed(double)"])], + scheduler: IDLE, + computed: { + name: "double", + dirty: false, + cached: "10", + evaluations: 2, + subscribers: [], + }, + bindings: null, + consoleOutput: [ + "getter ran", + "2", + "2", + "after write", + "getter ran", + "10", + ], + }, + ], +}; + +/* ------------------------------------------------------------------ */ +/* Example 3: ref vs reactive and losing reactivity */ +/* ------------------------------------------------------------------ */ + +const bCount = (value: string): BindingEntry => ({ + name: "count", + kind: "ref", + value: `RefImpl { value: ${value} }`, + note: "Primitives cannot be proxied. The RefImpl object owns a Dep and exposes it through .value.", +}); +const bState = (total: string): BindingEntry => ({ + name: "state", + kind: "reactive", + value: `Proxy { total: ${total} }`, + note: "Reads and writes go through the Proxy traps, so they can be tracked and triggered.", +}); +const bTotal: BindingEntry = { + name: "total", + kind: "plain", + value: "1", + note: "A copied number. No trap, no Dep, no way to notify anyone when state.total changes.", +}; +const bTotalRef = (total: string): BindingEntry => ({ + name: "totalRef", + kind: "toRef", + value: `ObjectRefImpl -> state.total (${total})`, + note: "Its .value getter forwards to state.total through the Proxy, so reads are tracked.", +}); + +const t3GetRef: TrapEntry = { + id: "t3-get-ref", + op: "get", + target: "count", + key: "value", + value: "0", + result: "track", + isRef: true, + note: "RefImpl getter calls trackRefValue(). effect#1 is added to the ref's own Dep.", +}; +const t3SetRef: TrapEntry = { + id: "t3-set-ref", + op: "set", + target: "count", + key: "value", + value: "1", + result: "trigger", + isRef: true, + note: "RefImpl setter calls triggerRefValue(). flush: 'sync' runs effect#1 immediately.", +}; +const t3GetTotalDestructure: TrapEntry = { + id: "t3-get-total-destructure", + op: "get", + target: "state", + key: "total", + value: "1", + result: "noop", + note: "Destructuring reads once at module scope. activeEffect is undefined, nothing is tracked.", +}; +const t3SetTotal2: TrapEntry = { + id: "t3-set-total-2", + op: "set", + target: "state", + key: "total", + value: "2", + result: "miss", + note: "trigger() finds no Dep for total. effect#2 never read through the proxy.", +}; +const t3GetTotalRef: TrapEntry = { + id: "t3-get-total-ref", + op: "get", + target: "state", + key: "total", + value: "2", + result: "track", + note: "totalRef.value forwarded to state.total inside effect#3, so the Dep for total is created.", +}; +const t3SetTotal3: TrapEntry = { + id: "t3-set-total-3", + op: "set", + target: "state", + key: "total", + value: "3", + result: "trigger", + note: "Dep for total holds effect#3. flush: 'sync' runs it inline.", +}; + +const pitfallsExample: VueReactivityExample = { + id: "ref-vs-reactive", + title: "Ref vs Reactive", + description: + "Reactivity lives in the ref or proxy object, not the variable. Destructuring copies the value out and severs the link; toRefs keeps it.", + kind: "pitfalls", + codeLines: [ + { + num: 1, + text: "import { ref, reactive, toRefs, watchEffect } from 'vue'", + }, + { num: 2, text: "const sync = { flush: 'sync' }" }, + { num: 3, text: "" }, + { num: 4, text: "const count = ref(0)" }, + { + num: 5, + text: "watchEffect(() => console.log('ref', count.value), sync)", + }, + { num: 6, text: "count.value++" }, + { num: 7, text: "" }, + { num: 8, text: "const state = reactive({ total: 1 })" }, + { num: 9, text: "const { total } = state" }, + { num: 10, text: "watchEffect(() => console.log('plain', total), sync)" }, + { num: 11, text: "state.total = 2" }, + { num: 12, text: "" }, + { num: 13, text: "const { total: totalRef } = toRefs(state)" }, + { + num: 14, + text: "watchEffect(() => console.log('toRef', totalRef.value), sync)", + }, + { num: 15, text: "state.total = 3" }, + ], + steps: [ + { + descriptionHtml: + "ref(0) returns a RefImpl: an object with a value accessor pair and its own Dep. A Proxy cannot wrap a number, so the wrapper object is what makes a primitive observable. flush: 'sync' is used here so each effect re-runs inline instead of in the microtask flush.", + activeLine: 4, + doneLines: [1, 2], + traps: [], + deps: [], + scheduler: IDLE, + computed: null, + bindings: [bCount("0")], + consoleOutput: [], + }, + { + descriptionHtml: + 'effect#1 runs at once. Reading count.value enters the RefImpl getter, which calls trackRefValue(). Same idea as the Proxy trap, different entry point: the ref\'s Dep now holds effect#1.', + activeLine: 5, + doneLines: [1, 2, 4], + traps: [t3GetRef], + deps: [dep("count", "value", ["effect#1"], "tracking")], + scheduler: sched({ activeEffect: "effect#1" }), + computed: null, + bindings: [bCount("0")], + consoleOutput: ["ref 0"], + }, + { + descriptionHtml: + "count.value++ goes through the setter. hasChanged(0, 1) is true, so triggerRefValue() notifies the Dep. With flush: 'sync' the scheduler runs effect#1 immediately and ref 1 prints. Without the option it would be queued for the next microtask.", + activeLine: 6, + doneLines: [1, 2, 4, 5], + traps: [t3GetRef, t3SetRef], + deps: [dep("count", "value", ["effect#1"], "triggering")], + scheduler: sched({ ranNow: ["effect#1"] }), + computed: null, + bindings: [bCount("1")], + consoleOutput: ["ref 0", "ref 1"], + }, + { + descriptionHtml: + "reactive() creates the Proxy for { total: 1 }. The Dep for count.value is untouched; each reactive source keeps its own entry in targetMap.", + activeLine: 8, + doneLines: [1, 2, 4, 5, 6], + traps: [t3GetRef, t3SetRef], + deps: [dep("count", "value", ["effect#1"])], + scheduler: IDLE, + computed: null, + bindings: [bCount("1"), bState("1")], + consoleOutput: ["ref 0", "ref 1"], + }, + { + descriptionHtml: + 'Destructuring evaluates state.total exactly once, right now, at module scope. The get trap fires with no activeEffect, so nothing is tracked, and the local total receives the plain number 1. The proxy is out of the picture for this variable.', + activeLine: 9, + doneLines: [1, 2, 4, 5, 6, 8], + traps: [t3GetRef, t3SetRef, t3GetTotalDestructure], + deps: [dep("count", "value", ["effect#1"])], + scheduler: IDLE, + computed: null, + bindings: [bCount("1"), bState("1"), bTotal], + consoleOutput: ["ref 0", "ref 1"], + }, + { + descriptionHtml: + 'effect#2 runs and reads the local total. No trap fires because a plain variable has no interceptor, so track() is never called. The effect finishes with zero dependencies, which means nothing can ever schedule it again.', + activeLine: 10, + doneLines: [1, 2, 4, 5, 6, 8, 9], + traps: [t3GetRef, t3SetRef, t3GetTotalDestructure], + deps: [dep("count", "value", ["effect#1"])], + scheduler: sched({ activeEffect: "effect#2" }), + computed: null, + bindings: [bCount("1"), bState("1"), bTotal], + consoleOutput: ["ref 0", "ref 1", "plain 1"], + }, + { + descriptionHtml: + 'state.total = 2 hits the set trap and trigger() looks up depsMap.get("total"). There is no Dep, so nothing runs. The proxy now says 2 while the local total still says 1, and the console does not change.', + activeLine: 11, + doneLines: [1, 2, 4, 5, 6, 8, 9, 10], + traps: [t3GetRef, t3SetRef, t3GetTotalDestructure, t3SetTotal2], + deps: [dep("count", "value", ["effect#1"])], + scheduler: IDLE, + computed: null, + bindings: [bCount("1"), bState("2"), bTotal], + consoleOutput: ["ref 0", "ref 1", "plain 1"], + }, + { + descriptionHtml: + 'toRefs(state) returns a plain object whose every property is an ObjectRefImpl. Destructuring that object copies the ref wrapper, not the number, and the wrapper\'s .value getter reads state.total through the Proxy on every access.', + activeLine: 13, + doneLines: [1, 2, 4, 5, 6, 8, 9, 10, 11], + traps: [t3GetRef, t3SetRef, t3GetTotalDestructure, t3SetTotal2], + deps: [dep("count", "value", ["effect#1"])], + scheduler: IDLE, + computed: null, + bindings: [bCount("1"), bState("2"), bTotal, bTotalRef("2")], + consoleOutput: ["ref 0", "ref 1", "plain 1"], + }, + { + descriptionHtml: + 'effect#3 reads totalRef.value, which forwards to state.total. This time the get trap fires with activeEffect set, so track() creates the Dep for total and records effect#3. The log shows the current value, 2.', + activeLine: 14, + doneLines: [1, 2, 4, 5, 6, 8, 9, 10, 11, 13], + traps: [ + t3GetRef, + t3SetRef, + t3GetTotalDestructure, + t3SetTotal2, + t3GetTotalRef, + ], + deps: [ + dep("count", "value", ["effect#1"]), + dep("state", "total", ["effect#3"], "tracking"), + ], + scheduler: sched({ activeEffect: "effect#3" }), + computed: null, + bindings: [bCount("1"), bState("2"), bTotal, bTotalRef("2")], + consoleOutput: ["ref 0", "ref 1", "plain 1", "toRef 2"], + }, + { + descriptionHtml: + "state.total = 3 triggers the Dep for total, which now holds effect#3, and toRef 3 prints. effect#2 is still in no Dep and stays silent. The rule: reactivity is a property of the ref or proxy object, so copy the wrapper, never the value. In <script setup> the compiler unwraps refs in templates so you rarely write .value there.", + activeLine: 15, + doneLines: [1, 2, 4, 5, 6, 8, 9, 10, 11, 13, 14], + traps: [ + t3GetRef, + t3SetRef, + t3GetTotalDestructure, + t3SetTotal2, + t3GetTotalRef, + t3SetTotal3, + ], + deps: [ + dep("count", "value", ["effect#1"]), + dep("state", "total", ["effect#3"], "triggering"), + ], + scheduler: sched({ ranNow: ["effect#3"] }), + computed: null, + bindings: [bCount("1"), bState("3"), bTotal, bTotalRef("3")], + consoleOutput: ["ref 0", "ref 1", "plain 1", "toRef 2", "toRef 3"], + }, + ], +}; + +export const EXAMPLES: VueReactivityExample[] = [ + trackExample, + computedExample, + pitfallsExample, +]; diff --git a/src/components/visualizations/vue-reactivity/helpers.ts b/src/components/visualizations/vue-reactivity/helpers.ts new file mode 100644 index 0000000..c83b36c --- /dev/null +++ b/src/components/visualizations/vue-reactivity/helpers.ts @@ -0,0 +1,58 @@ +import { + createKindBadgeClass, + createKindLabel, +} from "@/lib/visualization-helpers"; +import type { + BindingKind, + DepStatus, + TrapResult, + VueReactivityKind, +} from "./types"; + +export const kindBadgeClass = createKindBadgeClass({ + track: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25", + computed: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25", + pitfalls: "bg-rose-500/15 text-rose-400 border-rose-500/25", +}); + +export const kindLabel = createKindLabel({ + track: "track and trigger", + computed: "computed caching", + pitfalls: "ref vs reactive", +}); + +export const TRAP_RESULT_STYLES: Record = { + track: "border-emerald-500/30 bg-emerald-500/8 text-emerald-200", + trigger: "border-amber-500/30 bg-amber-500/8 text-amber-200", + noop: "border-slate-500/30 bg-slate-500/8 text-slate-400", + miss: "border-rose-500/30 bg-rose-500/8 text-rose-200", +}; + +export const TRAP_RESULT_LABELS: Record = { + track: "TRACK", + trigger: "TRIGGER", + noop: "NO-OP", + miss: "NO DEP", +}; + +export const DEP_STATUS_STYLES: Record = { + idle: "border-slate-600/40 bg-slate-800/30", + tracking: + "border-emerald-300/40 bg-emerald-400/10 shadow-[0_0_18px_rgba(52,211,153,0.1)]", + triggering: + "border-amber-300/40 bg-amber-400/10 shadow-[0_0_18px_rgba(251,191,36,0.1)]", +}; + +export const BINDING_STYLES: Record = { + reactive: "border-cyan-300/40 bg-cyan-400/10 text-cyan-200", + ref: "border-violet-300/40 bg-violet-400/10 text-violet-200", + toRef: "border-emerald-300/40 bg-emerald-400/10 text-emerald-200", + plain: "border-rose-400/40 bg-rose-400/10 text-rose-200", +}; + +export const BINDING_LABELS: Record = { + reactive: "reactive", + ref: "ref", + toRef: "toRef", + plain: "plain value", +}; diff --git a/src/components/visualizations/vue-reactivity/index.tsx b/src/components/visualizations/vue-reactivity/index.tsx new file mode 100644 index 0000000..46ecffa --- /dev/null +++ b/src/components/visualizations/vue-reactivity/index.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { NeonPanel } from "@/components/visualization-ui/NeonPanel"; +import { ConsoleOutput } from "@/components/visualization-ui/ConsoleOutput"; +import { + ExamplePicker, + KindBadge, +} from "@/components/visualization-ui/ExamplePicker"; +import { SourceCodePanel } from "@/components/visualization-ui/SourceCodePanel"; +import { + SourceGrid, + VisualizationSection, +} from "@/components/visualization-ui/VisualizationLayout"; +import { VisualizationToolbar } from "@/components/visualization-ui/VisualizationToolbar"; +import { useExampleTopic } from "@/hooks/useExampleTopic"; +import { useChangeFlash } from "@/hooks/useChangeFlash"; +import { EXAMPLES } from "./data"; +import { kindBadgeClass, kindLabel } from "./helpers"; +import { TrapLog } from "./components/TrapLog"; +import { DepMap } from "./components/DepMap"; +import { StatePanels } from "./components/StatePanels"; + +export const VueReactivity = () => { + const { + example, + activeExampleId, + handleExampleChange, + playback, + currentStep, + } = useExampleTopic(EXAMPLES); + const { currentStepIndex } = playback; + + const flashes = useChangeFlash( + { + description: currentStep?.descriptionHtml, + traps: currentStep?.traps, + deps: currentStep?.deps, + scheduler: currentStep?.scheduler, + computed: currentStep?.computed, + bindings: currentStep?.bindings, + console: currentStep?.consoleOutput, + }, + currentStepIndex, + ); + + return ( + <> + ( + + {kindLabel(ex.kind)} + + )} + /> + } + /> + + + + + +
    +
    + + + + + + + +
    + + + +
    + +
    +
    +
    +
    + + ); +}; diff --git a/src/components/visualizations/vue-reactivity/types.ts b/src/components/visualizations/vue-reactivity/types.ts new file mode 100644 index 0000000..34491ce --- /dev/null +++ b/src/components/visualizations/vue-reactivity/types.ts @@ -0,0 +1,75 @@ +import type { SourceLine, BaseStep } from "@/types/visualization"; +import type { ExampleOption } from "@/components/visualization-ui/ExampleSelector"; + +export type VueReactivityKind = "track" | "computed" | "pitfalls"; + +export type TrapOp = "get" | "set"; + +/** What the interceptor did with the access. */ +export type TrapResult = "track" | "trigger" | "noop" | "miss"; + +export interface TrapEntry { + id: string; + op: TrapOp; + /** Variable name holding the proxy or ref, e.g. "state" or "count". */ + target: string; + key: string; + /** Written value for set, read value for get. */ + value: string; + result: TrapResult; + note: string; + /** True when the access went through a ref accessor rather than a Proxy trap. */ + isRef?: boolean; +} + +export type DepStatus = "idle" | "tracking" | "triggering"; + +export interface DepEntry { + target: string; + key: string; + effects: string[]; + status: DepStatus; +} + +export interface SchedulerState { + activeEffect: string | null; + queue: string[]; + /** Whether a flushJobs microtask has been queued and not yet run. */ + flushPending: boolean; + /** Effects that ran synchronously in this step, e.g. flush: "sync" effects. */ + ranNow: string[]; +} + +export interface ComputedState { + name: string; + dirty: boolean; + cached: string; + evaluations: number; + /** Subscribers of the computed itself (effects that read .value). */ + subscribers: string[]; +} + +export type BindingKind = "reactive" | "ref" | "plain" | "toRef"; + +export interface BindingEntry { + name: string; + kind: BindingKind; + value: string; + /** Short note on whether reads of this binding can be tracked. */ + note: string; +} + +export interface VueReactivityStep extends BaseStep { + traps: TrapEntry[]; + deps: DepEntry[]; + scheduler: SchedulerState; + computed: ComputedState | null; + bindings: BindingEntry[] | null; + consoleOutput: string[]; +} + +export interface VueReactivityExample extends ExampleOption { + kind: VueReactivityKind; + codeLines: SourceLine[]; + steps: VueReactivityStep[]; +} diff --git a/src/content/static/onboarding/tourCopy.ts b/src/content/static/onboarding/tourCopy.ts index 448deb5..dc14226 100644 --- a/src/content/static/onboarding/tourCopy.ts +++ b/src/content/static/onboarding/tourCopy.ts @@ -24,6 +24,7 @@ export const ONBOARDING_TOUR_STEP_COPY: OnboardingTourStepCopy[] = [ "Step Back and Step Forward move one frame at a time.", "Play/Pause auto-runs the full sequence.", "Speed adjusts playback from 0.25x to 2x.", + "Space, the arrow keys, and R work as shortcuts. Drag the scrubber to jump.", ], }, { diff --git a/src/content/static/storageKeys.ts b/src/content/static/storageKeys.ts index a78407b..67e9cf6 100644 --- a/src/content/static/storageKeys.ts +++ b/src/content/static/storageKeys.ts @@ -1,4 +1,6 @@ export const STORAGE_KEYS = { onboardingSeen: "vjs-onboarding-seen", sandboxPrefix: "vjs-sandbox-", + topicProgress: "vjs-topic-progress", + explanationMode: "vjs-explanation-mode", } as const; diff --git a/src/content/theory/ai/attention.ts b/src/content/theory/ai/attention.ts new file mode 100644 index 0000000..a6d1353 --- /dev/null +++ b/src/content/theory/ai/attention.ts @@ -0,0 +1,95 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const attentionTheory: TopicTheoryContent = { + summary: + "Self-attention lets every token in a sequence build a new representation by taking a weighted average of the other tokens, where the weights come from how well each token's query matches the others' keys. It is the operation that gives transformers their ability to relate words at any distance.", + whatItIs: [ + 'Before attention, each token has a vector that only describes the token itself. Attention rewrites that vector so it also describes the token\'s context: after one attention layer, the vector for "sat" can carry information about "cat" because the layer let "sat" look at "cat" and copy part of it.', + "Three learned linear projections turn each input vector into a query, a key, and a value. The query is what the token is looking for, the key is what the token advertises about itself, and the value is the content it hands over when another token attends to it. Scores are dot products between one query and every key, softmax turns those scores into weights that sum to one, and the output is the weighted sum of the values.", + "The scores are divided by the square root of the key dimension before softmax. Dot products grow with the number of dimensions, so without scaling the softmax input would be large, the weights would collapse toward a single one-hot entry, and the gradient through softmax would be close to zero. Scaling keeps the distribution soft enough to train.", + "Because every query is scored against every key, the attention matrix is n by n for a sequence of n tokens. That quadratic cost in both time and memory is why context length is the expensive axis in a transformer and why so much engineering effort goes into sparse, linear, or cached variants of the same idea.", + ], + howItWorks: [ + "Step 1: each input embedding x is multiplied by three learned matrices to produce q = x Wq, k = x Wk, and v = x Wv. All tokens share the same matrices, so the projections are computed as one batched matmul.", + "Step 2: the raw score between query i and key j is the dot product q_i . k_j. Stacking all of them gives the matrix Q K^T with one row per query and one column per key.", + "Step 3: the score matrix is divided by sqrt(d_k). In a decoder, a causal mask then sets every entry where j > i to negative infinity, so a token cannot see positions that come after it.", + "Step 4: softmax is applied to each row. Exponentiating makes every entry positive and turns negative infinity into exactly zero, then dividing by the row sum makes each row a probability distribution over the keys.", + "Step 5: the output for token i is the sum over j of weight_ij times v_j. High-weight tokens contribute most of their value vector, low-weight tokens contribute almost nothing.", + "Step 6: multi-head attention runs several independent copies of steps 1 to 5 with smaller d_k each, concatenates their outputs, and multiplies by an output matrix Wo so the model can combine what the heads found.", + ], + commonMistakes: [ + { + title: "Applying the mask after softmax", + explanation: + "Zeroing future weights after softmax leaves the remaining weights summing to less than one, and the future positions still influenced the normaliser. The masked scores must be removed before the exponentials are summed.", + fix: "Fill masked positions with negative infinity (or a very large negative number) before calling softmax so exp of those entries is zero and the surviving weights renormalise correctly.", + }, + { + title: "Forgetting the sqrt(d_k) scaling", + explanation: + "With d_k of 64 or more the raw dot products are large, softmax saturates, and gradients through the attention weights vanish. Training stalls or the heads collapse onto single positions.", + fix: "Always divide the score matrix by sqrt(d_k), where d_k is the per-head key dimension, not the full model dimension.", + }, + { + title: "Treating keys and values as the same thing", + explanation: + "Keys and values come from different projections on purpose. The key decides how much attention a token receives, while the value decides what is copied. Sharing one matrix for both forces the thing that matches to also be the thing that is passed on.", + fix: "Keep Wk and Wv separate, and when reasoning about a head ask two questions: which tokens score highly, and what those tokens contribute once selected.", + }, + { + title: "Assuming attention is cheap for long inputs", + explanation: + "The weight matrix has n squared entries. Doubling the prompt length quadruples the attention work and the memory for the score matrix, which is why latency and cost scale so steeply with context.", + fix: "Budget context length deliberately, reuse the key-value cache during generation instead of recomputing past keys and values, and use attention variants designed for long sequences when the workload needs them.", + }, + ], + interviewQuestions: [ + { + question: + "What do the query, key, and value represent in self-attention?", + answer: + "All three are linear projections of the same input token. The query encodes what the token is looking for, the key encodes what the token offers for matching, and the value encodes the content the token contributes to the output once it has been selected. Scores compare queries with keys, and the output mixes values.", + }, + { + question: "Why is the score matrix divided by sqrt(d_k)?", + answer: + "If the components of q and k have unit variance, their dot product has variance d_k, so scores grow with the key dimension. Large scores push softmax into a near one-hot regime with vanishing gradients. Dividing by sqrt(d_k) brings the variance back to one and keeps the weights trainable.", + codeExample: { + language: "python", + code: `scores = Q @ K.T # variance grows with d_k +scores = scores / math.sqrt(d_k) # back to unit variance +weights = softmax(scores, axis=-1) +out = weights @ V`, + }, + }, + { + question: "How does the causal mask work and why is it needed?", + answer: + "The mask sets every score where the key position is later than the query position to negative infinity before softmax, so those weights become exactly zero. During training all positions are predicted in parallel and position i is trained to predict token i plus one. Without the mask the model could read the answer through attention. At inference the future tokens do not exist yet, so the mask keeps training and generation consistent.", + codeExample: { + language: "python", + code: `n = scores.shape[-1] +future = torch.triu(torch.ones(n, n), diagonal=1).bool() +scores = scores.masked_fill(future, float("-inf")) +weights = softmax(scores, dim=-1) # exp(-inf) == 0`, + }, + }, + { + question: "What does multi-head attention add over a single head?", + answer: + "Each head has its own projections, so different heads can learn different relevance patterns, for example one head tracking adjacent tokens and another tracking the subject of the sentence. The head outputs are concatenated and mixed by an output matrix Wo. At the same model dimension the total cost is the same as one big head, but each head works in a smaller subspace.", + }, + { + question: + "What is the time and memory complexity of self-attention, and what follows from it?", + answer: + "Computing Q K^T for n tokens costs O(n squared d) time and the score matrix takes O(n squared) memory per head. Context length is therefore the expensive axis: doubling it quadruples the attention work. This is why generation uses a key-value cache to avoid recomputing past keys and values, and why long-context models rely on sparse, windowed, or otherwise approximate attention.", + }, + ], + relatedTopicIds: [ + "embeddings", + "next-token-prediction", + "tokenization", + "backpropagation", + ], +}; diff --git a/src/content/theory/ai/backpropagation.ts b/src/content/theory/ai/backpropagation.ts new file mode 100644 index 0000000..468e35a --- /dev/null +++ b/src/content/theory/ai/backpropagation.ts @@ -0,0 +1,96 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const backpropagationTheory: TopicTheoryContent = { + summary: + "Backpropagation is the algorithm that computes how much each parameter of a neural network contributed to the loss, by applying the chain rule backward through the computation graph. Gradient descent then nudges every parameter against its gradient so the loss goes down.", + whatItIs: [ + "A neural network is a long chain of simple operations: multiply by a weight, add a bias, apply an activation, repeat. The forward pass runs that chain on an input and ends with a loss, a single number that says how wrong the prediction was. Training means changing the weights so that number gets smaller.", + "To change a weight sensibly you need to know which direction lowers the loss and by how much. That quantity is the gradient dL/dw, the rate at which the loss changes per unit change of the weight. Backpropagation computes every parameter's gradient in one sweep from the loss back to the inputs, reusing the values saved during the forward pass.", + "The sweep works because of the chain rule. If the loss depends on w only through some intermediate value z, then dL/dw equals dL/dz multiplied by dz/dw. Each operation in the graph only has to know its own local derivative. Multiplying local derivatives along the path from the loss to a parameter gives the full gradient, and the partial products are shared between parameters that sit on the same path.", + "Deep learning frameworks implement this as automatic differentiation: they record the operations of the forward pass into a graph, and each operation ships with a rule for its backward step. The same machinery that trains a two-parameter neuron trains the attention and embedding matrices of a transformer with billions of parameters. Only the size of the graph changes.", + ], + howItWorks: [ + "Step 1: the forward pass computes the prediction from the input and the current parameters, saving every intermediate value (pre-activations, activations, which ReLU units were active) because the backward pass will need them.", + "Step 2: the loss function compares the prediction with the target and produces one scalar, for example the squared error (yHat - y)^2. The gradient of the loss with respect to itself is 1, which seeds the backward pass.", + "Step 3: the backward pass visits the graph in reverse order. At each node it multiplies the gradient arriving from the node above by that node's local derivative. For yHat = w * h the local derivative with respect to w is h and with respect to h is w. For ReLU it is 1 where the input was positive and 0 elsewhere.", + "Step 4: whenever the backward pass reaches a parameter, the accumulated product is that parameter's gradient. Gradients flowing into a non-parameter node keep travelling to the layer below, which is how the error signal reaches early layers.", + "Step 5: once all gradients exist, gradient descent updates every parameter at once: w = w - lr * dL/dw. Subtracting moves against the gradient, which is the direction of steepest loss increase, so the loss decreases for a small enough learning rate.", + "Step 6: the loop repeats with the new parameters. Gradients shrink as the prediction approaches the target, so steps naturally get smaller. Optimizers such as Adam and tricks such as learning rate schedules only change how the gradient is turned into a step, not how it is computed.", + ], + commonMistakes: [ + { + title: "Adding the gradient instead of subtracting it", + explanation: + "The gradient points in the direction that increases the loss fastest. Adding it walks uphill, and the loss grows on every step even though the gradients were computed correctly.", + fix: "Always update with w = w - lr * grad. If the loss increases on every iteration, check the sign of the update before anything else.", + }, + { + title: "Learning rate too large", + explanation: + "A gradient only describes the slope at the current point. A big step can jump past the minimum to a point where the loss is higher, and with a quadratic loss the error can even flip sign and grow every epoch.", + fix: "Start small (0.001 to 0.1 depending on the model), watch the loss curve, and reduce the rate if the loss oscillates or explodes. Learning rate schedules and adaptive optimizers exist for this reason.", + }, + { + title: "Updating parameters before all gradients are computed", + explanation: + "Every gradient in one backward pass is a derivative at the same set of parameter values. Changing w2 before computing dL/dw1 mixes derivatives from two different points on the loss surface.", + fix: "Compute the full backward pass first, then apply all updates. Frameworks enforce this by separating loss.backward() from optimizer.step().", + }, + { + title: "Forgetting that dead ReLU units block gradients", + explanation: + "ReLU's derivative is 0 for negative inputs. A unit whose pre-activation is negative for every training example passes zero gradient to everything upstream and never recovers.", + fix: "Use sensible weight initialisation, moderate learning rates, and consider leaky ReLU or GELU if a large share of units stops activating.", + }, + ], + interviewQuestions: [ + { + question: "What does a gradient like dL/dw actually mean?", + answer: + "It is the rate of change of the loss with respect to that parameter: if w increases by a tiny amount d, the loss changes by roughly dL/dw times d. A negative gradient means increasing w lowers the loss, which is why gradient descent moves in the opposite direction of the gradient.", + codeExample: { + language: "javascript", + code: `const x = 2.0, y = 2.0; +let w = 0.5, b = 0.1; +const yHat = w * x + b; // 1.1 +const loss = (yHat - y) ** 2; // 0.81 +const dYHat = 2 * (yHat - y); // -1.8 +const dW = dYHat * x; // -3.6: raising w lowers loss +const dB = dYHat; // -1.8`, + }, + }, + { + question: + "How does the chain rule let backpropagation reach a weight in the first layer?", + answer: + "The loss depends on an early weight only through the intermediate values above it. The chain rule says the derivative along that path is the product of the local derivatives of each operation. Backpropagation computes the product incrementally from the loss backward, so by the time it reaches the first layer the incoming gradient already contains every factor from the layers above.", + codeExample: { + language: "javascript", + code: `// yHat = w2 * relu(w1 * x), loss = (yHat - y) ** 2 +const dYHat = 2 * (yHat - y); // dL/dyHat +const dW2 = dYHat * h; // local derivative of w2 * h wrt w2 is h +const dH = dYHat * w2; // wrt h it is w2, passed down +const dZ = dH * (z > 0 ? 1 : 0); // relu gate +const dW1 = dZ * x; // dL/dw1 = product of all factors`, + }, + }, + { + question: + "Why do we subtract the gradient in the update, and what does the learning rate do?", + answer: + "The gradient is the direction of steepest increase of the loss, so subtracting a multiple of it moves toward lower loss. The learning rate scales that move. Too small and training takes many steps; too large and the step overshoots the minimum, which can make the loss oscillate or diverge.", + }, + { + question: "Why does the forward pass have to save intermediate values?", + answer: + "Local derivatives depend on the forward values. The derivative of w * h with respect to w is h, and ReLU's derivative depends on whether its input was positive. Without the saved activations the backward pass could not compute those factors. This is also why training uses far more memory than inference.", + }, + { + question: + "How does this scale to a transformer with billions of parameters?", + answer: + "The mechanism is identical. Automatic differentiation records every tensor operation of the forward pass into a graph, and each operation has a known backward rule, including matrix multiplication, softmax, and layer norm. The backward pass sweeps the graph once and produces a gradient tensor for every weight matrix, and the optimizer applies the update to all of them in parallel on the GPU.", + }, + ], + relatedTopicIds: ["attention", "embeddings", "next-token-prediction"], +}; diff --git a/src/content/theory/ai/embeddings.ts b/src/content/theory/ai/embeddings.ts new file mode 100644 index 0000000..96a3bd1 --- /dev/null +++ b/src/content/theory/ai/embeddings.ts @@ -0,0 +1,96 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const embeddingsTheory: TopicTheoryContent = { + summary: + "An embedding is a learned vector of numbers that stands in for a token, so that tokens used in similar contexts end up close together and the model can do arithmetic on meaning instead of on ids.", + whatItIs: [ + "A tokenizer turns text into integer ids, but an id is only an index: id 1 is not closer to id 2 than to id 500. The embedding layer replaces each id with a row from a matrix E of shape vocabulary size by embedding dimension. That row is the vector the rest of the network works with.", + "The values in E are parameters, learned by gradient descent alongside every other weight. Training starts them at small random numbers and adjusts the rows that appear in each batch so the model predicts text better. Tokens that show up in similar contexts receive similar gradients over time, which pushes their rows toward the same region of the space.", + "Because the vectors live in a geometric space, similarity becomes a measurement. Cosine similarity divides the dot product of two vectors by the product of their lengths and returns a value in [-1, 1]: 1 for the same direction, 0 for perpendicular, -1 for opposite. Retrieval, semantic search, and deduplication all rank candidates by this number.", + "In a transformer, the token embedding is not the whole input. Attention treats its inputs as a set, so the model adds a position signal to each token vector before the first layer. In GPT-2 that is a second learned table indexed by position, the original transformer used fixed sinusoids, and many recent models apply a rotation inside attention instead. Typical embedding widths run from 768 in GPT-2 small to 12288 in the largest GPT-3.", + ], + howItWorks: [ + "Step 1: the tokenizer maps the input text to a list of integer ids, one per token.", + "Step 2: for each id, the model reads row E[id] from the embedding matrix. This is an array index, not a matrix multiply, so the cost is tiny regardless of vocabulary size.", + "Step 3: the model reads a position vector for each slot i, from a learned table P[i] or a fixed formula, and adds it to the token vector: x_i = E[id_i] + P[i].", + "Step 4: the resulting rows form a matrix X of shape sequence length by embedding dimension, which is the input to the first attention block.", + "Step 5: during training, the loss gradient flows back through X into E. Only the rows that were looked up in the batch receive updates, so rare tokens are trained less often than common ones.", + "Step 6: at inference or in a retrieval system, two vectors are compared with cosine similarity: dot(a, b) / (|a| * |b|). The angle between them, not their lengths, is what carries the comparison.", + ], + commonMistakes: [ + { + title: "Treating token ids as if they carried meaning", + explanation: + "Ids are assigned by the tokenizer, often by merge order or frequency, so numeric closeness between ids says nothing about the tokens. Feeding raw ids into a dense layer forces the model to learn around an arbitrary ordering.", + fix: "Always pass ids through an embedding lookup first. The learned rows, not the ids, are the representation.", + }, + { + title: "Comparing vectors with the dot product alone", + explanation: + "The raw dot product grows with vector length, so a long vector scores high against everything. Frequent tokens often have larger norms, which biases nearest neighbour results toward them.", + fix: "Normalise both vectors or use cosine similarity, which divides by the norms and compares direction only. If you store normalised vectors, the dot product and cosine similarity become the same number.", + }, + { + title: "Forgetting position information", + explanation: + "Attention is permutation equivariant: shuffling the input rows shuffles the outputs the same way. With token embeddings only, the model cannot tell the cat sat from sat the cat.", + fix: "Add a position embedding to each token vector, or use a positional scheme such as RoPE inside attention, before the first transformer layer.", + }, + { + title: "Mixing embeddings from different models", + explanation: + "Each model learns its own space. The dimension may match, but the axes mean different things, so a vector from one model compared with a vector from another produces a number that is not a similarity at all.", + fix: "Embed queries and documents with the same model and version, and re-embed the whole corpus when the model changes.", + }, + ], + interviewQuestions: [ + { + question: "What is an embedding layer, and what does it compute?", + answer: + "It is a matrix with one row per vocabulary token and one column per dimension. Given a token id, it returns the row at that index. There is no multiplication involved, it is a lookup, and the rows are trained parameters that the rest of the network updates through backpropagation.", + codeExample: { + language: "javascript", + code: `// E has shape [vocabSize, d] +const ids = tokenize("cat dog car"); // [1, 2, 3] +const X = ids.map((id) => E[id]); // shape [3, d]`, + }, + }, + { + question: + "How does cosine similarity differ from the dot product, and when does it matter?", + answer: + "Cosine similarity is the dot product divided by the product of the two norms, so it measures the angle between vectors and ignores their length. It matters when vectors have different magnitudes, which is common in learned embeddings, because the raw dot product would favour long vectors regardless of direction.", + codeExample: { + language: "javascript", + code: `const dot = (a, b) => a.reduce((s, x, i) => s + x * b[i], 0); +const norm = (a) => Math.sqrt(dot(a, a)); +const cosine = (a, b) => dot(a, b) / (norm(a) * norm(b)); + +cosine([0.8, 0.5, 0.1], [0.6, 0.7, 0.1]); // 0.95`, + }, + }, + { + question: "Why do transformers need positional embeddings?", + answer: + "Self attention computes a weighted sum over the set of input vectors, and the weights depend only on the vectors themselves. Permuting the input permutes the output identically, so word order would be invisible. Adding a position dependent vector to each token gives the model a way to tell slots apart.", + codeExample: { + language: "javascript", + code: `const X = ids.map((id, i) => + tokEmb[id].map((v, k) => v + posEmb[i][k]) +);`, + }, + }, + { + question: "How are embedding values learned?", + answer: + "They are initialised randomly and updated by gradient descent like any other weight. The gradient of the loss with respect to the input vectors flows back into the rows that were looked up, so each training example nudges only the rows for the tokens it contains. Over many examples, tokens with similar contexts converge toward similar rows.", + }, + { + question: + "Why can you not compare embeddings produced by two different models?", + answer: + "Each model learns its own coordinate system. Even with equal dimension, a direction that encodes one property in model A encodes something unrelated in model B, so cosine similarity across models is meaningless. A retrieval index has to be rebuilt whenever the embedding model changes.", + }, + ], + relatedTopicIds: ["tokenization", "attention", "next-token-prediction"], +}; diff --git a/src/content/theory/ai/next-token-prediction.ts b/src/content/theory/ai/next-token-prediction.ts new file mode 100644 index 0000000..2e44eaa --- /dev/null +++ b/src/content/theory/ai/next-token-prediction.ts @@ -0,0 +1,100 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const nextTokenPredictionTheory: TopicTheoryContent = { + summary: + "Next token prediction is the single operation a language model performs: given a sequence of token ids, produce a probability distribution over the vocabulary for what comes next. Text generation is that operation run in a loop, with a sampling rule deciding which token to append each time.", + whatItIs: [ + "A language model does not write sentences. It scores every token in its vocabulary as a candidate for the next position, and a decoding loop picks one, appends it, and asks again. Every word of a chat reply came out of this loop, one token at a time.", + "The forward pass ends with a linear layer (the lm head) that maps the last position's hidden vector to one real-valued score per vocabulary entry. These scores are logits: unbounded and only meaningful relative to each other. Softmax converts them into probabilities by exponentiating each score and dividing by the sum, so larger gaps between logits become larger ratios between probabilities.", + "Decoding strategies decide what to do with that distribution. Greedy decoding takes the argmax and is deterministic. Sampling draws a token in proportion to its probability, which is why the same prompt gives different answers on different runs. Temperature rescales the logits before softmax, top-k and top-p truncate the tail before drawing, and these are usually combined.", + "Only the last position's logits are used during generation, yet the model computes hidden states for every position. Inference engines avoid redoing that work with a key-value cache: attention keys and values from earlier positions are stored, so each new step processes only the newest token and attends over the cache. The cost per token is then roughly proportional to the current sequence length rather than to its square.", + ], + howItWorks: [ + "Step 1: the tokenizer converts the prompt into integer ids. The model receives ids, never raw text, and the id list is the state that the generation loop grows.", + "Step 2: a forward pass embeds every id, runs the sequence through the transformer blocks, and reads the hidden vector at the last position.", + "Step 3: the lm head multiplies that vector by the output embedding matrix, yielding one logit per vocabulary token, tens of thousands of scores for a single step.", + "Step 4: the logits are divided by the temperature T and passed through softmax. T below 1 sharpens the distribution toward the top token, T above 1 flattens it, and T = 0 is treated as pure argmax.", + "Step 5: an optional truncation removes the tail. Top-k keeps the k highest probabilities; top-p keeps the smallest set whose cumulative probability reaches p. The survivors are renormalized so they sum to 1 again.", + "Step 6: a token is drawn from the final distribution and appended to the ids. The loop repeats until an end-of-sequence token is drawn or a token budget runs out, reusing cached keys and values so earlier positions are not recomputed.", + ], + commonMistakes: [ + { + title: "Treating logits as probabilities", + explanation: + "Logits can be negative, exceed 1, and do not sum to anything in particular. Ranking them is fine, but reading a logit of 5.8 as 'very confident' ignores the values of every other candidate.", + fix: "Apply softmax (with the temperature you actually decode at) before interpreting confidence, and compare probabilities or log probabilities, not raw scores.", + }, + { + title: "Expecting reproducible output with temperature above 0", + explanation: + "Any nonzero temperature means a random draw at every step. Even at T = 0.5, tail tokens keep a small probability, and over hundreds of tokens a rare draw becomes likely.", + fix: "Use T = 0 (greedy) or a fixed random seed when outputs must be identical across runs, and remember that batching and floating point differences can still change results.", + }, + { + title: "Using top-k alone on peaked and flat distributions", + explanation: + "A fixed k keeps too few candidates when the model is unsure and too many junk candidates when it is certain. The right count changes from step to step.", + fix: "Prefer top-p, which sizes the candidate set by cumulative probability, or combine a generous top-k with top-p so the count adapts to the model's confidence.", + }, + { + title: "Applying truncation before temperature", + explanation: + "Temperature changes the probabilities that top-p measures against, so applying top-p first and temperature second produces a different candidate set than intended.", + fix: "Follow the conventional order: scale logits by temperature, softmax, then top-k and top-p, then draw. Check the order your inference library uses if results look off.", + }, + ], + interviewQuestions: [ + { + question: "What does a language model actually output at each step?", + answer: + "A vector of logits with one entry per vocabulary token, computed from the last position's hidden state. Softmax turns it into a probability distribution, and a decoding rule picks one token from it. The model itself never chooses; the sampler does.", + codeExample: { + language: "python", + code: `logits = model(ids)[-1] # shape: (vocab_size,) +probs = softmax(logits / T) # sums to 1 +next_id = sample(probs) # argmax when T == 0 +ids.append(next_id)`, + }, + }, + { + question: + "How does temperature change the distribution, and can it reorder tokens?", + answer: + "Temperature divides every logit by T before softmax. Because division preserves order, the ranking of tokens never changes; only the gaps do. T below 1 widens the gaps and concentrates mass on the top token, T above 1 narrows them and spreads mass into the tail. In the limit T approaches 0 the distribution becomes one-hot on the argmax.", + }, + { + question: "What is the difference between top-k and top-p sampling?", + answer: + "Top-k keeps the k most probable tokens regardless of their mass. Top-p sorts tokens by probability and keeps the smallest prefix whose cumulative probability is at least p, so the candidate count grows when the model is uncertain and shrinks when it is confident. Both zero out the rest and renormalize before drawing.", + codeExample: { + language: "python", + code: `def top_p(probs, p): + order = argsort(probs)[::-1] + cum = cumsum(probs[order]) + cutoff = (cum >= p).argmax() + 1 + keep = order[:cutoff] + mask = zeros_like(probs) + mask[keep] = probs[keep] + return mask / mask.sum()`, + }, + }, + { + question: + "Why is generation with a KV cache faster than recomputing the whole sequence?", + answer: + "Attention at each layer needs keys and values for every earlier position, but those do not change once computed because the model is causal. Caching them means a new step runs the transformer only for the newest token and attends over stored tensors. Without the cache each step costs work proportional to the full sequence length squared; with it the per-step cost is closer to linear in the sequence length.", + }, + { + question: + "Why does the same prompt give different completions on different runs?", + answer: + "Because sampling draws each token from a probability distribution using a random number. Any token with nonzero probability can be chosen, and over a long completion the sequence of draws diverges quickly. Greedy decoding or a fixed seed removes this source of variation, though hardware nondeterminism in floating point reductions can still cause small differences.", + }, + ], + relatedTopicIds: [ + "attention", + "tokenization", + "embeddings", + "backpropagation", + ], +}; diff --git a/src/content/theory/ai/tokenization.ts b/src/content/theory/ai/tokenization.ts new file mode 100644 index 0000000..8eb968b --- /dev/null +++ b/src/content/theory/ai/tokenization.ts @@ -0,0 +1,103 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const tokenizationTheory: TopicTheoryContent = { + summary: + "Tokenization is the step that turns raw text into the sequence of integer ids a language model actually reads. Modern models use subword schemes such as byte pair encoding, so a token is usually a word fragment rather than a whole word or a single character.", + whatItIs: [ + "A language model cannot consume strings. Its first layer is an embedding table indexed by integers, so every input has to be converted into a list of ids drawn from a fixed vocabulary. The tokenizer is the deterministic function that performs that conversion, and its inverse, decoding, turns generated ids back into text.", + "Splitting on whitespace would make the vocabulary unbounded and leave every misspelling or new name out of vocabulary. Splitting into characters keeps the vocabulary tiny but makes sequences very long, and each position then carries almost no meaning. Subword tokenization sits between the two: frequent strings such as common words become one token, rare strings are broken into pieces the model has seen before.", + "Byte pair encoding learns those pieces from data. Training starts from base symbols, counts every adjacent pair across the corpus, merges the most frequent one into a new symbol, and repeats until the vocabulary reaches a target size. The ordered list of merges is the tokenizer. Encoding replays the same merges on new text, so training and inference always segment a string the same way.", + "Byte-level variants, used by GPT-2 and most models after it, take UTF-8 bytes rather than characters as the base symbols. That guarantees every possible input can be encoded with 256 base tokens and no unknown-token placeholder, at the cost that scripts which were rare in the training corpus, or which need several bytes per character, split into many more tokens than English does.", + ], + howItWorks: [ + "Step 1: a pre-tokenizer splits the text into pieces with a regex. In GPT-style tokenizers a leading space stays attached to the word after it, contractions are separated, and runs of whitespace or digits are isolated so merges never cross those boundaries.", + "Step 2: each piece is expanded into its base symbols, which are UTF-8 bytes mapped to printable placeholder characters in a byte-level tokenizer.", + "Step 3: the encoder repeatedly finds the adjacent pair with the lowest merge rank among the current symbols and replaces it with the merged symbol. Rank order, not left-to-right order, decides what merges next.", + "Step 4: the loop stops when no remaining adjacent pair has a merge rule. Whatever symbols survive are the tokens for that piece; a common word ends as one symbol while a rare one ends as several.", + "Step 5: every surviving symbol is looked up in the vocabulary to get its integer id, and the ids from all pieces are concatenated into the model input.", + "Step 6: the model treats each id as an opaque position. It has no view of the underlying letters, which is why spelling, counting characters, and arithmetic on long numbers are harder than they look.", + ], + commonMistakes: [ + { + title: "Estimating cost or context by word count", + explanation: + "Context windows and API pricing are measured in tokens. English prose averages roughly four characters per token, but code, numbers, unusual names, and non-Latin scripts can run at one to three tokens per word.", + fix: "Count tokens with the model's own tokenizer before truncating or budgeting. Never assume a fixed words-to-tokens ratio across languages or content types.", + }, + { + title: "Treating a word and its space-prefixed form as the same token", + explanation: + "In GPT-style vocabularies the space belongs to the following word, so 'hello' and ' hello' are different ids with different embeddings. Prompts that end with a trailing space or start a line without one can shift which token the model sees.", + fix: "Keep the boundary consistent with training data: end prompts without a trailing space and let the model produce the space-prefixed token itself.", + }, + { + title: "Mixing tokenizers across models", + explanation: + "Ids only mean something relative to the vocabulary they came from. Feeding ids from one tokenizer into another model, or comparing token counts across model families, silently produces nonsense.", + fix: "Load the tokenizer that ships with the exact model checkpoint and treat token counts as model-specific numbers.", + }, + { + title: "Expecting character-level reasoning", + explanation: + "A model sees 'strawberry' as one or two ids, not as a run of letters, so questions about letter counts or reversing strings work only when the model has memorised the spelling.", + fix: "Break the string into visible characters in the prompt, or hand the task to code. Do not rely on the model to see inside tokens.", + }, + ], + interviewQuestions: [ + { + question: + "Why do language models use subword tokens instead of whole words?", + answer: + "Whole-word vocabularies are unbounded and any unseen word becomes an unknown token. Character vocabularies are tiny but produce very long sequences with little meaning per position. Subword tokenization keeps the vocabulary fixed while still encoding any string, and it lets common words be one token while rare words fall back to shared fragments such as prefixes and suffixes.", + }, + { + question: "Walk through how byte pair encoding is trained.", + answer: + "Start with each word split into base symbols and a frequency count per word. Count all adjacent symbol pairs weighted by frequency, merge the most frequent pair into a new symbol everywhere it occurs, add that symbol to the vocabulary, and record the merge. Repeat until the vocabulary reaches the target size. The ordered merge list is what encoding replays later.", + codeExample: { + language: "javascript", + code: `const merges = []; +while (merges.length < NUM_MERGES) { + const pairs = countPairs(splits, freq); // { "e s": 9, "s t": 9, ... } + const best = argmax(pairs); // ["e", "s"] + if (!best) break; + merges.push(best); + vocab.add(best[0] + best[1]); // "es" + for (const [word, syms] of splits) { + splits.set(word, applyMerge(syms, best)); + } +}`, + }, + }, + { + question: "How does encoding a new string use the learned merges?", + answer: + "Each pre-tokenized piece is split into base symbols, then the encoder repeatedly merges the adjacent pair with the lowest rank in the merge list until no pair has a rule. Applying merges in rank order reproduces the segmentation seen during training. The surviving symbols are mapped to ids through the vocabulary.", + codeExample: { + language: "javascript", + code: `function encodePiece(piece) { + let syms = [...piece]; + while (syms.length > 1) { + const pair = lowestRankPair(syms, ranks); + if (!pair) break; + syms = applyMerge(syms, pair); + } + return syms.map((s) => vocab.get(s)); +}`, + }, + }, + { + question: + "Why does 'hello' tokenize differently from ' hello' and 'Hello'?", + answer: + "The tokenizer works on bytes and has no notion of case or word boundaries beyond what the pre-tokenizer regex encodes. A leading space is part of the byte sequence, so ' hello' is a different string with its own merge path and id. The same holds for a different first byte in 'Hello'. Each variant gets its own embedding and the model learns their relationship only from co-occurrence.", + }, + { + question: + "What is byte fallback and why does it matter for non-English text?", + answer: + "In a byte-level tokenizer the 256 possible byte values are always in the vocabulary, so any UTF-8 input can be encoded without an unknown token. When no merge rules were learned for a script, every byte becomes its own token. A character that needs three bytes in UTF-8 then costs three tokens, which is why the same sentence can be several times more expensive in Georgian or Thai than in English.", + }, + ], + relatedTopicIds: ["embeddings", "attention", "next-token-prediction"], +}; diff --git a/src/content/theory/backend/caching-strategies.ts b/src/content/theory/backend/caching-strategies.ts new file mode 100644 index 0000000..6e2c68f --- /dev/null +++ b/src/content/theory/backend/caching-strategies.ts @@ -0,0 +1,112 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const cachingStrategiesTheory: TopicTheoryContent = { + summary: + "A cache keeps a copy of expensive data somewhere cheaper to read, so repeated requests skip the database or origin. The strategy decides who fills the cache, when it is updated, and what happens when the copy and the source disagree.", + whatItIs: [ + "Every request that reaches a database pays for disk reads, query planning, and a network hop to a machine that all other requests are also hitting. A cache, whether an in-process Map, a Redis instance, or a CDN edge, stores the result of that work so the next request for the same key returns in a millisecond or two instead of a hundred. Caches buy two things at once: lower latency for the caller and lower load on the source.", + "The strategy is the set of rules that connects the cache to the source of truth. Cache-aside puts the application in charge: check the cache, fall back to the database on a miss, write the result back with a TTL. Write-through updates the database and the cache in the same operation, so reads always see the latest write. Write-behind updates the cache immediately and queues the database write, which makes writes fast but leaves a window where the queue is the only copy.", + "Every copy can drift from its source, and the two hard problems follow from that. Invalidation is deciding which keys a write affects and removing them, which requires the writing code to know every cached shape of the data. Stale reads are what happens when invalidation is missed: the cache answers quickly with data the database no longer holds. A TTL bounds how long that can last, which is why even a well-invalidated cache still sets one.", + "Caches are bounded, so a full cache must evict. LRU removes the key that has gone longest without a read, on the bet that recent use predicts future use. Redis approximates it by sampling a few keys rather than tracking a full list, and offers LFU for workloads where one-off reads would otherwise flush the hot set. HTTP caching applies the same ideas one layer out: Cache-Control max-age is the TTL, and an ETag revalidation is the check that a cached copy still matches the origin.", + ], + howItWorks: [ + "Step 1: the application builds a cache key from the request, for example user:42, and asks the cache for it. The key is the only thing the cache and the database share, so it must be derived identically on every read and write path.", + "Step 2: on a hit the cached bytes are deserialized and returned, typically in 1 to 2 ms. The database never sees the request.", + "Step 3: on a miss the application queries the source, serializes the row, and stores it with a TTL such as EX 60. A cold miss costs the cache round trip plus the full query, so it is slightly slower than having no cache.", + "Step 4: when the TTL elapses the cache drops the key. If many requests arrive at that instant they all miss and run the same query, the thundering herd. A per-key lock, jittered TTLs, or serving the stale value while one request revalidates keeps the herd off the database.", + "Step 5: a write updates the source of truth, then either deletes the cached key (cache-aside), overwrites it in the same operation (write-through), or updates it first and queues the source write for a background flush (write-behind).", + "Step 6: when the cache reaches capacity it evicts. An LRU built on a Map deletes and re-inserts a key on every read so the first key in iteration order is always the least recently used, and set removes that key before inserting a new one.", + ], + commonMistakes: [ + { + title: "Writing to the database without invalidating", + explanation: + "The update path was written separately from the read path, so it commits the row and forgets that a cached copy exists. Every read until the TTL expires returns the old value, fast and wrong.", + fix: "Delete the cached key right after the write commits, and prefer delete over overwrite so the next reader rebuilds the entry from the row the database actually has.", + }, + { + title: "Caching without a TTL", + explanation: + "An entry with no expiry survives every missed invalidation forever, and a cache full of keys that are never read again still consumes memory until eviction pressure removes them.", + fix: "Always set a TTL that bounds how stale an entry can be, even when invalidation is in place. Treat the TTL as the safety net, not the primary consistency mechanism.", + }, + { + title: "Using write-behind for data that cannot be lost", + explanation: + "Write-behind confirms the write to the caller before any durable store has seen it. A crash before the queue drains loses every pending write, and once the cache restarts there is no trace of them.", + fix: "Reserve write-behind for counters, view tallies, and session touches. For anything a user would notice losing, use write-through or a durable queue such as Redis Streams or Kafka in front of the flush.", + }, + { + title: "Treating an LRU read as free", + explanation: + "Every hit moves the key to the most recent position. A one-off scan that reads every key once pushes the genuinely hot entries to the least recent end and evicts them on the next insert.", + fix: "Keep bulk scans and exports away from the application cache, size the cache for the working set rather than the dataset, or switch to an LFU policy when frequency predicts reuse better than recency.", + }, + ], + interviewQuestions: [ + { + question: "What is cache-aside and what does a miss cost?", + answer: + "Cache-aside means the application owns the lookup: it checks the cache, queries the database on a miss, and writes the result back with a TTL. A miss costs the cache round trip plus the full query, so a cold cache is marginally slower than no cache. The saving is deferred to every later read that hits.", + codeExample: { + code: `async function getUser(id) { + const key = \`user:\${id}\`; + const cached = await redis.get(key); + if (cached) return JSON.parse(cached); + const row = await db.query('SELECT * FROM users WHERE id = $1', [id]); + await redis.set(key, JSON.stringify(row), 'EX', 60); + return row; +}`, + }, + }, + { + question: + "What is the difference between write-through and write-behind?", + answer: + "Write-through updates the database and the cache in the same request, so writes pay database latency but reads always see the latest value. Write-behind updates the cache and queues the database write for a background flush. Writes return in a couple of milliseconds and bursts can batch, but a crash before the flush loses the queued writes.", + }, + { + question: "Why is cache invalidation considered hard?", + answer: + "The code that writes has to know every cached key the write affects, and those keys may be shaped differently from the row: a user profile, a list page that includes the user, an aggregate count. Missing one leaves a stale copy that is served quickly and confidently until its TTL expires. Deleting the key rather than overwriting it, and keeping the set of cached shapes small, limits the damage.", + }, + { + question: "How would you implement an LRU cache in JavaScript?", + answer: + "Use a Map, which iterates in insertion order. On get, delete and re-insert the key so it becomes the newest entry. On set of a new key when the map is full, delete the first key from map.keys(), which is the least recently used, then insert. Both operations are O(1).", + codeExample: { + code: `class LRUCache { + constructor(capacity) { + this.capacity = capacity; + this.map = new Map(); + } + get(key) { + if (!this.map.has(key)) return undefined; + const value = this.map.get(key); + this.map.delete(key); + this.map.set(key, value); + return value; + } + set(key, value) { + if (this.map.has(key)) this.map.delete(key); + else if (this.map.size >= this.capacity) { + this.map.delete(this.map.keys().next().value); + } + this.map.set(key, value); + } +}`, + }, + }, + { + question: "What is a thundering herd and how do you prevent it?", + answer: + "When a popular key expires, every concurrent request misses at the same moment and all of them run the same query against the database. Mitigations include a per-key lock so only one caller refills while the rest wait, adding jitter to TTLs so keys do not expire together, and serving the stale value while a single request revalidates in the background.", + }, + ], + relatedTopicIds: [ + "database-indexing", + "http-request-lifecycle", + "rate-limiting", + "garbage-collection", + ], +}; diff --git a/src/content/theory/backend/database-indexing.ts b/src/content/theory/backend/database-indexing.ts new file mode 100644 index 0000000..5778da7 --- /dev/null +++ b/src/content/theory/backend/database-indexing.ts @@ -0,0 +1,109 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const databaseIndexingTheory: TopicTheoryContent = { + summary: + "A database index is a separate, sorted data structure that maps column values to the rows that hold them, so a query can jump to matching rows in a handful of page reads instead of scanning the whole table.", + whatItIs: [ + "A table on disk is a heap: rows sit in pages in whatever order they were inserted. Finding every row where email equals a given value means reading every page and comparing every row, and that work grows in direct proportion to the table size. An index is a second structure, maintained alongside the table, that keeps the values of one or more columns in sorted order together with a pointer to each row.", + "Almost every relational database implements the default index as a B-tree. Internal pages hold separator keys and child pointers, leaf pages hold the actual keys with their row pointers (a page number and slot in PostgreSQL, a primary key value in InnoDB secondary indexes), and all leaves sit at the same depth. Because a single 8 KB page holds hundreds of keys, a tree that covers millions of rows is only three or four levels tall, so a lookup costs three or four page reads regardless of how many rows exist.", + "The query planner decides whether to use an index. It keeps statistics per column (number of distinct values, most common values, a histogram) and estimates how many rows a predicate will select. A highly selective predicate on an indexed column is served by an index scan. A predicate that matches a large fraction of the table is often cheaper as a sequential scan, since following thousands of row pointers into random heap pages costs more than reading the pages in order.", + "Indexes are not free. Every INSERT, every DELETE, and every UPDATE that touches an indexed column must also modify each affected index, and a full leaf page has to split, which writes two leaves and updates the parent. Indexes also take disk space and buffer pool memory. The engineering task is to create the few indexes that match the predicates and sort orders queries actually use, and to remove the ones that only slow writes down.", + ], + howItWorks: [ + "Step 1: a query arrives and the planner checks the catalog for indexes whose leading columns match the predicates, join conditions, or ORDER BY of the query.", + "Step 2: for each candidate plan the planner estimates cost from column statistics: how many rows the predicate selects, how many index pages and heap pages that implies, and whether the heap fetches would be random or sequential.", + "Step 3: with an index scan chosen, the executor reads the root page, compares the search key against the separators, and follows one child pointer per level until it reaches a leaf. Each level rules out all but one subtree.", + "Step 4: at the leaf, a binary search over the sorted keys finds the first matching entry. For a range or a prefix match the executor keeps walking forward through the leaf chain while the keys still match.", + "Step 5: each matching entry carries a row pointer, so the executor fetches that heap page and slot to return the full row. If every column the query needs is already in the index, the heap fetch is skipped entirely (an index only scan).", + "Step 6: on writes, the engine inserts the new key into every index in sorted position, splitting a full leaf and promoting a separator to the parent when necessary. The same descent that makes reads cheap makes each write cost one or more extra page writes per index.", + ], + commonMistakes: [ + { + title: "Composite index in the wrong column order", + explanation: + "An index on (last_name, first_name) sorts by last_name first. A filter on first_name alone has no leading prefix to descend by, so the planner cannot use the index and scans the table, even though the column is in the index.", + fix: "Order columns so that the ones used with equality in most queries come first, followed by the column used for a range or sort. Create a second index if two query shapes need different leading columns.", + }, + { + title: "Wrapping the indexed column in a function", + explanation: + "WHERE lower(email) = ... or WHERE date(created_at) = ... compares an expression, not the column the index stores. The planner cannot prove the expression preserves the index order, so the index is ignored.", + fix: "Either rewrite the predicate to compare the raw column, or create an expression index on lower(email) so the stored keys match the predicate exactly.", + }, + { + title: "Adding an index for every column", + explanation: + "Each index is another B-tree to update on every write. A table with ten indexes turns a single row insert into ten index inserts plus occasional page splits, and the extra pages compete for buffer pool memory with the data itself.", + fix: "Add indexes only for predicates and sorts that appear in real query plans, check usage statistics such as pg_stat_user_indexes, and drop indexes that are never scanned.", + }, + { + title: "Expecting an index to help a low selectivity predicate", + explanation: + "A filter like status = 'active' that matches 90 percent of the rows would force the executor to follow a row pointer into a random heap page for nearly every row, which is slower than reading the heap in order. The planner correctly chooses a sequential scan and the index sits unused.", + fix: "Index columns whose values narrow the result down, or use a partial index (WHERE status = 'pending') that only covers the small, frequently queried subset.", + }, + ], + interviewQuestions: [ + { + question: + "Why is a B-tree lookup O(log n) and what does that mean in page reads?", + answer: + "Each internal page holds hundreds of separator keys, so every level divides the remaining search space by hundreds. A tree over a million rows is typically three or four levels tall, so a point lookup reads three or four index pages plus one heap page. A sequential scan on the same table reads every page, tens of thousands of them, and doubles when the table doubles.", + }, + { + question: "What is the leftmost prefix rule for composite indexes?", + answer: + "An index on (a, b, c) can serve predicates on a, on a and b, or on a, b, and c, because the keys are sorted by a first. A predicate on b alone or on c alone cannot descend the tree, since matching values are scattered under every a. Equality on a combined with a range on b still works, but a range on a followed by equality on b only uses the index for a.", + codeExample: { + language: "sql", + code: `CREATE INDEX users_name_idx ON users (last_name, first_name); + +-- uses the index (full key) +SELECT * FROM users WHERE last_name = 'Lovelace' AND first_name = 'Ada'; +-- uses the index (leading prefix) +SELECT * FROM users WHERE last_name = 'Lovelace'; +-- cannot use the index: no leading column +SELECT * FROM users WHERE first_name = 'Ada';`, + }, + }, + { + question: + "What is a covering index and when does the planner use an index only scan?", + answer: + "A covering index contains every column the query reads, either as key columns or as INCLUDE columns. The executor can answer from the leaf entries and skip the heap fetch, as long as the visibility map shows the heap page has no rows that need a visibility check. This removes the random heap reads that make index scans expensive on wide result sets.", + codeExample: { + language: "sql", + code: `CREATE INDEX orders_customer_idx + ON orders (customer_id) INCLUDE (total, created_at); + +-- every referenced column is in the index: Index Only Scan +SELECT total, created_at FROM orders WHERE customer_id = 42;`, + }, + }, + { + question: + "Why might the planner ignore an index that matches the WHERE clause?", + answer: + "The planner compares estimated costs, not just applicability. If the statistics say the predicate selects a large share of the table, the random heap fetches of an index scan cost more than a sequential read of the whole table. Stale statistics, a function around the column, or a type mismatch that forces a cast can also make the index unusable.", + }, + { + question: + "What happens inside a B-tree when a leaf page is full and a new key arrives?", + answer: + "The leaf splits: roughly half its keys stay, the other half move to a newly allocated leaf, and the first key of the new leaf is copied into the parent as a separator. If the parent is also full it splits the same way, and a split of the root adds a new level. This keeps every leaf at the same depth, which is the balance property that guarantees the logarithmic lookup cost.", + codeExample: { + language: "sql", + code: `-- one row, three indexes: one heap write plus three B-tree inserts, +-- each of which may split a full leaf and update its parent +INSERT INTO users (id, email, last_name, first_name) +VALUES (13, 'donald@example.com', 'Knuth', 'Donald');`, + }, + }, + ], + relatedTopicIds: [ + "caching-strategies", + "http-request-lifecycle", + "rate-limiting", + "heap-stack", + ], +}; diff --git a/src/content/theory/backend/http-request-lifecycle.ts b/src/content/theory/backend/http-request-lifecycle.ts new file mode 100644 index 0000000..00cd109 --- /dev/null +++ b/src/content/theory/backend/http-request-lifecycle.ts @@ -0,0 +1,113 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const httpRequestLifecycleTheory: TopicTheoryContent = { + summary: + "The HTTP request lifecycle is everything that happens between a client calling fetch and the response body landing in its hands: DNS resolution, the TCP and TLS handshakes, the request on the wire, the server's middleware and handler, and the response coming back over a connection that usually stays open for the next call.", + whatItIs: [ + "HTTP is a text-based request and response protocol layered on top of a reliable byte stream. A request is a method, a path, a version, a set of headers, and an optional body. A response is a status line, headers, and an optional body. Nothing about HTTP itself says how those bytes reach the other machine, which is why a single fetch call involves several lower layers before any HTTP text is exchanged.", + "Before the first byte of HTTP can be sent the client needs an IP address (DNS), a connection (the TCP three-way handshake), and for https an encrypted channel with a verified peer (the TLS handshake). Each of these costs at least one network round trip, so on a 30 ms link a cold request spends roughly 20 ms on DNS, 30 ms on TCP, and 30 ms on TLS 1.3 before the request line even leaves the machine. Warm connections skip all three, which is why keep-alive and connection pooling matter so much.", + "On the server the request is parsed into an object and passed through an ordered chain of functions. In Express-style frameworks these are middleware: each one can read or modify the request, end the response, or call next() to pass control on. The router is one of those functions; it matches the method and path pattern, fills route params, and invokes the handler. Handlers are usually async because they wait on a database or another service, and that await suspends only that one request while the event loop keeps serving others.", + "The response carries a status code that classifies the outcome (2xx success, 3xx redirect, 4xx client error, 5xx server error) and headers that tell the client how to treat the body: its type, its length, whether it may be cached, and whether the connection can be reused. The client's promise resolves as soon as the headers are in; reading the body is a second asynchronous step because the body may still be streaming.", + ], + howItWorks: [ + "Step 1: the client parses the URL into scheme, host, port, and path, then resolves the host to an IP address. Browser and OS caches are checked first; on a miss a recursive resolver answers with an A or AAAA record and a TTL that controls how long the answer may be reused.", + "Step 2: the client opens a TCP connection with SYN, SYN-ACK, ACK. This costs one round trip and gives HTTP an ordered, reliable byte stream. HTTP/3 replaces TCP with QUIC over UDP to fold this handshake into the TLS one.", + "Step 3: for https the client sends a TLS ClientHello with its supported ciphers, an ephemeral key share, the server name (SNI), and ALPN protocol preferences. The server replies with its choice, its certificate chain, and its own key share. The client verifies the chain, both sides derive session keys, and TLS 1.3 completes this in one round trip where TLS 1.2 needed two.", + "Step 4: the client writes the request line and headers into the encrypted stream. Host is required in HTTP/1.1 because many sites share one IP. Over HTTP/2 the same connection carries many requests at once as interleaved streams; over HTTP/1.1 a connection handles one request at a time and browsers open up to six per host.", + "Step 5: the server's listener accepts the bytes, parses them into a request object, and walks the middleware chain: logging, body parsing, authentication, then the router. The matched handler does its work, typically awaiting a database query while the event loop handles other connections, then serialises a result and writes status, headers, and body.", + "Step 6: the client receives the status line and headers, resolves the fetch promise with a Response, and streams the body. If the response says keep-alive the socket stays open for the next request; if the status is a redirect the client may follow it automatically; if Cache-Control allows, a future identical request may not touch the network at all.", + ], + commonMistakes: [ + { + title: "Treating a 4xx or 5xx response as a rejected promise", + explanation: + "fetch only rejects on network failures such as DNS errors, refused connections, or CORS blocks. A 404 or 500 is a successfully delivered HTTP response, so the promise resolves and code that relies on catch alone silently continues with an error body.", + fix: "Check res.ok or res.status after every fetch and throw or branch explicitly. Keep catch for transport failures, which are a different class of problem and usually deserve a different retry strategy.", + }, + { + title: "Forgetting to call next() or ending the response twice", + explanation: + "A middleware that neither ends the response nor calls next() leaves the request hanging until the client times out. The opposite mistake, writing to the response and then also falling through to another res.send, throws ERR_HTTP_HEADERS_SENT because headers were already flushed with the first body chunk.", + fix: "Every middleware path must do exactly one of two things: end the response, or call next(). Use return res.status(...).json(...) so the function exits after responding, and pass errors with next(err) instead of throwing into the void.", + }, + { + title: "Blocking the event loop inside a handler", + explanation: + "An awaited database call costs the server almost nothing because the handler is suspended while I/O happens elsewhere. Synchronous CPU work such as a large JSON.parse, a tight loop, or a sync file read blocks the single thread, so every other in-flight request stalls until it finishes.", + fix: "Keep handlers I/O-bound and asynchronous. Move heavy CPU work to a worker thread, a queue, or a separate service, and measure event loop lag in production so a regression shows up as latency on unrelated endpoints.", + }, + { + title: "Opening a fresh connection for every request", + explanation: + "DNS, TCP, and TLS together cost two to three round trips before any HTTP bytes flow. Clients that disable keep-alive, or servers that send Connection: close, pay that price on every call and add load to the TLS termination layer.", + fix: "Leave keep-alive on, reuse a single agent or client instance with a connection pool for server-to-server calls, and prefer HTTP/2 where the origin supports it so many requests share one handshake.", + }, + ], + interviewQuestions: [ + { + question: + "What happens between calling fetch and the promise resolving on a cold connection?", + answer: + "The browser parses the URL, resolves the hostname through DNS, opens a TCP connection with a three-way handshake, negotiates TLS (one round trip for 1.3), sends the request line and headers, waits for the server to process the request, and resolves the promise as soon as the response status and headers have arrived. The body is read afterwards through a separate promise such as res.json().", + }, + { + question: + "Why does an await inside an Express handler not block other requests?", + answer: + "The await suspends only that handler's function. The database driver has written the query to a socket and registered a callback; control returns to the event loop, which keeps accepting and serving other connections. When the reply arrives, the promise resolves and the handler continuation runs as a microtask. Only synchronous CPU work blocks the loop.", + codeExample: { + language: "javascript", + code: `app.get('/products/:id', async (req, res) => { + // Suspends this handler only; the event loop stays free. + const product = await db.query( + 'SELECT * FROM products WHERE id = $1', + [req.params.id], + ); + res.status(200).json(product); +});`, + }, + }, + { + question: + "What is the difference between a 401 and a 403, and between a 301 and a 302?", + answer: + "401 Unauthorized means the request lacks valid credentials and should include a WWW-Authenticate header telling the client how to retry. 403 Forbidden means the credentials were fine but the action is not allowed. 301 Moved Permanently tells clients the resource has a new address and may be cached, so future requests skip the old URL. 302 Found is temporary and must not be cached; 307 and 308 are the variants that guarantee the method and body are preserved on the follow-up request.", + }, + { + question: + "How does Express route an error to the error-handling middleware?", + answer: + "Calling next(err) with an argument tells Express to skip every remaining normal middleware and route handler and jump to the first middleware declared with four parameters (err, req, res, next). That handler logs the error and sends a response, usually a generic 500 so internal details never reach the client. In Express 5 a rejected promise from an async handler is forwarded to next automatically.", + codeExample: { + language: "javascript", + code: `app.get('/products/:id', async (req, res, next) => { + try { + const product = await db.findProduct(req.params.id); + if (!product) return res.status(404).json({ error: 'Not found' }); + res.json(product); + } catch (err) { + next(err); // jumps to the 4-arity handler below + } +}); + +app.use((err, req, res, next) => { + console.error(err); + res.status(500).json({ error: 'Internal Server Error' }); +});`, + }, + }, + { + question: + "What does keep-alive change about the cost of a second request, and how does HTTP/2 go further?", + answer: + "With keep-alive the TCP and TLS session stay open after the first response, so a second request to the same host skips DNS, the TCP handshake, and the TLS handshake and pays only one round trip plus server time. HTTP/2 multiplexes many requests as interleaved streams over that single connection, removing head-of-line blocking at the HTTP layer and the need for six parallel connections per host.", + }, + ], + relatedTopicIds: [ + "caching-strategies", + "jwt-authentication", + "rate-limiting", + "event-loop", + "promises", + ], +}; diff --git a/src/content/theory/backend/jwt-authentication.ts b/src/content/theory/backend/jwt-authentication.ts new file mode 100644 index 0000000..aa9e48f --- /dev/null +++ b/src/content/theory/backend/jwt-authentication.ts @@ -0,0 +1,111 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const jwtAuthenticationTheory: TopicTheoryContent = { + summary: + "A JSON Web Token is a signed, base64url-encoded set of claims that a server issues at login and verifies on every later request without a database lookup, so authentication state lives in the token instead of in a session store.", + whatItIs: [ + "After a user proves who they are with a password, the server needs a way to recognise them on the next request. A JWT solves this by handing the client a small string that says who the user is and until when, plus a signature that lets the server confirm it wrote that string. The client sends the string back with each request and the server checks the signature instead of asking a database.", + "A JWT (RFC 7519) has three parts separated by dots: a header that names the signing algorithm, a payload of claims such as sub (subject), iat (issued at) and exp (expires at), and a signature. The first two parts are JSON encoded with base64url, which is ordinary base64 with a URL-safe alphabet and no padding. For HS256 the signature is HMAC-SHA256 over the string header.payload, keyed with a secret only the server holds.", + "Signing proves integrity and origin: if any byte of the header or payload changes, the recomputed HMAC no longer matches, and nobody without the secret can produce a matching one. It does not provide secrecy. The payload is readable by anyone holding the token, so it must never contain passwords, secrets or personal data you would not print in a log. Asymmetric algorithms such as RS256 sign with a private key and verify with a public one, which lets other services verify tokens without being able to mint them.", + "The trade-off of stateless verification is that the server cannot forget a token it already signed. An access token stays valid until exp, whatever happens in between. Production systems therefore keep access tokens short, on the order of minutes, and pair them with a long-lived refresh token that is stored server side and can be rotated and revoked.", + ], + howItWorks: [ + "Step 1: the client sends credentials to a login endpoint. The server looks the user up, compares the password with the stored hash, and builds the claims: sub, any application data such as a role, iat set to the current Unix time in seconds, and exp a fixed interval later.", + "Step 2: the server base64url-encodes the JSON header and the JSON payload, computes HMAC-SHA256 over header.payload with its secret, base64url-encodes the digest, and joins the three parts with dots. It returns the token and stores nothing about it.", + "Step 3: on a protected request the client presents the token, usually as Authorization: Bearer . Middleware splits the token on the dots and recomputes the HMAC over the first two parts with the same secret, always using the algorithm the server expects rather than the one named in the header.", + "Step 4: the recomputed digest is compared with the presented one using a constant-time comparison so response timing does not leak which byte differed. A mismatch means the token was forged or edited and the request is rejected with 401 before any claim is read.", + "Step 5: once the signature passes, the payload is decoded and the time claims are checked: exp must be in the future and nbf, if present, must be in the past. A token whose signature is valid but whose exp has passed is rejected as expired.", + "Step 6: the verified claims are attached to the request, for example as req.user, and the route handler uses them for authorization. When the access token expires, the client presents its refresh token, the server checks that token against its allow-list, rotates it, and issues a new access token; logout revokes the refresh token.", + ], + commonMistakes: [ + { + title: "Treating the payload as private", + explanation: + "Base64url looks opaque, so developers put email addresses, internal ids, or even password hashes in the payload. Anyone who intercepts or is handed the token can decode it with a single function call.", + fix: "Put only what the server needs to authorize the request in the claims, treat every claim as public, and use encryption (JWE) or a server-side lookup if data must stay secret.", + }, + { + title: "Letting the header choose the algorithm", + explanation: + "A verifier that reads alg from the token and dispatches on it can be tricked with alg none, which skips signing, or with an RS256 public key reused as an HS256 secret. Both let an attacker forge tokens the server accepts.", + fix: "Hard-code the expected algorithm and key in the verifier and reject any token whose header does not match it.", + }, + { + title: "Expecting logout to invalidate access tokens", + explanation: + "Because verification is stateless, a signed access token keeps working until exp even after the user logs out or has their permissions removed. Long-lived access tokens turn this into a real security gap.", + fix: "Keep access tokens short, revoke the refresh token on logout, and for sensitive actions re-check the database or a denylist keyed by the jti claim.", + }, + { + title: "Storing tokens in localStorage without weighing XSS", + explanation: + "localStorage is readable by any script running on the page, so a single XSS vulnerability hands the attacker a valid token. httpOnly cookies avoid that but are sent automatically, which reopens CSRF.", + fix: "Keep the access token in memory and the refresh token in an httpOnly, secure, sameSite cookie scoped to the refresh path, and add CSRF protection wherever cookies carry auth.", + }, + ], + interviewQuestions: [ + { + question: "What are the three parts of a JWT and what does each contain?", + answer: + "The header is JSON naming the algorithm and token type, such as alg HS256 and typ JWT. The payload is JSON holding claims like sub, iat, exp and application data. The signature is a MAC or digital signature over the base64url-encoded header and payload joined by a dot. All three are base64url-encoded and joined with dots.", + codeExample: { + language: "javascript", + code: `const { createHmac } = require("crypto"); +const b64url = (s) => Buffer.from(s).toString("base64url"); + +function sign(payload, secret) { + const header = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" })); + const body = b64url(JSON.stringify(payload)); + const sig = createHmac("sha256", secret) + .update(\`\${header}.\${body}\`) + .digest("base64url"); + return \`\${header}.\${body}.\${sig}\`; +}`, + }, + }, + { + question: "Does a JWT encrypt its contents?", + answer: + "No. A signed JWT (JWS) is encoded, not encrypted, and anyone holding it can decode the claims. The signature only guarantees the claims were not altered and were produced by a holder of the key. Confidential data needs JWE or must stay on the server.", + }, + { + question: + "How does the server verify a token, and why does it compare signatures in constant time?", + answer: + "It splits the token, recomputes the HMAC over header.payload with its own secret and algorithm, and compares the result with the presented signature. A naive string comparison returns as soon as one byte differs, so an attacker can measure response times to discover the correct signature byte by byte. Comparing all bytes regardless of where they differ removes that side channel.", + codeExample: { + language: "javascript", + code: `function verify(token, secret) { + const [header, body, sig] = token.split("."); + const expected = createHmac("sha256", secret) + .update(\`\${header}.\${body}\`) + .digest("base64url"); + if (expected.length !== sig.length || + !timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) { + throw new Error("invalid signature"); + } + const payload = JSON.parse(Buffer.from(body, "base64url").toString()); + if (payload.exp <= Math.floor(Date.now() / 1000)) throw new Error("token expired"); + return payload; +}`, + }, + }, + { + question: + "What is the difference between HS256 and RS256, and when would you pick each?", + answer: + "HS256 uses one shared secret for both signing and verification, so every service that verifies tokens could also forge them. RS256 signs with a private key and verifies with the matching public key, so verification can be spread across services or third parties without sharing signing power. HS256 is simpler and faster for a single backend; RS256 fits multi-service or federated setups.", + }, + { + question: + "Why use refresh tokens if access tokens already carry an expiry?", + answer: + "An access token cannot be revoked before exp because the server holds no state for it, so it must be short-lived, which would force frequent logins on its own. A refresh token is long-lived but stored server side, so it can be checked, rotated on every use, and revoked at logout or on suspected theft. The pair gives fast stateless checks on most requests and a revocable anchor for the session.", + }, + ], + relatedTopicIds: [ + "http-request-lifecycle", + "rate-limiting", + "caching-strategies", + ], +}; diff --git a/src/content/theory/backend/rate-limiting.ts b/src/content/theory/backend/rate-limiting.ts new file mode 100644 index 0000000..f867a62 --- /dev/null +++ b/src/content/theory/backend/rate-limiting.ts @@ -0,0 +1,104 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const rateLimitingTheory: TopicTheoryContent = { + summary: + "Rate limiting caps how many requests a client may make in a period of time. The server tracks usage per client key, accepts requests inside the budget, and answers the rest with 429 Too Many Requests and a Retry-After header.", + whatItIs: [ + "An API without a rate limit is only as available as its most aggressive caller. A buggy retry loop, a scraper, or a credential stuffing attack can consume all of the database connections and CPU that every other client depends on. Rate limiting gives each client a budget and enforces it before the request reaches expensive code.", + "Every limiter answers the same question, has this key made too many requests recently, but they differ in what they store. A fixed window keeps one counter per clock-aligned window. A token bucket keeps a fractional token count and a timestamp, refilling continuously and allowing bursts up to the capacity. A sliding window log keeps the timestamp of each accepted request and counts the ones inside the last N seconds, which is exact but costs one entry per request.", + "The key decides what is being protected. Limiting by IP address is the default at the edge and stops anonymous floods, but shared NATs and mobile carriers put thousands of users behind one address. Limiting by API key or user id after authentication is fairer and lets you sell tiers. Many systems apply both, a loose IP limit at the load balancer and a per-account limit in the application.", + "The state has to be shared. A Map inside one Node process only limits that process, so three instances behind a load balancer silently triple the budget. Production limiters keep counters in Redis and run the check and the update as one atomic operation, either INCR with EXPIRE or a short Lua script, so two instances cannot both read 2 and both accept when the limit is 3.", + ], + howItWorks: [ + "Step 1: the middleware derives a key from the request, typically the client IP, the API key, or the authenticated user id, sometimes combined with the route so a login endpoint has a tighter budget than a read endpoint.", + "Step 2: it loads the state for that key. For a token bucket that is the token count and the last refill time, for a fixed window it is the count and the window start, for a sliding log it is the list of recent timestamps.", + "Step 3: it brings the state up to the current time. The bucket adds elapsed seconds times the refill rate, capped at capacity. The fixed window resets the count if the clock has crossed into a new window. The sliding log drops timestamps older than now minus the window.", + "Step 4: it decides. If a token is available, or the count is under the limit, or fewer than the limit timestamps remain, the request is accepted and the state is updated (a token spent, the count incremented, the timestamp pushed). Otherwise the request is rejected and the state is left as it is, so rejected requests do not extend the lockout.", + "Step 5: the state is written back atomically. In Redis the load, update, and store happen inside one command or one Lua script so concurrent requests from other instances cannot interleave between the read and the write.", + "Step 6: the response carries the verdict. Accepted requests continue to the handler with X-RateLimit-Limit and X-RateLimit-Remaining headers. Rejected requests get 429 and a Retry-After value in whole seconds, computed from when the next token or slot becomes available and rounded up.", + ], + commonMistakes: [ + { + title: "Counting the window from a clock boundary", + explanation: + "A fixed window resets at t=10, t=20, and so on, so a client can send its full budget at t=9.9 and again at t=10.1. Two adjacent windows spent at the boundary give twice the nominal rate over a short interval.", + fix: "Use a sliding window log when exactness matters, or the sliding window counter approximation, prev * (1 - position) + current, when you want two integers per key and can tolerate a small error at the edges.", + }, + { + title: "Keeping limiter state in process memory", + explanation: + "A Map in a Node process is invisible to the other instances behind the load balancer. Each instance enforces its own copy of the limit, so the effective limit is multiplied by the instance count and resets on every deploy.", + fix: "Store the counter or bucket in Redis and perform the read, update, and write in one atomic step: INCR plus EXPIRE for fixed windows, or a Lua script for token buckets.", + }, + { + title: "Recording rejected requests in the log", + explanation: + "If a sliding log pushes the timestamp of every request, including rejected ones, a client that keeps retrying keeps pushing its own unlock time further away and can never recover, and the log grows without bound under attack.", + fix: "Only accepted requests count toward the limit. Push the timestamp inside the accepted branch, and prune expired entries on every call so the log never exceeds the limit in length.", + }, + { + title: "Returning 429 without Retry-After", + explanation: + "A bare 429 tells the client to stop but not for how long, so SDKs fall back to guessing, usually exponential backoff starting from a fixed value. That wastes capacity when the lockout is short and hammers the server when it is long.", + fix: "Compute the seconds until the next token or slot, round up, and send it as Retry-After. Add X-RateLimit-Remaining on every response so well-behaved clients can slow down before they hit the wall.", + }, + ], + interviewQuestions: [ + { + question: + "What is the difference between a token bucket and a fixed window limiter?", + answer: + "A fixed window counts requests per clock-aligned interval and resets the count at the boundary, which is cheap but lets a client double its budget by straddling the reset. A token bucket refills continuously at a fixed rate up to a capacity, so it allows a burst up to the capacity while capping the long-run average, and it has no boundary to exploit.", + codeExample: { + language: "javascript", + code: `function allow(bucket, now, capacity = 5, ratePerSec = 1) { + const elapsed = (now - bucket.last) / 1000; + bucket.tokens = Math.min(capacity, bucket.tokens + elapsed * ratePerSec); + bucket.last = now; + if (bucket.tokens < 1) return false; + bucket.tokens -= 1; + return true; +}`, + }, + }, + { + question: "How do you rate limit across multiple server instances?", + answer: + "Keep the state in a shared store, almost always Redis, and make the check and update atomic. For a fixed window, INCR the key for the current window and set EXPIRE on the first hit; the returned value tells you whether the request is over the limit. For a token bucket, run the refill and spend in a Lua script so no other instance can interleave between the read and the write.", + codeExample: { + language: "javascript", + code: `const key = \`rl:\${ip}:\${Math.floor(Date.now() / 10_000)}\`; +const count = await redis.incr(key); +if (count === 1) await redis.expire(key, 10); +if (count > 3) { + res.set("Retry-After", "10"); + return res.status(429).end(); +} +next();`, + }, + }, + { + question: + "What does a 429 response mean and which headers should accompany it?", + answer: + "429 Too Many Requests means the client has exceeded its budget and the request was not processed. Retry-After tells the client how many seconds to wait, rounded up. X-RateLimit-Limit and X-RateLimit-Remaining, sent on every response, let clients pace themselves before they are rejected. The newer RateLimit-Policy header from the IETF draft standardizes the same information.", + }, + { + question: "What should you use as the rate limit key?", + answer: + "It depends on what you are protecting. IP address works for anonymous traffic at the edge but punishes users behind a shared NAT. After authentication, the API key or user id is fairer and supports per-tier limits. Sensitive endpoints such as login often combine keys, for example a per-IP limit and a per-account limit, so neither a distributed attack nor a single abusive account gets through.", + }, + { + question: + "How does server-side rate limiting differ from debounce and throttle on the client?", + answer: + "Debounce and throttle run in the browser and reduce how often a cooperative client fires a call, which improves UX and saves bandwidth. They cannot protect the server, because an attacker controls the client code. Rate limiting runs on the server or at the edge and enforces the budget regardless of the client. Use throttle for typing and scrolling, and a server limiter for the API.", + }, + ], + relatedTopicIds: [ + "http-request-lifecycle", + "caching-strategies", + "jwt-authentication", + "debounce-throttle", + ], +}; diff --git a/src/content/theory/frameworks/angular-change-detection.ts b/src/content/theory/frameworks/angular-change-detection.ts new file mode 100644 index 0000000..173903c --- /dev/null +++ b/src/content/theory/frameworks/angular-change-detection.ts @@ -0,0 +1,114 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const angularChangeDetectionTheory: TopicTheoryContent = { + summary: + "Change detection is the process Angular runs to find template bindings whose values changed and write the new values to the DOM. How it is triggered and how much of the tree it visits depends on whether the app uses zone.js, OnPush, or signals.", + whatItIs: [ + 'Angular templates contain bindings such as {{ qty }} or [items]="items". Angular does not know when the underlying values change, so it periodically re-evaluates the bindings, compares each result with the value it stored last time, and updates the DOM only where the result differs. This comparison is called dirty checking, and a full run of it is a change detection pass.', + "In a classic application the pass is triggered by zone.js. zone.js monkey-patches browser APIs such as addEventListener, setTimeout, Promise.then and XMLHttpRequest so that every callback runs inside an Angular zone. When the callback finishes and the microtask queue drains, NgZone emits onMicrotaskEmpty and ApplicationRef.tick() walks the whole component tree from the root, checking every binding in every Default-strategy component whether or not anything relevant changed.", + "ChangeDetectionStrategy.OnPush lets a component opt out of that blanket check. An OnPush view is skipped unless something marks it dirty: an @Input received a new reference (compared with Object.is), a template event fired inside the view, an async pipe emitted, or ChangeDetectorRef.markForCheck() was called. Marking a view also marks its ancestors, so the next tick can reach it without checking the siblings along the way.", + "Signals make the trigger precise instead of coarse. A template that reads a signal registers itself as a consumer in a reactive graph. Writing the signal marks its consumers dirty, which flags exactly those views for refresh and asks the scheduler for a tick. In a zoneless app (provideZonelessChangeDetection) that notification is the only trigger, so the traversal visits the flagged views and passes through their ancestors without evaluating anything else.", + ], + howItWorks: [ + "Step 1: something notifies Angular. With zone.js it is a patched async callback finishing; with OnPush it is an input reference change, a template event, an async pipe, or markForCheck(); with signals it is a signal write that marks a consuming view dirty.", + "Step 2: the scheduler coalesces notifications from the same turn into one ApplicationRef.tick(). The zoneless scheduler races setTimeout against requestAnimationFrame and runs on whichever fires first.", + "Step 3: tick() starts at the root view and walks children in template order. A Default view is always checked. An OnPush view is checked only when its dirty flag is set. A view that only has HasChildViewsToRefresh is passed through without evaluating its own bindings.", + "Step 4: checking a view runs its template update function. Each binding expression is re-evaluated and compared with the stored value using Object.is; on a difference the DOM property or text node is written and the stored value is replaced.", + "Step 5: input bindings on child components are compared the same way. A changed reference calls the input setter, fires ngOnChanges, and marks an OnPush child dirty so the traversal descends into it.", + "Step 6: in development mode tick() runs a second pass, checkNoChanges(), that re-evaluates every binding without writing. Any value that differs from the first pass throws ExpressionChangedAfterItHasBeenCheckedError, which reveals bindings that mutate state during rendering.", + ], + commonMistakes: [ + { + title: "Mutating an OnPush input in place", + explanation: + "Pushing into an array or changing a field on an object keeps the same reference, so the parent's input binding compares equal and the OnPush child is never marked dirty. The model changes, the DOM does not.", + fix: "Treat inputs as immutable and produce a new reference for every change (spread, map, filter). If the change originates inside the component, call ChangeDetectorRef.markForCheck() or store the value in a signal.", + }, + { + title: "Expecting third-party callbacks to trigger a check", + explanation: + "Code that runs outside the Angular zone, such as a callback from a library that captured the native setTimeout before zone.js patched it, or anything scheduled with NgZone.runOutsideAngular, finishes without notifying NgZone, so no tick follows.", + fix: "Wrap the callback in NgZone.run(), or in a zoneless or signals-based design write the result into a signal so the consuming view is marked dirty explicitly.", + }, + { + title: "Changing state during change detection", + explanation: + "Setting a parent's value from a child's ngAfterViewInit, or calling a method with side effects from a template, changes a binding that has already been checked in the same pass. Dev mode reports it as ExpressionChangedAfterItHasBeenCheckedError; production silently renders stale output.", + fix: "Move the update to a point before the check (ngOnInit, a signal computed, or an effect) rather than working around the error with setTimeout or detectChanges().", + }, + { + title: + "Reading a signal outside a reactive context and expecting updates", + explanation: + "Calling count() once inside a constructor or a plain method copies the value; it does not subscribe anything. Only reads inside a template, computed, or effect create graph edges.", + fix: "Read signals where the value is consumed: in the template, in a computed() that the template reads, or in an effect() when you need a side effect.", + }, + ], + interviewQuestions: [ + { + question: "What does zone.js do for Angular, and what does it not do?", + answer: + "zone.js patches async browser APIs so that callbacks run inside an Angular zone and Angular is notified when they finish. That notification triggers ApplicationRef.tick(). zone.js does not know what the callback changed, which is why the default pass checks every binding in every Default component.", + }, + { + question: "When is an OnPush component checked?", + answer: + "When one of its @Input references changes (Object.is comparison), when a template event listener inside it fires, when an async pipe in its template emits, when markForCheck() is called, or when a signal read in its template is written. Otherwise the whole subtree is skipped during the pass.", + codeExample: { + language: "typescript", + code: `@Component({ + selector: 'app-list', + changeDetection: ChangeDetectionStrategy.OnPush, + template: \`@for (it of items; track it.id) { + + }\`, +}) +export class ListComponent { + @Input() items: Item[] = []; +} + +// parent: same reference, List is skipped +this.items[0].qty++; +// parent: new reference, List is marked dirty +this.items = this.items.map((it, i) => + i === 0 ? { ...it, qty: it.qty + 1 } : it);`, + }, + }, + { + question: + "What is ExpressionChangedAfterItHasBeenCheckedError and why does it only appear in development?", + answer: + "After the real pass, dev mode runs checkNoChanges(), which re-evaluates every binding without writing to the DOM. If any expression now returns a different value, some code changed state during the pass, and Angular throws to surface it. Production skips the verification pass for performance, so the same bug renders stale output instead of throwing.", + }, + { + question: + "How do signals make change detection local rather than global?", + answer: + "Reading a signal in a template records a producer to consumer edge. Writing the signal marks its consumers dirty, which flags the exact views that read it for refresh and marks their ancestors for traversal only. The next tick descends through those ancestors without evaluating their bindings and refreshes only the flagged views, so the work is proportional to the readers of the changed value.", + codeExample: { + language: "typescript", + code: `export class ItemComponent { + count = signal(0); + double = computed(() => this.count() * 2); + add() { this.count.set(this.count() + 1); } +} +// template: {{ double() }} +// count.set(1) -> double dirty -> this view flagged +// double recomputes lazily during the refresh: 1 * 2 = 2`, + }, + }, + { + question: + "What changes when an app switches to provideZonelessChangeDetection()?", + answer: + "zone.js is no longer loaded, so finishing an async task does not trigger anything. Change detection runs only when Angular is told explicitly: a signal read in a template changes, a template event fires, markForCheck() is called, an async pipe emits, or ComponentRef.setInput() is used. Code that relied on a plain setTimeout or a manual property assignment to refresh the view stops working until it goes through one of those paths.", + }, + ], + relatedTopicIds: [ + "vue-reactivity", + "svelte-runes", + "render-cycle", + "memoization", + "fiber-tree", + ], +}; diff --git a/src/content/theory/frameworks/svelte-runes.ts b/src/content/theory/frameworks/svelte-runes.ts new file mode 100644 index 0000000..3ef856d --- /dev/null +++ b/src/content/theory/frameworks/svelte-runes.ts @@ -0,0 +1,99 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const svelteRunesTheory: TopicTheoryContent = { + summary: + "Runes are the Svelte 5 reactivity primitives. $state creates a signal, $derived creates a lazy memoized computation, and $effect subscribes to whatever it reads, while the compiler turns each template expression into a direct DOM write with no virtual DOM in between.", + whatItIs: [ + "In Svelte 4, reactivity was tied to assignment inside a component: the compiler instrumented every `x = ...` statement it could see. That worked inside a .svelte file and broke as soon as state moved into a plain module or a function. Runes replace that with explicit primitives that behave the same everywhere: $state, $derived and $effect are compiler keywords that expand into calls to a small signal runtime.", + "A $state value is a source signal: an object holding the current value, a version number, and a list of reactions that read it. $derived creates a derived signal that stays unevaluated until something reads it, then caches the result and records which sources it touched. $effect creates an effect that runs after the DOM has been updated and re-runs whenever one of its dependencies changes. Dependencies are never declared; they are collected by tracking reads while the function runs.", + "The graph is push-pull. A write to a source pushes a dirty flag along its recorded edges: deriveds that read it become dirty, and reactions of those deriveds become maybe-dirty, since a derived may recompute to an equal value. Nothing recomputes at write time. A microtask flush then runs the queued effects, and each effect pulls fresh values as it reads them, which is what finally recomputes a dirty derived. Equal results stop propagation early.", + "The other half is the compiler. Because markup is static, Svelte knows at build time that a given text node depends on a given expression. It emits a template string that is cloned into real DOM once, then a template effect per dynamic expression that calls set_text or set_attribute on the exact node it owns. There is no tree of virtual nodes to build on every update and no diff to find what changed, because the dependency between signal and node was resolved before the code ran.", + ], + howItWorks: [ + "Step 1: the compiler rewrites `let count = $state(0)` to `let count = $.state(0)`, and every read of count to `$.get(count)` and every write to `$.set(count, ...)`. For objects and arrays, $state wraps the value in a Proxy that lazily creates one source per property on first read.", + "Step 2: markup becomes `$.template('')`. On mount the template is cloned with cloneNode, and the compiler emits `$.child` and `$.sibling` calls that grab references to the nodes that hold dynamic expressions.", + "Step 3: each dynamic expression becomes a `$.template_effect`. It runs immediately: reading a signal inside it registers the effect as a reaction of that signal, and reading an unread $derived computes it now and links it to the sources it touched. The effect writes the result into its node.", + "Step 4: user effects created with $effect are scheduled to run after mount, in a microtask, once the DOM is in place. They subscribe to whatever they read.", + "Step 5: a write such as `count++` compiles to `$.update(count)`. It bumps the source version, marks direct reactions dirty and reactions-of-deriveds maybe-dirty, and queues one flush. Multiple writes in the same tick share that flush.", + "Step 6: the flush runs template effects first, then user effects. A maybe-dirty effect first checks whether its derived dependencies actually changed version; if not, it is skipped. Each running effect re-collects its dependencies from scratch, so the graph always reflects the last run.", + ], + commonMistakes: [ + { + title: "Destructuring $state and expecting it to stay reactive", + explanation: + "`const { done } = todos[0]` copies the value out of the proxy. The compiler can only rewrite reads of the variable itself, so the copy is a plain boolean that never updates.", + fix: "Read through the proxy where the value is used, or wrap the read in $derived if you want a named reactive alias: `const done = $derived(todos[0].done)`.", + }, + { + title: "Mutating $state.raw and waiting for an update", + explanation: + "$state.raw stores the object without a Proxy, so `raw.n = 1` is an ordinary property write that no trap observes. The source is only written on reassignment.", + fix: "Reassign the whole value (`raw = { ...raw, n: 1 }`) or use plain $state when nested mutation needs to be tracked. Reserve $state.raw for large or immutable data.", + }, + { + title: "Using $effect to derive state", + explanation: + "Writing `$effect(() => { double = count * 2 })` works but runs after the DOM update, so the first render sees the stale value, and it turns a pure computation into a side effect that can chain into further flushes.", + fix: "Use $derived for values computed from other state. It is lazy, memoized, and consistent within the same flush. Keep $effect for real side effects such as logging, timers or imperative DOM APIs.", + }, + { + title: "Reading state after a write and expecting the DOM to match", + explanation: + "Signals update synchronously but effects, including template effects, run in a later microtask. Reading `element.textContent` right after `count++` returns the old text.", + fix: "Await `tick()` from svelte before reading the DOM, or move the DOM read into an $effect that depends on the same state.", + }, + ], + interviewQuestions: [ + { + question: "What is the difference between $state and $derived?", + answer: + "$state creates a source signal that you write to directly. $derived creates a derived signal computed from other signals; it is lazy, so the expression does not run until something reads it, and memoized, so repeated reads return the cached value until a dependency changes. You never assign to a $derived.", + codeExample: { + code: `let count = $state(0); +let double = $derived(count * 2); + +// double is not computed yet +count = 5; +console.log(double); // computes now, 10 +console.log(double); // cached, no recompute`, + }, + }, + { + question: "Why does Svelte 5 not need a virtual DOM?", + answer: + "The compiler sees the markup at build time, so it knows which text node or attribute depends on which expression. It emits one template effect per expression that writes directly to that node with set_text or set_attribute. A signal write marks only those effects dirty, so there is no need to rebuild a tree and diff it to discover what changed.", + }, + { + question: "What does push-pull mean in the signal graph?", + answer: + "A write pushes dirty flags along the recorded dependency edges without recomputing anything. When the flush runs an effect, the effect pulls values by reading them, and a dirty derived recomputes at that moment. This keeps writes cheap and avoids computing values nobody reads.", + }, + { + question: + "When does an $effect run relative to the DOM update, and how does it know its dependencies?", + answer: + "Effects run in a microtask after template effects have updated the DOM, so DOM reads inside them see the new state. Dependencies are collected by tracking every signal read while the callback runs, and they are re-collected on each run, so conditional reads change the subscription set.", + codeExample: { + code: `let open = $state(false); +let name = $state('a'); + +$effect(() => { + // subscribes to name only while open is true + if (open) console.log(name); +});`, + }, + }, + { + question: + "How does $state track nested writes like todos[0].done = true?", + answer: + "$state wraps objects and arrays in a Proxy. The get trap lazily creates a source signal per property the first time it is read and nests another proxy for object values. The set trap writes that per-property source, so only reactions that read todos[0].done are marked dirty; the array length and other items are unaffected.", + }, + ], + relatedTopicIds: [ + "vue-reactivity", + "angular-change-detection", + "virtual-dom", + "memoization", + ], +}; diff --git a/src/content/theory/frameworks/vue-reactivity.ts b/src/content/theory/frameworks/vue-reactivity.ts new file mode 100644 index 0000000..f5600da --- /dev/null +++ b/src/content/theory/frameworks/vue-reactivity.ts @@ -0,0 +1,105 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const vueReactivityTheory: TopicTheoryContent = { + summary: + "Vue's reactivity system intercepts property reads and writes with Proxy traps and ref accessors, records which effect read which property, and re-runs exactly those effects when the property changes.", + whatItIs: [ + "In Vue you mutate state directly: state.count++ or count.value++. There is no setter function to call and no dependency array to declare. The framework still knows what to update because every reactive object is a Proxy and every ref is an object with an accessor, so each read and write passes through code Vue controls.", + "That interception feeds two operations. During an effect's run, the get trap calls track(target, key), which adds the currently running effect to a Dep set stored under that target and key in a global WeakMap called targetMap. When a write reaches the set trap, trigger(target, key) looks up the same Dep and schedules every effect in it. The set of dependencies is rebuilt on every run, so a branch that stops reading a property also stops subscribing to it.", + "Effects come in a few forms with the same core. watchEffect and watch wrap a user callback. computed wraps a getter, caches the result, and flips a dirty flag on trigger instead of recomputing eagerly. A component's render function is also an effect, which is why a Vue component re-renders only when a property it actually read changes, rather than on every state update it owns as in React.", + "Triggered effects do not run inline by default. A scheduler pushes them into a job queue, deduplicates by identity, and flushes the queue in a single microtask. Several synchronous writes therefore produce one re-run. nextTick returns the promise that resolves after that flush. Vue 3.4 rewrote the internals around version counters so a computed can check whether any dependency changed without re-walking its subscriptions, and 3.5 lowered memory use further, but the track and trigger model is unchanged.", + ], + howItWorks: [ + "Step 1: reactive(obj) creates a Proxy with get, set, has, deleteProperty, and ownKeys handlers and caches it in a WeakMap so the same raw object always maps to the same proxy. ref(value) creates a RefImpl whose value accessor pair does the same job for a single slot, which is how primitives become observable.", + "Step 2: creating an effect (watchEffect, computed, a component render) wraps the function in a ReactiveEffect. Running it sets the global activeEffect to that instance for the duration of the call, so any interceptor that fires during the run can identify the reader.", + "Step 3: a get trap or ref getter calls track(target, key). If activeEffect is set, track finds or creates targetMap.get(raw).get(key), a Dep, and links the effect to it. If activeEffect is undefined, for example during a read at module scope or inside an event handler, nothing is recorded.", + "Step 4: a set trap or ref setter writes the new value, compares it with the old one using Object.is, and calls trigger(target, key) only when they differ. trigger collects the effects in that Dep. Adding or deleting a key also triggers effects that iterated the object, which is why ownKeys and has are trapped too.", + "Step 5: each collected effect is handed to its scheduler. A plain effect runs immediately, a computed is marked dirty and notifies its own subscribers, and a watchEffect or render effect is pushed into the job queue. queueJob ignores an effect that is already queued and schedules one flushJobs microtask if none is pending.", + "Step 6: flushJobs runs pre-flush jobs, then component updates sorted by component id so parents render before children, then post-flush jobs such as watchers with flush: 'post'. Each run resets activeEffect, re-executes the function, and rebuilds its dependency set, after which the nextTick promise resolves.", + ], + commonMistakes: [ + { + title: "Destructuring a reactive object", + explanation: + "const { total } = state reads state.total once through the get trap and stores the plain value in a local variable. The variable has no interceptor, so an effect that reads it records no dependency and never re-runs.", + fix: "Keep property access on the proxy inside the effect, or convert with toRefs(state) so each destructured binding is an ObjectRefImpl whose .value forwards to the proxy.", + }, + { + title: "Forgetting .value on a ref in script", + explanation: + "Templates and reactive() objects unwrap refs automatically, so it is easy to write count++ in a script block. That reassigns the local variable to NaN or a new number and discards the RefImpl entirely.", + fix: "Always read and write count.value in JavaScript. Use ref for primitives and reactive for objects you never reassign, and let the template compiler handle unwrapping.", + }, + { + title: "Expecting the DOM to update synchronously", + explanation: + "After state.count++ the DOM still shows the old value because the render effect is queued and runs in a microtask. Code that measures the DOM right after a write reads stale layout.", + fix: "await nextTick() before reading the DOM, or put the measurement in a watcher with flush: 'post'. Do not switch effects to flush: 'sync' just to make a test pass, that removes batching.", + }, + { + title: "Relying on a computed for side effects", + explanation: + "A computed getter runs lazily, only when something reads .value and a dependency changed. A side effect placed inside it may run late, several times, or never, depending on who reads the computed.", + fix: "Keep computed getters pure and use watch or watchEffect for side effects. If a value must be recalculated eagerly on change, a watcher with an explicit source expresses that intent.", + }, + ], + interviewQuestions: [ + { + question: + "How does Vue know which effects to re-run when a property changes?", + answer: + "Every reactive object is a Proxy. While an effect runs, Vue sets a global activeEffect, and the get trap calls track(target, key) to add that effect to a Dep stored in targetMap under the object and key. The set trap calls trigger(target, key), which schedules every effect in that Dep. Dependencies are collected from actual reads, so there is no declaration step.", + codeExample: { + language: "javascript", + code: `const state = reactive({ count: 0, name: 'Ada' }) + +watchEffect(() => console.log(state.count)) +// get trap: track(state, 'count') -> Dep { effect } + +state.name = 'Bob' // set trap: no Dep for 'name', nothing runs +state.count = 1 // set trap: trigger(state, 'count') -> effect queued`, + }, + }, + { + question: + "What is the difference between ref and reactive, and why do both exist?", + answer: + "reactive wraps an object in a Proxy, so it cannot hold a primitive and stops tracking if you reassign or destructure it. ref wraps any value in a RefImpl object with a value accessor, so primitives become observable and the wrapper can be passed around or returned from a composable without losing reactivity. Inside reactive objects and templates, refs are unwrapped automatically.", + codeExample: { + language: "javascript", + code: `const count = ref(0) // RefImpl { value: 0 } +count.value++ // setter -> triggerRefValue + +const state = reactive({ n: 1 }) +const { n } = state // n is a plain 1, not reactive +const { n: nRef } = toRefs(state) +nRef.value // forwards to state.n through the Proxy`, + }, + }, + { + question: + "How does computed caching work, and when does the getter actually run?", + answer: + "computed creates a ComputedRefImpl with a dirty flag and a cached value. Reading .value runs the getter only when dirty is true, then caches the result and clears the flag. When a dependency triggers, the computed is marked dirty and its own subscribers are notified, but the getter is not re-run until someone reads .value again. Vue 3.4 backs this with version counters so an unchanged computed can return its cache without walking its dependencies.", + }, + { + question: + "Why does the DOM not update immediately after a state mutation, and what does nextTick do?", + answer: + "Triggered render effects are pushed into a job queue rather than run inline. queueJob deduplicates by effect identity and schedules a single flushJobs microtask, so multiple synchronous writes produce one render. nextTick returns the promise chained after that flush, so awaiting it guarantees the DOM reflects the latest state.", + }, + { + question: + "How does Vue's reactivity differ from React's re-render model?", + answer: + "React re-runs the whole component function when any of its state changes and then diffs the output, relying on memoization to skip children. Vue tracks reads at the property level, so a component's render effect re-runs only when a property it read changes, and unrelated state updates cost nothing. The trade-off is that Vue state must stay inside proxies or refs, and copying a value out of them silently breaks tracking.", + }, + ], + relatedTopicIds: [ + "svelte-runes", + "angular-change-detection", + "render-cycle", + "hooks", + "closures", + ], +}; diff --git a/src/content/theory/index.test.ts b/src/content/theory/index.test.ts new file mode 100644 index 0000000..3295d74 --- /dev/null +++ b/src/content/theory/index.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; +import { + getRelatedTopicsFromTheory, + getTheoryContent, + getTheoryTopicsByCategory, + hasTheoryContent, +} from "@/content/theory"; +import { CATEGORY_ORDER } from "@/lib/categories"; +import { getTopicsByCategory, isTopicId, topics } from "@/lib/topics"; + +const EM_DASH = "—"; +const EMOJI = /\p{Extended_Pictographic}/u; + +describe("theory registry", () => { + it("has theory content for every topic", () => { + for (const topic of topics) { + expect(hasTheoryContent(topic.id)).toBe(true); + expect(getTheoryContent(topic.id)).toBeDefined(); + } + }); + + it("returns nothing for unknown topics", () => { + expect(getTheoryContent("missing")).toBeUndefined(); + expect(hasTheoryContent("missing")).toBe(false); + }); + + it("fills every section for every topic", () => { + for (const topic of topics) { + const theory = getTheoryContent(topic.id)!; + expect(theory.summary.trim().length).toBeGreaterThan(40); + expect(theory.whatItIs.length).toBeGreaterThan(0); + expect(theory.howItWorks.length).toBeGreaterThan(0); + expect(theory.commonMistakes.length).toBeGreaterThan(0); + expect(theory.interviewQuestions.length).toBeGreaterThan(0); + for (const mistake of theory.commonMistakes) { + expect(mistake.title.trim()).not.toBe(""); + expect(mistake.explanation.trim()).not.toBe(""); + expect(mistake.fix.trim()).not.toBe(""); + } + for (const question of theory.interviewQuestions) { + expect(question.question.trim()).not.toBe(""); + expect(question.answer.trim()).not.toBe(""); + if (question.codeExample) { + expect(question.codeExample.code.trim()).not.toBe(""); + } + } + } + }); + + it("links three to five related topics that exist and are not itself", () => { + for (const topic of topics) { + const ids = getTheoryContent(topic.id)!.relatedTopicIds; + expect(ids.length).toBeGreaterThanOrEqual(3); + expect(ids.length).toBeLessThanOrEqual(5); + expect(new Set(ids).size).toBe(ids.length); + expect(ids).not.toContain(topic.id); + for (const id of ids) { + expect(isTopicId(id)).toBe(true); + } + } + }); + + it("keeps prose free of em dashes and emojis", () => { + for (const topic of topics) { + const serialized = JSON.stringify(getTheoryContent(topic.id)); + expect(serialized).not.toContain(EM_DASH); + expect(serialized).not.toMatch(EMOJI); + } + }); +}); + +describe("getTheoryTopicsByCategory", () => { + it("matches the topic registry because every topic has theory", () => { + for (const category of CATEGORY_ORDER) { + expect(getTheoryTopicsByCategory(category)).toEqual( + getTopicsByCategory(category), + ); + } + }); +}); + +describe("getRelatedTopicsFromTheory", () => { + it("resolves related ids to topics in authored order", () => { + const theory = getTheoryContent("closures")!; + const related = getRelatedTopicsFromTheory("closures"); + expect(related.map((topic) => topic.id)).toEqual(theory.relatedTopicIds); + for (const topic of related) { + expect(topic.route).toBe(`/${topic.category}/${topic.id}`); + } + }); + + it("returns an empty list for unknown topics", () => { + expect(getRelatedTopicsFromTheory("missing")).toEqual([]); + }); +}); diff --git a/src/content/theory/index.ts b/src/content/theory/index.ts index 02a719e..24fcb52 100644 --- a/src/content/theory/index.ts +++ b/src/content/theory/index.ts @@ -1,94 +1,120 @@ import type { TopicTheoryContent } from "@/content/theory/types"; +import { attentionTheory } from "@/content/theory/ai/attention"; +import { backpropagationTheory } from "@/content/theory/ai/backpropagation"; +import { embeddingsTheory } from "@/content/theory/ai/embeddings"; +import { nextTokenPredictionTheory } from "@/content/theory/ai/next-token-prediction"; +import { tokenizationTheory } from "@/content/theory/ai/tokenization"; +import { cachingStrategiesTheory } from "@/content/theory/backend/caching-strategies"; +import { databaseIndexingTheory } from "@/content/theory/backend/database-indexing"; +import { httpRequestLifecycleTheory } from "@/content/theory/backend/http-request-lifecycle"; +import { jwtAuthenticationTheory } from "@/content/theory/backend/jwt-authentication"; +import { rateLimitingTheory } from "@/content/theory/backend/rate-limiting"; +import { angularChangeDetectionTheory } from "@/content/theory/frameworks/angular-change-detection"; +import { svelteRunesTheory } from "@/content/theory/frameworks/svelte-runes"; +import { vueReactivityTheory } from "@/content/theory/frameworks/vue-reactivity"; +import { asyncAwaitTheory } from "@/content/theory/javascript/async-await"; import { closuresTheory } from "@/content/theory/javascript/closures"; +import { debounceThrottleTheory } from "@/content/theory/javascript/debounce-throttle"; +import { destructuringTheory } from "@/content/theory/javascript/destructuring"; +import { eventDelegationTheory } from "@/content/theory/javascript/event-delegation"; import { eventLoopTheory } from "@/content/theory/javascript/event-loop"; import { executionContextTheory } from "@/content/theory/javascript/execution-context"; import { garbageCollectionTheory } from "@/content/theory/javascript/garbage-collection"; import { generatorsTheory } from "@/content/theory/javascript/generators"; import { heapStackTheory } from "@/content/theory/javascript/heap-stack"; import { hoistingTheory } from "@/content/theory/javascript/hoisting"; +import { modulesImportsTheory } from "@/content/theory/javascript/modules-imports"; import { promisesTheory } from "@/content/theory/javascript/promises"; import { prototypalInheritanceTheory } from "@/content/theory/javascript/prototypal-inheritance"; import { referenceValueTheory } from "@/content/theory/javascript/reference-value"; import { scopeChainTheory } from "@/content/theory/javascript/scope-chain"; +import { spreadRestTheory } from "@/content/theory/javascript/spread-rest"; import { thisKeywordTheory } from "@/content/theory/javascript/this-keyword"; import { typeCoercionTheory } from "@/content/theory/javascript/type-coercion"; -import { reconciliationTheory } from "@/content/theory/react/reconciliation"; -import { virtualDomTheory } from "@/content/theory/react/virtual-dom"; +import { concurrentRenderingTheory } from "@/content/theory/react/concurrent-rendering"; import { contextPropagationTheory } from "@/content/theory/react/context-propagation"; -import { destructuringTheory } from "@/content/theory/javascript/destructuring"; -import { spreadRestTheory } from "@/content/theory/javascript/spread-rest"; +import { errorBoundariesTheory } from "@/content/theory/react/error-boundaries"; import { fiberTreeTheory } from "@/content/theory/react/fiber-tree"; import { hooksTheory } from "@/content/theory/react/hooks"; -import { renderCycleTheory } from "@/content/theory/react/render-cycle"; import { memoizationTheory } from "@/content/theory/react/memoization"; -import { suspenseTheory } from "@/content/theory/react/suspense"; +import { reconciliationTheory } from "@/content/theory/react/reconciliation"; +import { renderCycleTheory } from "@/content/theory/react/render-cycle"; import { serverComponentsTheory } from "@/content/theory/react/server-components"; -import { eventDelegationTheory } from "@/content/theory/javascript/event-delegation"; -import { modulesImportsTheory } from "@/content/theory/javascript/modules-imports"; -import { errorBoundariesTheory } from "@/content/theory/react/error-boundaries"; +import { stateBatchingTheory } from "@/content/theory/react/state-batching"; +import { suspenseTheory } from "@/content/theory/react/suspense"; import { useEffectLifecycleTheory } from "@/content/theory/react/use-effect-lifecycle"; -import { getTopicById, getTopicsByCategory } from "@/lib/topics"; +import { virtualDomTheory } from "@/content/theory/react/virtual-dom"; +import { getTopicById, getTopicsByCategory, type TopicId } from "@/lib/topics"; import type { Category, Topic } from "@/types"; -const THEORY_CONTENT_BY_TOPIC_ID: Record = { +const THEORY_CONTENT_BY_TOPIC_ID: Record = { + "angular-change-detection": angularChangeDetectionTheory, + "async-await": asyncAwaitTheory, + attention: attentionTheory, + backpropagation: backpropagationTheory, + "caching-strategies": cachingStrategiesTheory, closures: closuresTheory, + "concurrent-rendering": concurrentRenderingTheory, + "context-propagation": contextPropagationTheory, + "database-indexing": databaseIndexingTheory, + "debounce-throttle": debounceThrottleTheory, + destructuring: destructuringTheory, + embeddings: embeddingsTheory, + "error-boundaries": errorBoundariesTheory, + "event-delegation": eventDelegationTheory, "event-loop": eventLoopTheory, "execution-context": executionContextTheory, + "fiber-tree": fiberTreeTheory, "garbage-collection": garbageCollectionTheory, generators: generatorsTheory, "heap-stack": heapStackTheory, hoisting: hoistingTheory, + hooks: hooksTheory, + "http-request-lifecycle": httpRequestLifecycleTheory, + "jwt-authentication": jwtAuthenticationTheory, + memoization: memoizationTheory, + "modules-imports": modulesImportsTheory, + "next-token-prediction": nextTokenPredictionTheory, promises: promisesTheory, "prototypal-inheritance": prototypalInheritanceTheory, + "rate-limiting": rateLimitingTheory, + reconciliation: reconciliationTheory, "reference-value": referenceValueTheory, + "render-cycle": renderCycleTheory, "scope-chain": scopeChainTheory, - "this-keyword": thisKeywordTheory, - "type-coercion": typeCoercionTheory, - "virtual-dom": virtualDomTheory, - "context-propagation": contextPropagationTheory, - reconciliation: reconciliationTheory, - destructuring: destructuringTheory, + "server-components": serverComponentsTheory, "spread-rest": spreadRestTheory, - "fiber-tree": fiberTreeTheory, - hooks: hooksTheory, - "render-cycle": renderCycleTheory, - memoization: memoizationTheory, + "state-batching": stateBatchingTheory, suspense: suspenseTheory, - "server-components": serverComponentsTheory, - "event-delegation": eventDelegationTheory, - "modules-imports": modulesImportsTheory, - "error-boundaries": errorBoundariesTheory, + "svelte-runes": svelteRunesTheory, + "this-keyword": thisKeywordTheory, + tokenization: tokenizationTheory, + "type-coercion": typeCoercionTheory, "use-effect-lifecycle": useEffectLifecycleTheory, + "virtual-dom": virtualDomTheory, + "vue-reactivity": vueReactivityTheory, }; -export function getTheoryContent(topicId: string): TopicTheoryContent | undefined { - return THEORY_CONTENT_BY_TOPIC_ID[topicId]; -} +export const getTheoryContent = ( + topicId: string, +): TopicTheoryContent | undefined => + THEORY_CONTENT_BY_TOPIC_ID[topicId as TopicId]; -export function hasTheoryContent(topicId: string): boolean { - return Boolean(THEORY_CONTENT_BY_TOPIC_ID[topicId]); -} +export const hasTheoryContent = (topicId: string): boolean => + Boolean(getTheoryContent(topicId)); -export function getTheoryTopicsByCategory(category: Category): Topic[] { - return getTopicsByCategory(category).filter((topic) => hasTheoryContent(topic.id)); -} +export const getTheoryTopicsByCategory = (category: Category): Topic[] => + getTopicsByCategory(category).filter((topic) => hasTheoryContent(topic.id)); -export function getRelatedTopicsFromTheory(topicId: string): Topic[] { +/** Related topics in authored order, skipping self references and duplicates. */ +export const getRelatedTopicsFromTheory = (topicId: string): Topic[] => { const theory = getTheoryContent(topicId); - if (!theory) { - return []; - } - - const seen = new Set(); + if (!theory) return []; - return theory.relatedTopicIds - .filter((relatedTopicId) => { - if (relatedTopicId === topicId || seen.has(relatedTopicId)) { - return false; - } - seen.add(relatedTopicId); - return true; - }) - .map((relatedTopicId) => getTopicById(relatedTopicId)) + const ids = [...new Set(theory.relatedTopicIds)].filter( + (id) => id !== topicId, + ); + return ids + .map((id) => getTopicById(id)) .filter((topic): topic is Topic => Boolean(topic)); -} +}; diff --git a/src/content/theory/javascript/async-await.ts b/src/content/theory/javascript/async-await.ts new file mode 100644 index 0000000..6694edb --- /dev/null +++ b/src/content/theory/javascript/async-await.ts @@ -0,0 +1,112 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const asyncAwaitTheory: TopicTheoryContent = { + summary: + "async/await is syntax that lets a function pause on a promise and resume later without callbacks. Under the hood it is promise.then plus a suspended stack frame, scheduled through the microtask queue.", + whatItIs: [ + "An async function is a normal function with two guarantees: it always returns a promise, and it can use await inside its body. Callers get a promise immediately, even if the body has not finished, and the eventual return value or thrown error becomes that promise's fulfillment value or rejection reason.", + "await takes any value, converts it to a promise, and suspends the function until that promise settles. The engine saves the current stack frame, registers the rest of the function as the promise's reaction, and returns control to whoever called the async function. When the promise settles, the reaction runs as a microtask and the frame is restored with the awaited value in place of the await expression.", + "The mechanism is the same one generators use. A generator pauses on yield and resumes on next(); an async function pauses on await and is resumed by a promise reaction. Early transpilers literally compiled async/await into a generator driven by a promise loop, and V8's implementation still shares the suspend and resume machinery.", + "Because resumption happens through microtasks, async code interleaves with other work at predictable points. Everything before the first await runs synchronously on the caller's stack. Everything after runs later, after the current synchronous code finishes and before any timer or I/O callback, because the event loop drains the microtask queue between tasks.", + ], + howItWorks: [ + "Step 1: calling an async function creates its result promise, then runs the body synchronously on the current call stack. Any code before the first await, including side effects and console output, executes right away.", + "Step 2: at an await, the operand is passed through PromiseResolve. A native promise is used as is; a non-promise such as null or a number is wrapped in an already fulfilled promise, so awaiting a plain value still suspends and still costs one microtask tick.", + "Step 3: the engine attaches the continuation to that promise as a reaction, saves the stack frame, and returns the pending result promise to the caller. The caller keeps running as if the function had returned normally.", + "Step 4: when the awaited promise settles, its reaction job is queued in the microtask queue. The event loop runs it after the current task completes and before picking up the next task, restoring the frame and resuming execution after the await.", + "Step 5: if the awaited promise rejected, resumption throws the rejection reason at the await expression. A surrounding try/catch inside the async function catches it exactly as it would a synchronous throw.", + "Step 6: when the body returns, the result promise resolves with the return value. Returning a promise makes the result promise adopt it, which takes extra microtask ticks. Throwing rejects the result promise, and a rejection nobody handles is reported by the host as an unhandled rejection.", + ], + commonMistakes: [ + { + title: "Awaiting independent calls in sequence", + explanation: + "Writing const a = await f(); const b = await g(); means g() does not even start until f() has settled. Two 100 ms requests take 200 ms, because each timer is created only after the previous await resumes.", + fix: "Start every independent promise first, then await them together with Promise.all (or Promise.allSettled when partial failure is acceptable). Reserve sequential awaits for calls that depend on earlier results.", + }, + { + title: "Forgetting to await or catch", + explanation: + "Calling an async function without await and without .catch discards its promise. If that promise rejects, the error never reaches any handler, the host reports an unhandled rejection, and Node exits with a non-zero code by default.", + fix: "Either await the call inside a try/catch, attach .catch at the call site, or deliberately mark it with void and a catch handler when fire-and-forget is intended.", + }, + { + title: "return promise instead of return await inside try", + explanation: + "return fetchData() inside a try block returns the promise without inspecting it. The try completes normally, the catch never runs, and the rejection is only visible to the caller. Adopting the returned promise also costs extra microtask ticks compared with awaiting it.", + fix: "Use return await when the function has a try/catch or finally that must observe the rejection. Outside of try blocks, plain return promise is fine and marginally cheaper.", + }, + { + title: "Assuming await blocks the thread", + explanation: + "await suspends only the async function it appears in. The caller, other tasks, and other async functions keep running. Code that reads shared state after an await may see values changed by something that ran in between.", + fix: "Treat every await as a point where other code can run. Re-read state after resuming rather than caching it before the await, and avoid holding assumptions across suspension points.", + }, + ], + interviewQuestions: [ + { + question: + "What does an async function return, and when does its body start running?", + answer: + "It always returns a promise. The body starts immediately and runs synchronously on the caller's stack until the first await. Only then does the function suspend and hand a pending promise back to the caller.", + codeExample: { + language: "javascript", + code: `console.log('A'); +async function run() { + console.log('B'); + await null; + console.log('C'); +} +run(); +console.log('D'); +// A, B, D, C`, + }, + }, + { + question: "Why does await null still yield to the caller?", + answer: + "await wraps its operand with PromiseResolve, so a non-promise becomes an already fulfilled promise. The continuation is still scheduled as a microtask rather than running inline, so the function suspends for one tick even though there was nothing to wait for.", + }, + { + question: "How does async/await relate to generators?", + answer: + "Both suspend a stack frame and resume it later. A generator pauses at yield and is resumed by next(); an async function pauses at await and is resumed by a promise reaction. async/await can be expressed as a generator driven by a loop that awaits each yielded promise, and that is how early transpilers implemented it.", + codeExample: { + language: "javascript", + code: `function run(gen) { + const it = gen(); + const step = (value) => { + const { value: p, done } = it.next(value); + return done ? p : Promise.resolve(p).then(step); + }; + return step(); +} +run(function* () { + const a = yield fetchUser(1); + console.log(a); +});`, + }, + }, + { + question: "When does return await matter?", + answer: + "Inside a try/catch or finally. return promise exits the try before the promise settles, so a rejection skips the local catch and surfaces to the caller instead. return await rethrows the rejection at the await, where the catch can handle it.", + codeExample: { + language: "javascript", + code: `async function leaky() { + try { return fail(); } catch { /* never runs */ } +} +async function safe() { + try { return await fail(); } catch (e) { return 'handled'; } +}`, + }, + }, + { + question: + "Two independent requests each take 100 ms. How long do sequential awaits take versus Promise.all, and why?", + answer: + "Sequential awaits take about 200 ms because the second request is not created until the first has resolved. Promise.all takes about 100 ms because both requests are started before the function suspends, so their timers overlap and the total is the slowest one.", + }, + ], + relatedTopicIds: ["promises", "event-loop", "generators", "closures"], +}; diff --git a/src/content/theory/javascript/closures.ts b/src/content/theory/javascript/closures.ts index 9d7e28e..43b5813 100644 --- a/src/content/theory/javascript/closures.ts +++ b/src/content/theory/javascript/closures.ts @@ -67,5 +67,11 @@ next(); // 2`, "Async callbacks still use lexical scoping, so they read the captured bindings that exist when they eventually execute.", }, ], - relatedTopicIds: ["scope-chain", "execution-context", "hoisting", "garbage-collection", "promises"], + relatedTopicIds: [ + "scope-chain", + "execution-context", + "hoisting", + "garbage-collection", + "promises", + ], }; diff --git a/src/content/theory/javascript/debounce-throttle.ts b/src/content/theory/javascript/debounce-throttle.ts new file mode 100644 index 0000000..4543d5e --- /dev/null +++ b/src/content/theory/javascript/debounce-throttle.ts @@ -0,0 +1,107 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const debounceThrottleTheory: TopicTheoryContent = { + summary: + "Debounce and throttle are wrapper functions that limit how often an event handler runs. Debounce waits for a pause in events before calling once; throttle guarantees at most one call per time window. Both are built from a closure and a timer.", + whatItIs: [ + "Browsers emit some events far faster than an application can usefully respond to them. A user typing fires a keyup every 50 to 100 ms, scrolling fires dozens of events per second, and resizing a window fires continuously while the drag is in progress. Running a network request or a layout calculation on every one of those events wastes work and can make the page feel slower, not faster.", + "Debounce solves this by postponing the call until the events stop. Each new event cancels the previous timer and starts a fresh one, so the wrapped function only runs after a full quiet period of the configured length. Throttle takes the opposite stance: it lets the first event through immediately, then ignores everything else until a fixed window has elapsed, so the handler runs at a steady maximum rate while the stream continues.", + "Both are implemented with the same two ingredients. A closure holds state that must survive between calls, such as the current timeout id or an in-window flag, and setTimeout provides the clock. The wrapper returned by debounce or throttle is the only thing the caller sees, and it forwards the receiver and arguments to the real function with fn.apply so that method handlers and event arguments still work.", + "The choice between them is about which moment matters. If only the final state is useful, such as the finished search query or the settled window size, debounce is correct because intermediate calls would be thrown away. If the UI must track the stream while it happens, such as a scroll position indicator or a drag preview, throttle is correct because debounce would freeze until the user stopped.", + ], + howItWorks: [ + "Step 1: the factory runs once. debounce(fn, wait) or throttle(fn, limit) executes a single time, declares its state variables, and returns an inner function. That inner function closes over the state, which is why the state persists across every later event.", + "Step 2: an event invokes the wrapper. The wrapper receives the same this and arguments the listener would have received. Debounce stores them and clears any pending timer; throttle checks whether a window is currently open.", + "Step 3: the wrapper schedules or skips. Debounce always calls setTimeout for a new deadline of wait milliseconds. Throttle either calls fn immediately and opens a window with setTimeout, or, if the window is still open, records the arguments as pending and returns without calling.", + "Step 4: the timer callback is queued as a macrotask. When the timer expires, the callback joins the task queue and the event loop runs it once the call stack is empty. A long synchronous task can delay it past the nominal deadline.", + "Step 5: the callback delivers the call. For debounce it invokes fn.apply(this, lastArgs) with whatever the most recent event provided. For throttle it closes the window, and with the trailing option enabled, it invokes fn with the pending arguments and opens a new window.", + "Step 6: control methods act on the same closure. cancel() clears the pending timer so nothing fires, and flush() runs the pending call immediately. Both are needed for cleanup when a component unmounts or the user navigates away.", + ], + commonMistakes: [ + { + title: "Creating the wrapper on every render", + explanation: + "Calling debounce inside a React component body, or inline in an event handler, produces a new closure with fresh state on each render. The previous timer belongs to a wrapper that no longer receives events, so nothing can cancel it and it fires with stale data alongside the new one.", + fix: "Create the debounced or throttled function once, at module scope, or inside useMemo or useRef with a stable dependency list. Cancel it in the cleanup of a useEffect.", + }, + { + title: "Using an arrow function and losing this", + explanation: + "If the returned wrapper is an arrow function, this is captured from the factory scope instead of the caller, so method-style handlers see the wrong receiver. Forwarding with fn(...args) instead of fn.apply(this, args) has the same effect.", + fix: "Return a regular function expression from the factory and forward the receiver explicitly with fn.apply(this, args) or fn.call(this, ...args).", + }, + { + title: "Debouncing something that needs live feedback", + explanation: + "A debounced scroll or drag handler runs only after the user stops, so a progress indicator or a sticky header appears to lag behind and then jump. The wait time reads as unresponsiveness.", + fix: "Use throttle for handlers that should track a stream in progress, and reserve debounce for work where only the final value matters.", + }, + { + title: "Forgetting the pending call on unmount", + explanation: + "The timer inside the closure keeps running after the component that created it is gone. When it fires it can call setState on an unmounted component or send a request that nobody will read.", + fix: "Expose or use a cancel() method and call it in the effect cleanup. Use flush() first if the pending work should still be saved.", + }, + ], + interviewQuestions: [ + { + question: "What is the difference between debounce and throttle?", + answer: + "Debounce delays the call until a quiet period of the configured length has passed since the last event, so a burst of events produces one call at the end. Throttle calls at most once per time window while events continue, so a burst produces calls at a steady rate, starting with the first event.", + }, + { + question: "Implement debounce.", + answer: + "The factory declares a timeout id in its closure and returns a regular function. Each call clears the existing timer and schedules a new one for the wait period. The timer callback forwards this and the latest arguments to the wrapped function with apply.", + codeExample: { + language: "javascript", + code: `function debounce(fn, wait) { + let timeoutId = null; + return function (...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + timeoutId = null; + fn.apply(this, args); + }, wait); + }; +}`, + }, + }, + { + question: "Implement a leading-edge throttle.", + answer: + "The closure holds a flag that marks an open window. If the flag is set the call returns immediately. Otherwise the wrapper calls the function, sets the flag, and schedules a timer to clear it after the limit. A trailing variant also records the last skipped arguments and calls with them when the window closes.", + codeExample: { + language: "javascript", + code: `function throttle(fn, limit) { + let inThrottle = false; + return function (...args) { + if (inThrottle) return; + fn.apply(this, args); + inThrottle = true; + setTimeout(() => { + inThrottle = false; + }, limit); + }; +}`, + }, + }, + { + question: + "Why does a debounced function created inside a React render body stop debouncing?", + answer: + "Each render calls debounce again and produces a new closure with its own timeoutId. The listener is attached to the newest wrapper, so an event after a re-render clears a timer that was never set and the previous render's timer keeps counting down. That orphaned timer still fires with stale arguments, so instead of one call per burst you get one call per render. The fix is to create the wrapper once with useMemo or useRef.", + }, + { + question: "What do lodash's leading, trailing, and maxWait options do?", + answer: + "leading calls on the first event of a burst and trailing calls after the burst ends; debounce defaults to trailing only and throttle enables both. maxWait forces a debounced function to run at least every N milliseconds even if events never stop, which is how lodash builds throttle on top of debounce by setting maxWait equal to wait.", + }, + ], + relatedTopicIds: [ + "closures", + "event-loop", + "event-delegation", + "async-await", + ], +}; diff --git a/src/content/theory/javascript/destructuring.ts b/src/content/theory/javascript/destructuring.ts index 0611750..fc85dd6 100644 --- a/src/content/theory/javascript/destructuring.ts +++ b/src/content/theory/javascript/destructuring.ts @@ -36,7 +36,8 @@ export const destructuringTheory: TopicTheoryContent = { ], interviewQuestions: [ { - question: "What is the difference between array and object destructuring?", + question: + "What is the difference between array and object destructuring?", answer: "Array destructuring matches by position (index), while object destructuring matches by property name. Arrays use square brackets, objects use curly braces.", codeExample: { diff --git a/src/content/theory/javascript/event-delegation.ts b/src/content/theory/javascript/event-delegation.ts index de72477..dfb17ee 100644 --- a/src/content/theory/javascript/event-delegation.ts +++ b/src/content/theory/javascript/event-delegation.ts @@ -50,12 +50,16 @@ export const eventDelegationTheory: TopicTheoryContent = { "Event delegation attaches a single event listener to a parent element instead of individual listeners on each child. It works because events bubble up. Benefits include fewer listeners (lower memory), automatic handling of dynamically added children, and simpler setup for lists and tables.", }, { - question: - "What does stopPropagation() do and when should you use it?", + question: "What does stopPropagation() do and when should you use it?", answer: "stopPropagation() prevents the event from continuing to the next element in the propagation path. It does not prevent other listeners on the same element from firing (stopImmediatePropagation does that). Use it when a child component must handle an event exclusively and parent handlers should not react.", }, ], - relatedTopicIds: ["event-loop", "scope-chain", "closures", "execution-context"], + relatedTopicIds: [ + "event-loop", + "debounce-throttle", + "scope-chain", + "closures", + ], }; diff --git a/src/content/theory/javascript/event-loop.ts b/src/content/theory/javascript/event-loop.ts index 89f7d93..8f9759e 100644 --- a/src/content/theory/javascript/event-loop.ts +++ b/src/content/theory/javascript/event-loop.ts @@ -64,5 +64,11 @@ console.log("B"); "No. async/await is syntax over Promises, so continuation after await still resumes through microtasks.", }, ], - relatedTopicIds: ["promises", "closures", "scope-chain", "heap-stack"], + relatedTopicIds: [ + "promises", + "async-await", + "closures", + "scope-chain", + "heap-stack", + ], }; diff --git a/src/content/theory/javascript/execution-context.ts b/src/content/theory/javascript/execution-context.ts index a648a6e..975ada7 100644 --- a/src/content/theory/javascript/execution-context.ts +++ b/src/content/theory/javascript/execution-context.ts @@ -37,7 +37,8 @@ export const executionContextTheory: TopicTheoryContent = { ], interviewQuestions: [ { - question: "What happens during the creation phase of an execution context?", + question: + "What happens during the creation phase of an execution context?", answer: "The engine scans the code for declarations. var variables are hoisted and set to undefined, function declarations are stored in full, parameters are bound to argument values, and the this value is determined. No code is executed yet.", }, @@ -47,7 +48,8 @@ export const executionContextTheory: TopicTheoryContent = { "Exactly one. The Global EC is created when the script starts and remains at the bottom of the call stack until the program ends.", }, { - question: "What is the difference between Variable Environment and Lexical Environment?", + question: + "What is the difference between Variable Environment and Lexical Environment?", answer: "The Variable Environment holds var and function declarations. The Lexical Environment additionally tracks let and const bindings. In practice both are part of the same execution context, but the spec separates them to handle block scoping for let/const.", codeExample: { diff --git a/src/content/theory/javascript/garbage-collection.ts b/src/content/theory/javascript/garbage-collection.ts index ca47eb1..f8e9ef1 100644 --- a/src/content/theory/javascript/garbage-collection.ts +++ b/src/content/theory/javascript/garbage-collection.ts @@ -16,7 +16,8 @@ export const garbageCollectionTheory: TopicTheoryContent = { ], commonMistakes: [ { - title: "Expecting immediate memory release after setting a variable to null", + title: + "Expecting immediate memory release after setting a variable to null", explanation: "Nulling a reference only makes an object eligible for collection if no other reachable references exist.", fix: "Remove all strong references and let the collector run naturally.", diff --git a/src/content/theory/javascript/heap-stack.ts b/src/content/theory/javascript/heap-stack.ts index bf7c5b0..b5da92b 100644 --- a/src/content/theory/javascript/heap-stack.ts +++ b/src/content/theory/javascript/heap-stack.ts @@ -21,7 +21,8 @@ export const heapStackTheory: TopicTheoryContent = { ], commonMistakes: [ { - title: "Treating stack and heap as strict language guarantees for every value", + title: + "Treating stack and heap as strict language guarantees for every value", explanation: "The model is conceptual and implementation details can vary by engine optimization strategy.", fix: "Use stack and heap as a mental model for behavior, not as a strict byte-level storage contract.", @@ -47,7 +48,8 @@ export const heapStackTheory: TopicTheoryContent = { ], interviewQuestions: [ { - question: "What is the difference between stack and heap memory in JavaScript?", + question: + "What is the difference between stack and heap memory in JavaScript?", answer: "The stack holds execution frames and local bindings in last-in-first-out order, with fast automatic cleanup and a small fixed size. The heap holds objects with dynamic lifetimes, costs more to allocate, and is reclaimed by the garbage collector when nothing reachable references the object.", codeExample: { @@ -100,5 +102,11 @@ console.log(person.name); // "Ada"`, "Garbage collection reclaims heap objects that are no longer reachable from active roots.", }, ], - relatedTopicIds: ["execution-context", "reference-value", "garbage-collection", "closures", "event-loop"], + relatedTopicIds: [ + "execution-context", + "reference-value", + "garbage-collection", + "closures", + "event-loop", + ], }; diff --git a/src/content/theory/javascript/hoisting.ts b/src/content/theory/javascript/hoisting.ts index d4de4be..c954f10 100644 --- a/src/content/theory/javascript/hoisting.ts +++ b/src/content/theory/javascript/hoisting.ts @@ -64,5 +64,11 @@ let counter = 1;`, "It is better to write declarations in a clear top-down flow. Understanding hoisting helps debugging, but explicit ordering improves readability.", }, ], - relatedTopicIds: ["execution-context", "scope-chain", "closures", "this-keyword", "type-coercion"], + relatedTopicIds: [ + "execution-context", + "scope-chain", + "closures", + "this-keyword", + "type-coercion", + ], }; diff --git a/src/content/theory/javascript/modules-imports.ts b/src/content/theory/javascript/modules-imports.ts index 4323e1b..f122e13 100644 --- a/src/content/theory/javascript/modules-imports.ts +++ b/src/content/theory/javascript/modules-imports.ts @@ -27,7 +27,7 @@ export const modulesImportsTheory: TopicTheoryContent = { { title: "Reading circular imports at the top level", explanation: - "When two modules import each other, one will evaluate first and see uninitialized values from the other. Top-level reads of circular imports can produce undefined.", + "When two modules import each other, one will evaluate first and see uninitialized values from the other. Top-level reads of circular imports see undefined for var and function-expression bindings, and throw a ReferenceError for let, const, and class bindings.", fix: "Move circular reads inside functions that execute after both modules have finished evaluating. Alternatively, restructure the dependency graph to break the cycle.", }, { @@ -52,7 +52,7 @@ export const modulesImportsTheory: TopicTheoryContent = { { question: "How does JavaScript handle circular dependencies?", answer: - "The engine creates module records for all modules in the graph before evaluating any of them. When a circular import is encountered, the engine uses the already-created (but not yet evaluated) module record. Bindings exist but may be uninitialized if the exporting module has not yet run the assignment. This is why circular reads at the top level can see undefined.", + "The engine creates module records for all modules in the graph before evaluating any of them. When a circular import is encountered, the engine uses the already-created (but not yet evaluated) module record. Bindings exist but may be uninitialized if the exporting module has not yet run the assignment. A var binding reads as undefined, while a let or const binding throws a ReferenceError because it is still in its temporal dead zone.", codeExample: { code: `// a.js import { b } from "./b.js"; @@ -68,5 +68,11 @@ console.log(a); // undefined (a.js not evaluated yet)`, }, ], - relatedTopicIds: ["scope-chain", "hoisting", "execution-context", "closures", "event-delegation"], + relatedTopicIds: [ + "scope-chain", + "hoisting", + "execution-context", + "closures", + "event-delegation", + ], }; diff --git a/src/content/theory/javascript/promises.ts b/src/content/theory/javascript/promises.ts index d2cc26f..fe5961b 100644 --- a/src/content/theory/javascript/promises.ts +++ b/src/content/theory/javascript/promises.ts @@ -53,7 +53,8 @@ export const promisesTheory: TopicTheoryContent = { "Yes. Thrown errors in then callbacks reject the next Promise in the chain, and catch can handle that rejection.", }, { - question: "What is the difference between Promise.all and Promise.allSettled?", + question: + "What is the difference between Promise.all and Promise.allSettled?", answer: "Promise.all fails fast on the first rejection, while Promise.allSettled waits for all Promises and returns each outcome.", }, @@ -63,5 +64,5 @@ export const promisesTheory: TopicTheoryContent = { "No. async/await is syntax built on top of Promises; underlying scheduling and resolution behavior still follows Promise rules.", }, ], - relatedTopicIds: ["event-loop", "closures", "generators", "type-coercion"], + relatedTopicIds: ["event-loop", "async-await", "closures", "generators"], }; diff --git a/src/content/theory/javascript/prototypal-inheritance.ts b/src/content/theory/javascript/prototypal-inheritance.ts index b4503ed..f127e49 100644 --- a/src/content/theory/javascript/prototypal-inheritance.ts +++ b/src/content/theory/javascript/prototypal-inheritance.ts @@ -65,5 +65,10 @@ console.log(Object.getPrototypeOf(u) === User.prototype); // true`, "The chain ends at null, typically after reaching Object.prototype for ordinary objects.", }, ], - relatedTopicIds: ["this-keyword", "reference-value", "scope-chain", "heap-stack"], + relatedTopicIds: [ + "this-keyword", + "reference-value", + "scope-chain", + "heap-stack", + ], }; diff --git a/src/content/theory/javascript/reference-value.ts b/src/content/theory/javascript/reference-value.ts index 57a9123..c9e8d40 100644 --- a/src/content/theory/javascript/reference-value.ts +++ b/src/content/theory/javascript/reference-value.ts @@ -64,5 +64,10 @@ console.log(a.count); // 2 (shared object identity)`, "No. freeze is shallow and does not recursively freeze nested objects unless you do so manually.", }, ], - relatedTopicIds: ["heap-stack", "garbage-collection", "type-coercion", "closures"], + relatedTopicIds: [ + "heap-stack", + "garbage-collection", + "type-coercion", + "closures", + ], }; diff --git a/src/content/theory/javascript/spread-rest.ts b/src/content/theory/javascript/spread-rest.ts index 0396596..585578d 100644 --- a/src/content/theory/javascript/spread-rest.ts +++ b/src/content/theory/javascript/spread-rest.ts @@ -78,5 +78,11 @@ console.log(original); // [1, 2, 3]`, }, }, ], - relatedTopicIds: ["destructuring", "reference-value", "closures", "scope-chain", "hoisting"], + relatedTopicIds: [ + "destructuring", + "reference-value", + "closures", + "scope-chain", + "hoisting", + ], }; diff --git a/src/content/theory/javascript/this-keyword.ts b/src/content/theory/javascript/this-keyword.ts index 37527c0..360685b 100644 --- a/src/content/theory/javascript/this-keyword.ts +++ b/src/content/theory/javascript/this-keyword.ts @@ -28,7 +28,8 @@ export const thisKeywordTheory: TopicTheoryContent = { fix: "Use bind, wrapper callbacks, or class fields with arrow functions when appropriate.", }, { - title: "Using arrow functions for prototype methods that need dynamic this", + title: + "Using arrow functions for prototype methods that need dynamic this", explanation: "Arrow functions capture outer this and ignore call-site binding, which can break method semantics on instances.", fix: "Use regular methods when behavior depends on the receiver object.", @@ -66,5 +67,11 @@ showThis(); // undefined`, "When a bound function is called with new, constructor behavior takes priority and this points to the newly created instance.", }, ], - relatedTopicIds: ["execution-context", "prototypal-inheritance", "scope-chain", "closures", "hoisting"], + relatedTopicIds: [ + "execution-context", + "prototypal-inheritance", + "scope-chain", + "closures", + "hoisting", + ], }; diff --git a/src/content/theory/react/concurrent-rendering.ts b/src/content/theory/react/concurrent-rendering.ts new file mode 100644 index 0000000..26c3e66 --- /dev/null +++ b/src/content/theory/react/concurrent-rendering.ts @@ -0,0 +1,95 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const concurrentRenderingTheory: TopicTheoryContent = { + summary: + "Concurrent rendering lets React prepare a new tree in small interruptible chunks, so urgent updates like typing can cut in front of slow ones, and half-finished work can be thrown away without the user ever seeing it.", + whatItIs: [ + "Before React 18, every state update produced one synchronous render: React called your components from the root down and did not return control to the browser until the whole tree was built and committed. A slow render of a large list meant dropped frames, and a keystroke that arrived during it waited until the render finished. Concurrent rendering changes the shape of that work without changing what your components look like.", + "The core idea is priority. Every update is stamped with a lane, one bit in a 31-bit mask defined in ReactFiberLane. A state update inside a click or keypress gets SyncLane. An update wrapped in startTransition, or the catch-up render scheduled by useDeferredValue, gets a TransitionLane. When React decides what to render next it picks the highest priority pending lane, and it renders only the updates that belong to that lane, leaving lower priority updates queued for a later pass.", + "Transition lanes are rendered by workLoopConcurrent, which processes one fiber at a time and asks the Scheduler package whether it should yield. The Scheduler answers yes roughly every 5 ms. React then exits the loop and posts a MessageChannel message to resume, which gives the browser a chance to paint and dispatch events. If a higher priority update arrives during one of those gaps, React abandons the work-in-progress tree and starts again with the new state.", + "Throwing work away is safe because the render phase is pure: it only builds fibers in memory and never touches the DOM or runs effects. The commit phase, which does touch the DOM, is still synchronous and cannot be interrupted, so the screen never shows a partially applied update. Concurrent behaviour is opted into per update through startTransition, useTransition, and useDeferredValue. Plain setState calls in event handlers are still rendered synchronously, exactly as before.", + ], + howItWorks: [ + "Step 1: a setState call creates an update object and asks requestUpdateLane for a lane. Discrete events yield SyncLane; a call made while startTransition's flag is set yields a TransitionLane. The lane is merged into the root's pendingLanes bitmask.", + "Step 2: ensureRootIsScheduled reads getNextLanes and schedules the right work loop. SyncLane work runs in a microtask through performSyncWorkOnRoot; transition work is handed to the Scheduler as a normal-priority task that calls performConcurrentWorkOnRoot.", + "Step 3: the concurrent loop calls performUnitOfWork on each fiber and checks shouldYield between them. Once about 5 ms have passed the loop returns, the Scheduler yields to the browser, and rendering resumes in a later task from the fiber where it stopped.", + "Step 4: if a higher priority lane becomes pending mid-render, React calls prepareFreshStack, which drops the current work-in-progress tree. The queued update objects remain in each hook's queue, so the restart processes them all in order against the latest state.", + "Step 5: when the tree is complete React enters commitRoot. All DOM mutations, ref attachments, and layout effects run synchronously in one block, then passive effects are scheduled. useTransition's isPending flips back to false in this commit.", + "Step 6: if a component suspends during a transition render, React does not commit the fallback for content that is already visible. It marks the root as suspended, keeps the current tree on screen, and retries when the thrown promise pings. A suspension during a sync render commits the nearest fallback immediately.", + ], + commonMistakes: [ + { + title: "Wrapping the controlled input in the transition", + explanation: + "If setQuery for a controlled input is placed inside startTransition, the input's value is rendered at low priority and can lag behind the user's typing, which is the opposite of the intended effect.", + fix: "Keep the state that drives the input urgent, and put only the expensive derived state, such as the filter that renders a large list, inside the transition. Alternatively read the list from useDeferredValue(query).", + }, + { + title: "Expecting a transition to speed up rendering", + explanation: + "A transition still does the same amount of work; it only splits it into interruptible slices and lets urgent updates go first. Total render time stays the same or grows slightly because of the yields.", + fix: "Use transitions to keep the UI responsive, and use memoization, virtualization, or smaller components to make the render itself cheaper.", + }, + { + title: "Relying on render-phase side effects", + explanation: + "Concurrent React may call a component several times before committing, and may discard a render entirely. Code that mutates a ref, writes to a global, or logs analytics during render will run an unpredictable number of times.", + fix: "Keep render pure. Move side effects into useEffect or event handlers, and treat StrictMode's double render as a test for this property.", + }, + { + title: "Calling setState asynchronously inside startTransition", + explanation: + "The transition flag is only set for the synchronous duration of the callback. A setState that runs after an await inside the callback is scheduled with the default lane, not a transition lane, and does not get transition semantics.", + fix: "Call the state setter synchronously inside the callback, or wrap the specific setter call after the await in its own startTransition.", + }, + ], + interviewQuestions: [ + { + question: "What is a lane in React, and why is it a bitmask?", + answer: + "A lane is a priority level represented as a single bit in a 31-bit integer. Bitmasks let React store many pending priorities on one root, test for the highest one with a cheap bit operation, and merge several updates into one render by OR-ing their lanes together. SyncLane is the lowest set bit and therefore the highest priority.", + }, + { + question: "How does React interrupt a render, and why is that safe?", + answer: + "The concurrent work loop checks shouldYield between fibers and returns to the Scheduler about every 5 ms. If a higher priority lane appears, React discards the work-in-progress tree and restarts. This is safe because the render phase only builds fibers in memory; nothing is written to the DOM until the synchronous commit phase, so no partial state is ever visible.", + codeExample: { + language: "javascript", + code: `function workLoopConcurrent() { + // Process fibers until the Scheduler asks us to yield + while (workInProgress !== null && !shouldYield()) { + performUnitOfWork(workInProgress); + } +}`, + }, + }, + { + question: + "What is the difference between startTransition and useDeferredValue?", + answer: + "startTransition marks specific state updates as low priority at the point where they are dispatched, and useTransition adds an isPending flag. useDeferredValue works on a value instead: during an urgent render it returns the previous value and schedules a transition-priority re-render with the new one. Use startTransition when you own the setter, and useDeferredValue when the value comes from props or a parent you cannot change.", + codeExample: { + language: "jsx", + code: `// Owning the setter +const [isPending, startTransition] = useTransition(); +startTransition(() => setFilter(value)); + +// Receiving the value from a parent +const deferredQuery = useDeferredValue(query); +`, + }, + }, + { + question: + "Why does a transition that suspends not show the Suspense fallback?", + answer: + "When a transition render suspends, React treats replacing already visible content with a fallback as a worse outcome than waiting. It leaves the current tree committed, marks the root as suspended, and retries the render when the promise resolves. A sync update that suspends has no such option and commits the nearest fallback, which is why data fetching triggered by user input is usually wrapped in a transition.", + }, + { + question: "Is concurrent rendering a mode you turn on for the whole app?", + answer: + "No. With createRoot every app has the concurrent renderer, but individual updates still default to synchronous behaviour. Only updates marked through startTransition, useTransition, useDeferredValue, or a suspending Suspense boundary render in interruptible slices. This per-update design lets a codebase adopt concurrency one hot path at a time.", + }, + ], + relatedTopicIds: ["fiber-tree", "render-cycle", "suspense", "state-batching"], +}; diff --git a/src/content/theory/react/error-boundaries.ts b/src/content/theory/react/error-boundaries.ts index fa59d3b..6ee37f7 100644 --- a/src/content/theory/react/error-boundaries.ts +++ b/src/content/theory/react/error-boundaries.ts @@ -56,5 +56,11 @@ export const errorBoundariesTheory: TopicTheoryContent = { }, ], - relatedTopicIds: ["reconciliation", "render-cycle", "suspense", "hooks", "fiber-tree"], + relatedTopicIds: [ + "reconciliation", + "render-cycle", + "suspense", + "hooks", + "fiber-tree", + ], }; diff --git a/src/content/theory/react/fiber-tree.ts b/src/content/theory/react/fiber-tree.ts index f42bf2a..a2022c2 100644 --- a/src/content/theory/react/fiber-tree.ts +++ b/src/content/theory/react/fiber-tree.ts @@ -61,7 +61,8 @@ while (workInProgress !== null) { }, }, { - question: "Why did React move from a stack reconciler to a fiber reconciler?", + question: + "Why did React move from a stack reconciler to a fiber reconciler?", answer: "The stack reconciler processed the entire tree synchronously in one call, blocking the main thread. The fiber reconciler breaks work into units (fibers) that can be paused, prioritized, and resumed, enabling concurrent features like time-slicing and Suspense.", }, @@ -71,5 +72,11 @@ while (workInProgress !== null) { "child points to the fiber's first child. sibling points to the next sibling fiber. return points to the parent fiber. Together they form a linked-list representation of the tree that the work loop traverses without recursion.", }, ], - relatedTopicIds: ["reconciliation", "hooks", "render-cycle", "execution-context", "heap-stack"], + relatedTopicIds: [ + "reconciliation", + "hooks", + "render-cycle", + "execution-context", + "heap-stack", + ], }; diff --git a/src/content/theory/react/hooks.ts b/src/content/theory/react/hooks.ts index f6ce352..06ba4d1 100644 --- a/src/content/theory/react/hooks.ts +++ b/src/content/theory/react/hooks.ts @@ -69,7 +69,7 @@ export const hooksTheory: TopicTheoryContent = { "fiber-tree", "render-cycle", "use-effect-lifecycle", + "state-batching", "closures", - "reconciliation", ], }; diff --git a/src/content/theory/react/reconciliation.ts b/src/content/theory/react/reconciliation.ts index 61ce6d1..c1ac9e4 100644 --- a/src/content/theory/react/reconciliation.ts +++ b/src/content/theory/react/reconciliation.ts @@ -67,7 +67,8 @@ export const reconciliationTheory: TopicTheoryContent = { }, }, { - question: "What happens when React encounters different element types during reconciliation?", + question: + "What happens when React encounters different element types during reconciliation?", answer: "React tears down the entire old subtree (unmounting all components, running cleanup effects, destroying DOM nodes) and builds the new subtree from scratch. No diffing of children occurs across type boundaries.", }, diff --git a/src/content/theory/react/render-cycle.ts b/src/content/theory/react/render-cycle.ts index 0c7120f..2d84900 100644 --- a/src/content/theory/react/render-cycle.ts +++ b/src/content/theory/react/render-cycle.ts @@ -77,7 +77,7 @@ export const renderCycleTheory: TopicTheoryContent = { "fiber-tree", "hooks", "reconciliation", - "event-loop", - "use-effect-lifecycle", + "concurrent-rendering", + "vue-reactivity", ], }; diff --git a/src/content/theory/react/server-components.ts b/src/content/theory/react/server-components.ts index 1679b10..2a7837e 100644 --- a/src/content/theory/react/server-components.ts +++ b/src/content/theory/react/server-components.ts @@ -6,14 +6,14 @@ export const serverComponentsTheory: TopicTheoryContent = { whatItIs: [ "Server Components are React components that execute exclusively on the server. They can use async/await, access databases and file systems directly, and their code never reaches the browser bundle.", "Client Components are marked with 'use client' at the top of their file. They run in the browser (and optionally on the server for SSR) and can use hooks like useState, useEffect, and event handlers.", - "The RSC protocol serializes the server render output into a streaming payload: HTML chunks for server-rendered content, client references for 'use client' components, and serialized props at the boundary between server and client.", + "The RSC protocol serializes the server render output into a streaming payload: a serialized element tree for server-rendered content, client references for 'use client' components, and serialized props at the boundary between server and client. The framework turns that payload into HTML for the first paint.", ], howItWorks: [ "Step 1: React starts a server render pass. It calls Server Component functions on the server, executes async operations, and produces rendered output.", "Step 2: when React encounters a 'use client' component, it stops rendering and records a client reference (module path + export name) in the payload.", "Step 3: props crossing the server/client boundary are serialized. Only JSON-serializable values (strings, numbers, booleans, arrays, plain objects, Promises) can cross. Functions, classes, and Symbols cannot.", "Step 4: the RSC payload streams to the browser. The client runtime loads the referenced client modules and hydrates them with the serialized props.", - "Step 5: Server Components that were passed as children to Client Components (the composition pattern) are already rendered to HTML on the server. The client component receives them as opaque JSX it can render without re-executing.", + "Step 5: Server Components that were passed as children to Client Components (the composition pattern) are already rendered on the server. The client component receives them as an opaque, serialized element tree it can place without re-executing.", ], commonMistakes: [ { @@ -65,7 +65,7 @@ export default function ServerParent() { { question: "Why do Server Components reduce bundle size?", answer: - "Server Component code runs only on the server. Their imports (database drivers, markdown parsers, large data processing libraries) are never sent to the browser. Only the rendered output (HTML) and client references are included in the payload.", + "Server Component code runs only on the server. Their imports (database drivers, markdown parsers, large data processing libraries) are never sent to the browser. Only the rendered output (a serialized element tree) and client references are included in the payload.", }, ], relatedTopicIds: [ diff --git a/src/content/theory/react/state-batching.ts b/src/content/theory/react/state-batching.ts new file mode 100644 index 0000000..43c2757 --- /dev/null +++ b/src/content/theory/react/state-batching.ts @@ -0,0 +1,103 @@ +import type { TopicTheoryContent } from "@/content/theory/types"; + +export const stateBatchingTheory: TopicTheoryContent = { + summary: + "State batching is how React collects every setState call made during one event handler or callback into a single render, so the UI updates once with the final result instead of once per call.", + whatItIs: [ + 'Calling setState does not change anything immediately. It records an update on the hook\'s queue and asks React to render later. Batching is the rule for when "later" is: React waits until the current handler or callback finishes, then processes every queued update in one render pass and commits once.', + "Each render captures its state as a snapshot. The count variable inside an event handler is a plain constant captured by a closure, so three setCount(count + 1) calls in a row all compute the same number. To build on the pending value instead of the snapshot, you pass an updater function, setCount(c => c + 1), and React calls it with the result of the previous queued update.", + "Before React 18, batching only applied inside React's own event handlers. Updates made in a setTimeout, a promise callback, or a native event listener each rendered synchronously on their own. React 18 introduced automatic batching for roots created with createRoot: updates are batched wherever they come from, with the batch boundary at the end of the callback that issued them.", + "This design matters because rendering is the expensive part. One render per tick keeps intermediate states off the screen, avoids running effects for values nobody sees, and lets React treat the update queue as data it can reorder by priority under concurrent rendering. flushSync exists as the explicit escape hatch for the rare case where the DOM must be updated before the next line of code runs.", + ], + howItWorks: [ + "Step 1: a component renders and useState stores its current value in the hook object on the fiber. The event handlers created during that render close over the snapshot value.", + "Step 2: an event fires. React runs the handler inside a batched context, so any setState call appends an update object to the hook's queue, marks the fiber as needing work, and returns without rendering.", + "Step 3: each update records either a value or an updater function. A value replaces the state when its turn comes; an updater is stored uncalled and receives the pending state at that position in the queue.", + "Step 4: the handler returns. React flushes the scheduled work: it calls the component again, and useState walks the queue in call order from the base state to compute the new memoizedState.", + "Step 5: the render produces one new element tree and React commits it once. The DOM reflects the final state, the queue is cleared, and the new render creates fresh closures with the updated snapshot.", + "Step 6: outside event handlers, React 18+ applies the same rule per callback. A microtask or timer callback that calls setState several times still produces a single flush when that callback ends. flushSync forces the flush to happen synchronously inside the call instead.", + ], + commonMistakes: [ + { + title: "Expecting the next line to see the new state", + explanation: + "Logging count right after setCount prints the old value. The handler's count is a closure over the render that created it, and the render that would produce the new value has not run yet.", + fix: "Compute the next value in a local variable and use that, or move logic that depends on the committed state into a useEffect keyed on that state.", + }, + { + title: "Calling setCount(count + 1) repeatedly", + explanation: + "Every call reads the same snapshot, so three calls queue three updates with the same payload and the final state increments by one.", + fix: "Use the updater form, setCount(c => c + 1), whenever the next state depends on the previous state. React feeds each updater the result of the one before it.", + }, + { + title: "Assuming timeouts and promises batch on every React version", + explanation: + "On a React 17 legacy root, or a React 18 app still mounted with ReactDOM.render, each setState inside a setTimeout or .then callback triggers its own synchronous render.", + fix: "Mount with createRoot to get automatic batching everywhere. If a component still needs several updates to land together on an older root, wrap them in unstable_batchedUpdates or combine them into one state object.", + }, + { + title: "Reaching for flushSync to fix ordering bugs", + explanation: + "flushSync forces a synchronous render and commit in the middle of the handler, which defeats batching and can noticeably hurt performance when overused.", + fix: "Reserve flushSync for cases where the DOM must be current before the next statement, such as measuring a node or scrolling to a freshly inserted item. Everything else belongs in effects or derived state.", + }, + ], + interviewQuestions: [ + { + question: "Why does this handler leave count at 1 instead of 3?", + answer: + "count is a constant captured when the component rendered with value 0. All three calls evaluate count + 1 to 1 and queue three replace-with-1 updates. React processes them in one render and the result is 1.", + codeExample: { + language: "jsx", + code: `function handleClick() { + setCount(count + 1); // queues 1 + setCount(count + 1); // queues 1 + setCount(count + 1); // queues 1 +} +// one render, count === 1`, + }, + }, + { + question: "How does the updater form differ from passing a value?", + answer: + "A value update replaces the state outright when React reaches it in the queue. An updater is stored as a function and called during the render with the pending state at that point, so consecutive updaters chain: 0 to 1 to 2 to 3. Mixing them still runs in call order, so a trailing value overwrites whatever the updaters computed.", + codeExample: { + language: "jsx", + code: `setCount(count + 5); // queue: replace with 5 (count was 0) +setCount(c => c + 1); // queue: 5 -> 6 +setCount(42); // queue: replace with 42 +// one render, count === 42`, + }, + }, + { + question: "What changed about batching in React 18?", + answer: + "React 17 batched updates only inside React event handlers; updates in timeouts, promises, and native listeners each rendered on their own. React 18 with createRoot batches automatically in every callback, so two setState calls inside a .then produce one render instead of two.", + }, + { + question: "When would you use flushSync, and what does it cost?", + answer: + "flushSync runs its callback, then synchronously renders and commits any updates it queued before returning, so code after the call can read the updated DOM. It bypasses batching for that update and blocks the main thread for the render, so it should be limited to layout measurement or scroll positioning after a state change.", + codeExample: { + language: "jsx", + code: `flushSync(() => { + setItems([...items, newItem]); +}); +// DOM already has the new row here +listRef.current.lastChild.scrollIntoView();`, + }, + }, + { + question: "How does batching relate to the event loop?", + answer: + "React does not use a timer to batch. Inside an event handler it holds updates until the handler returns and then flushes synchronously in the same task. For updates issued from a microtask or timer callback, React 18 schedules a flush that runs once that callback finishes, so the batch boundary lines up with a single unit of event loop work.", + }, + ], + relatedTopicIds: [ + "hooks", + "render-cycle", + "concurrent-rendering", + "event-loop", + ], +}; diff --git a/src/content/theory/react/suspense.ts b/src/content/theory/react/suspense.ts index ae674df..3fe391d 100644 --- a/src/content/theory/react/suspense.ts +++ b/src/content/theory/react/suspense.ts @@ -25,7 +25,7 @@ export const suspenseTheory: TopicTheoryContent = { { title: "Wrapping every component in its own Suspense boundary", explanation: - "Too many boundaries cause a staggered \"popcorn\" loading effect where content pops in one piece at a time, creating a poor user experience.", + 'Too many boundaries cause a staggered "popcorn" loading effect where content pops in one piece at a time, creating a poor user experience.', fix: "Group related content within a single boundary. Use nested boundaries only when parts of the page should reveal independently (e.g., main content vs comments).", }, { @@ -42,7 +42,8 @@ export const suspenseTheory: TopicTheoryContent = { "Suspense relies on the thrown-Promise protocol: a component (or its data source) throws a Promise during render. React catches it at the nearest Suspense boundary. This is different from returning a loading state; the component literally cannot finish rendering yet.", }, { - question: "What happens when multiple children in one Suspense boundary are loading?", + question: + "What happens when multiple children in one Suspense boundary are loading?", answer: "The boundary waits for all pending Promises to resolve before revealing any content. This prevents partial UI from appearing and ensures siblings render together in a single commit, avoiding layout shifts.", }, @@ -69,8 +70,8 @@ export const suspenseTheory: TopicTheoryContent = { relatedTopicIds: [ "render-cycle", "fiber-tree", - "reconciliation", "server-components", + "concurrent-rendering", "error-boundaries", ], }; diff --git a/src/content/theory/react/use-effect-lifecycle.ts b/src/content/theory/react/use-effect-lifecycle.ts index 454dfe0..dbbaee0 100644 --- a/src/content/theory/react/use-effect-lifecycle.ts +++ b/src/content/theory/react/use-effect-lifecycle.ts @@ -69,5 +69,11 @@ export const useEffectLifecycleTheory: TopicTheoryContent = { }, ], - relatedTopicIds: ["hooks", "render-cycle", "closures", "memoization", "suspense"], + relatedTopicIds: [ + "hooks", + "render-cycle", + "closures", + "memoization", + "suspense", + ], }; diff --git a/src/hooks/useChangeFlash.test.tsx b/src/hooks/useChangeFlash.test.tsx new file mode 100644 index 0000000..f13c9c5 --- /dev/null +++ b/src/hooks/useChangeFlash.test.tsx @@ -0,0 +1,117 @@ +import { act, renderHook } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useChangeFlash } from "@/hooks/useChangeFlash"; + +type Channels = { heap: unknown; stack: unknown }; +type Props = { channels: Channels; stepIndex: number }; + +const render = (initial: Props) => + renderHook( + ({ channels, stepIndex }: Props) => useChangeFlash(channels, stepIndex), + { + initialProps: initial, + }, + ); + +describe("useChangeFlash", () => { + beforeEach(() => vi.useFakeTimers()); + afterEach(() => vi.useRealTimers()); + + it("starts with every channel off", () => { + const { result } = render({ + channels: { heap: [], stack: [] }, + stepIndex: -1, + }); + expect(result.current).toEqual({ heap: false, stack: false }); + }); + + it("does not flash on the first real step", () => { + const { result, rerender } = render({ + channels: { heap: [], stack: [] }, + stepIndex: -1, + }); + rerender({ channels: { heap: [1], stack: ["a"] }, stepIndex: 0 }); + expect(result.current).toEqual({ heap: false, stack: false }); + }); + + it("flashes only the channels whose data changed", () => { + const { result, rerender } = render({ + channels: { heap: [1], stack: ["a"] }, + stepIndex: 0, + }); + rerender({ channels: { heap: [1, 2], stack: ["a"] }, stepIndex: 1 }); + expect(result.current).toEqual({ heap: true, stack: false }); + }); + + it("compares by value, not by reference", () => { + const { result, rerender } = render({ + channels: { heap: [1], stack: ["a"] }, + stepIndex: 0, + }); + rerender({ channels: { heap: [1], stack: ["a"] }, stepIndex: 1 }); + expect(result.current).toEqual({ heap: false, stack: false }); + }); + + it("clears the flash after 850ms", () => { + const { result, rerender } = render({ + channels: { heap: [1], stack: [] }, + stepIndex: 0, + }); + rerender({ channels: { heap: [2], stack: [] }, stepIndex: 1 }); + expect(result.current.heap).toBe(true); + act(() => vi.advanceTimersByTime(849)); + expect(result.current.heap).toBe(true); + act(() => vi.advanceTimersByTime(1)); + expect(result.current.heap).toBe(false); + }); + + it("restarts the timer when another change lands mid-flash", () => { + const { result, rerender } = render({ + channels: { heap: [1], stack: [] }, + stepIndex: 0, + }); + rerender({ channels: { heap: [2], stack: [] }, stepIndex: 1 }); + act(() => vi.advanceTimersByTime(500)); + rerender({ channels: { heap: [2], stack: ["x"] }, stepIndex: 2 }); + expect(result.current).toEqual({ heap: false, stack: true }); + act(() => vi.advanceTimersByTime(500)); + expect(result.current.stack).toBe(true); + act(() => vi.advanceTimersByTime(350)); + expect(result.current.stack).toBe(false); + }); + + it("flashes when stepping backwards too", () => { + const { result, rerender } = render({ + channels: { heap: [1], stack: [] }, + stepIndex: 1, + }); + rerender({ channels: { heap: [], stack: [] }, stepIndex: 0 }); + expect(result.current.heap).toBe(true); + }); + + it("clears everything and forgets history on reset", () => { + const { result, rerender } = render({ + channels: { heap: [1], stack: [] }, + stepIndex: 0, + }); + rerender({ channels: { heap: [2], stack: [] }, stepIndex: 1 }); + expect(result.current.heap).toBe(true); + rerender({ channels: { heap: [], stack: [] }, stepIndex: -1 }); + expect(result.current).toEqual({ heap: false, stack: false }); + act(() => vi.advanceTimersByTime(1000)); + // After a reset the next step is a "first step" again and must not flash. + rerender({ channels: { heap: [9], stack: [] }, stepIndex: 0 }); + expect(result.current).toEqual({ heap: false, stack: false }); + }); + + it("does not update state after unmount", () => { + const { rerender, unmount } = render({ + channels: { heap: [1], stack: [] }, + stepIndex: 0, + }); + rerender({ channels: { heap: [2], stack: [] }, stepIndex: 1 }); + unmount(); + expect(() => vi.advanceTimersByTime(1000)).not.toThrow(); + expect(vi.getTimerCount()).toBe(0); + }); +}); diff --git a/src/hooks/useClickOutside.test.tsx b/src/hooks/useClickOutside.test.tsx new file mode 100644 index 0000000..d141479 --- /dev/null +++ b/src/hooks/useClickOutside.test.tsx @@ -0,0 +1,58 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { useClickOutside } from "@/hooks/useClickOutside"; + +const Popover = ({ open, onClose }: { open: boolean; onClose: () => void }) => { + const ref = useClickOutside(open, onClose); + return ( +
    +
    + +
    + +
    + ); +}; + +describe("useClickOutside", () => { + it("closes on a mousedown outside the element", () => { + const onClose = vi.fn(); + render(); + fireEvent.mouseDown(screen.getByText("outside")); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("ignores mousedown inside the element", () => { + const onClose = vi.fn(); + render(); + fireEvent.mouseDown(screen.getByText("inner")); + fireEvent.mouseDown(screen.getByTestId("inside")); + expect(onClose).not.toHaveBeenCalled(); + }); + + it("closes on Escape and ignores other keys", () => { + const onClose = vi.fn(); + render(); + fireEvent.keyDown(document, { key: "Enter" }); + expect(onClose).not.toHaveBeenCalled(); + fireEvent.keyDown(document, { key: "Escape" }); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("does nothing while closed", () => { + const onClose = vi.fn(); + render(); + fireEvent.mouseDown(screen.getByText("outside")); + fireEvent.keyDown(document, { key: "Escape" }); + expect(onClose).not.toHaveBeenCalled(); + }); + + it("removes its listeners on unmount", () => { + const onClose = vi.fn(); + const { unmount } = render(); + unmount(); + fireEvent.mouseDown(document.body); + fireEvent.keyDown(document, { key: "Escape" }); + expect(onClose).not.toHaveBeenCalled(); + }); +}); diff --git a/src/hooks/useExampleTopic.test.tsx b/src/hooks/useExampleTopic.test.tsx new file mode 100644 index 0000000..acecb4b --- /dev/null +++ b/src/hooks/useExampleTopic.test.tsx @@ -0,0 +1,73 @@ +import { act, renderHook } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { useExampleTopic } from "@/hooks/useExampleTopic"; + +const EXAMPLES = [ + { + id: "first", + steps: [ + { descriptionHtml: "a1", activeLine: 1, doneLines: [] }, + { descriptionHtml: "a2", activeLine: 2, doneLines: [1] }, + ], + }, + { + id: "second", + steps: [ + { + descriptionHtml: "b1", + activeLine: 1, + doneLines: [], + simpleHtml: "plain", + }, + ], + }, +] as const; + +describe("useExampleTopic", () => { + it("starts on the first example before the first step", () => { + const { result } = renderHook(() => useExampleTopic(EXAMPLES)); + expect(result.current.activeExampleId).toBe("first"); + expect(result.current.example).toBe(EXAMPLES[0]); + expect(result.current.currentStep).toBeNull(); + expect(result.current.playback.currentStepIndex).toBe(-1); + }); + + it("exposes the step at the current index", () => { + const { result } = renderHook(() => useExampleTopic(EXAMPLES)); + act(() => result.current.playback.step()); + expect(result.current.currentStep).toBe(EXAMPLES[0].steps[0]); + act(() => result.current.playback.step()); + expect(result.current.currentStep).toBe(EXAMPLES[0].steps[1]); + expect(result.current.playback.canStep).toBe(false); + }); + + it("switches examples and restarts playback", () => { + const { result } = renderHook(() => useExampleTopic(EXAMPLES)); + act(() => result.current.playback.step()); + act(() => result.current.handleExampleChange("second")); + expect(result.current.activeExampleId).toBe("second"); + expect(result.current.example).toBe(EXAMPLES[1]); + expect(result.current.playback.currentStepIndex).toBe(-1); + expect(result.current.currentStep).toBeNull(); + }); + + it("falls back to the first example for an unknown id", () => { + const { result } = renderHook(() => useExampleTopic(EXAMPLES)); + act(() => result.current.handleExampleChange("missing")); + expect(result.current.example).toBe(EXAMPLES[0]); + }); + + it("reports whether the active example has plain-language text", () => { + const { result } = renderHook(() => useExampleTopic(EXAMPLES)); + expect(result.current.hasSimpleText).toBe(false); + act(() => result.current.handleExampleChange("second")); + expect(result.current.hasSimpleText).toBe(true); + }); + + it("keeps a stable example change handler", () => { + const { result, rerender } = renderHook(() => useExampleTopic(EXAMPLES)); + const handler = result.current.handleExampleChange; + rerender(); + expect(result.current.handleExampleChange).toBe(handler); + }); +}); diff --git a/src/hooks/useExampleTopic.ts b/src/hooks/useExampleTopic.ts new file mode 100644 index 0000000..2575a77 --- /dev/null +++ b/src/hooks/useExampleTopic.ts @@ -0,0 +1,73 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; +import { useStepPlayback, type StepPlayback } from "@/hooks/useStepPlayback"; + +interface ExampleLike { + id: string; + steps: readonly unknown[]; +} + +type StepOf = TExample extends { steps: readonly (infer TStep)[] } + ? TStep + : never; + +export interface ExampleTopic { + example: TExample; + activeExampleId: string; + handleExampleChange: (id: string) => void; + playback: StepPlayback; + /** The step at the current index, or null before playback starts. */ + currentStep: StepOf | null; + /** True when any step of the active example authored a simpleHtml. */ + hasSimpleText: boolean; +} + +/** + * State shared by every topic with an example selector: which example is + * active, playback over its steps, and the step currently on screen. + * Switching examples restarts playback. + */ +export const useExampleTopic = ( + examples: readonly TExample[], +): ExampleTopic => { + const [activeExampleId, setActiveExampleId] = useState(examples[0].id); + + const handleExampleChange = useCallback((id: string) => { + setActiveExampleId(id); + }, []); + + const example = + examples.find((entry) => entry.id === activeExampleId) ?? examples[0]; + + const playback = useStepPlayback({ + totalSteps: example.steps.length, + resetKey: activeExampleId, + }); + + const currentStep = + playback.currentStepIndex >= 0 + ? (example.steps[playback.currentStepIndex] as StepOf) + : null; + + const hasSimpleText = useMemo( + () => + example.steps.some( + (step) => + typeof step === "object" && + step !== null && + "simpleHtml" in step && + Boolean((step as { simpleHtml?: string }).simpleHtml), + ), + [example], + ); + + return { + example, + activeExampleId, + handleExampleChange, + playback, + currentStep, + hasSimpleText, + }; +}; diff --git a/src/hooks/useExplanationMode.test.tsx b/src/hooks/useExplanationMode.test.tsx new file mode 100644 index 0000000..1aa4e1a --- /dev/null +++ b/src/hooks/useExplanationMode.test.tsx @@ -0,0 +1,35 @@ +import { act, renderHook } from "@testing-library/react"; +import { beforeEach, describe, expect, it } from "vitest"; +import { STORAGE_KEYS } from "@/content/static/storageKeys"; +import { useExplanationMode } from "@/hooks/useExplanationMode"; +import { writeExplanationMode } from "@/lib/explanationMode"; + +describe("useExplanationMode", () => { + beforeEach(() => { + writeExplanationMode("detailed"); + }); + + it("reads the stored mode and persists changes", () => { + const { result } = renderHook(() => useExplanationMode()); + expect(result.current.mode).toBe("detailed"); + act(() => result.current.setMode("simple")); + expect(result.current.mode).toBe("simple"); + expect(window.localStorage.getItem(STORAGE_KEYS.explanationMode)).toBe( + "simple", + ); + }); + + it("shares the preference between consumers", () => { + const a = renderHook(() => useExplanationMode()); + const b = renderHook(() => useExplanationMode()); + act(() => a.result.current.setMode("simple")); + expect(b.result.current.mode).toBe("simple"); + }); + + it("keeps a stable setter", () => { + const { result, rerender } = renderHook(() => useExplanationMode()); + const setter = result.current.setMode; + rerender(); + expect(result.current.setMode).toBe(setter); + }); +}); diff --git a/src/hooks/useExplanationMode.ts b/src/hooks/useExplanationMode.ts new file mode 100644 index 0000000..9388890 --- /dev/null +++ b/src/hooks/useExplanationMode.ts @@ -0,0 +1,23 @@ +"use client"; + +import { useCallback, useSyncExternalStore } from "react"; +import { + getServerExplanationMode, + readExplanationMode, + subscribeExplanationMode, + writeExplanationMode, + type ExplanationMode, +} from "@/lib/explanationMode"; + +/** The reader's Simple / Detailed preference, shared across every topic. */ +export const useExplanationMode = () => { + const mode = useSyncExternalStore( + subscribeExplanationMode, + readExplanationMode, + getServerExplanationMode, + ); + const setMode = useCallback((next: ExplanationMode) => { + writeExplanationMode(next); + }, []); + return { mode, setMode }; +}; diff --git a/src/hooks/useFocusTrap.test.tsx b/src/hooks/useFocusTrap.test.tsx new file mode 100644 index 0000000..faffe1b --- /dev/null +++ b/src/hooks/useFocusTrap.test.tsx @@ -0,0 +1,93 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { useRef } from "react"; +import { describe, expect, it } from "vitest"; +import { useFocusTrap } from "@/hooks/useFocusTrap"; + +const Dialog = ({ + active, + empty = false, +}: { + active: boolean; + empty?: boolean; +}) => { + const ref = useRef(null); + useFocusTrap(ref, active); + return ( +
    + {!empty && ( + <> + + middle + + + )} +
    + ); +}; + +describe("useFocusTrap", () => { + it("focuses the first focusable element when activated", () => { + render(); + expect(screen.getByText("first")).toHaveFocus(); + }); + + it("does nothing while inactive", () => { + const outside = document.createElement("button"); + document.body.append(outside); + outside.focus(); + render(); + expect(outside).toHaveFocus(); + outside.remove(); + }); + + it("wraps Tab from the last element to the first", () => { + render(); + const dialog = screen.getByTestId("dialog"); + screen.getByText("last").focus(); + fireEvent.keyDown(dialog, { key: "Tab" }); + expect(screen.getByText("first")).toHaveFocus(); + }); + + it("wraps Shift+Tab from the first element to the last", () => { + render(); + const dialog = screen.getByTestId("dialog"); + fireEvent.keyDown(dialog, { key: "Tab", shiftKey: true }); + expect(screen.getByText("last")).toHaveFocus(); + }); + + it("lets Tab move normally in the middle of the trap", () => { + render(); + const dialog = screen.getByTestId("dialog"); + screen.getByText("middle").focus(); + const event = new KeyboardEvent("keydown", { + key: "Tab", + bubbles: true, + cancelable: true, + }); + dialog.dispatchEvent(event); + expect(event.defaultPrevented).toBe(false); + expect(screen.getByText("middle")).toHaveFocus(); + }); + + it("swallows Tab when nothing inside is focusable", () => { + render(); + const event = new KeyboardEvent("keydown", { + key: "Tab", + bubbles: true, + cancelable: true, + }); + screen.getByTestId("dialog").dispatchEvent(event); + expect(event.defaultPrevented).toBe(true); + }); + + it("restores focus to the previously focused element on deactivate", () => { + const opener = document.createElement("button"); + document.body.append(opener); + opener.focus(); + const { rerender } = render(); + expect(screen.getByText("first")).toHaveFocus(); + rerender(); + expect(opener).toHaveFocus(); + opener.remove(); + }); +}); diff --git a/src/hooks/useFocusTrap.ts b/src/hooks/useFocusTrap.ts index d12d896..5be063f 100644 --- a/src/hooks/useFocusTrap.ts +++ b/src/hooks/useFocusTrap.ts @@ -18,13 +18,15 @@ export const useFocusTrap = ( if (!container) return; // Focus the first focusable element - const firstFocusable = container.querySelector(FOCUSABLE_SELECTOR); + const firstFocusable = + container.querySelector(FOCUSABLE_SELECTOR); firstFocusable?.focus(); const onKeyDown = (event: KeyboardEvent) => { if (event.key !== "Tab") return; - const focusableElements = container.querySelectorAll(FOCUSABLE_SELECTOR); + const focusableElements = + container.querySelectorAll(FOCUSABLE_SELECTOR); if (focusableElements.length === 0) { event.preventDefault(); return; diff --git a/src/hooks/useIsHoverDevice.test.tsx b/src/hooks/useIsHoverDevice.test.tsx new file mode 100644 index 0000000..42732a4 --- /dev/null +++ b/src/hooks/useIsHoverDevice.test.tsx @@ -0,0 +1,78 @@ +import { act, renderHook } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { useIsHoverDevice } from "@/hooks/useIsHoverDevice"; + +type Listener = () => void; + +const installMatchMedia = (matches: boolean, { legacy = false } = {}) => { + const listeners = new Set(); + const mql = { + matches, + media: "", + addEventListener: legacy + ? undefined + : vi.fn((_: string, cb: Listener) => listeners.add(cb)), + removeEventListener: legacy + ? undefined + : vi.fn((_: string, cb: Listener) => listeners.delete(cb)), + addListener: vi.fn((cb: Listener) => listeners.add(cb)), + removeListener: vi.fn((cb: Listener) => listeners.delete(cb)), + }; + const matchMedia = vi.fn(() => mql); + vi.stubGlobal("matchMedia", matchMedia); + return { + mql, + matchMedia, + change(next: boolean) { + mql.matches = next; + listeners.forEach((listener) => listener()); + }, + get listenerCount() { + return listeners.size; + }, + }; +}; + +describe("useIsHoverDevice", () => { + it("reports the hover media query result", () => { + const media = installMatchMedia(true); + const { result } = renderHook(() => useIsHoverDevice()); + expect(result.current).toBe(true); + expect(media.matchMedia).toHaveBeenCalledWith( + "(hover: hover) and (pointer: fine)", + ); + }); + + it("updates when the media query changes", () => { + const media = installMatchMedia(false); + const { result } = renderHook(() => useIsHoverDevice()); + expect(result.current).toBe(false); + act(() => media.change(true)); + expect(result.current).toBe(true); + }); + + it("shares one media query across consumers and detaches after the last unmounts", () => { + const media = installMatchMedia(true); + const a = renderHook(() => useIsHoverDevice()); + const b = renderHook(() => useIsHoverDevice()); + expect(media.matchMedia).toHaveBeenCalledTimes(1); + expect(media.listenerCount).toBe(1); + + a.unmount(); + expect(media.listenerCount).toBe(1); + b.unmount(); + expect(media.listenerCount).toBe(0); + expect(media.mql.removeEventListener).toHaveBeenCalledTimes(1); + }); + + it("falls back to addListener on older browsers", () => { + const media = installMatchMedia(true, { legacy: true }); + const { result, unmount } = renderHook(() => useIsHoverDevice()); + expect(result.current).toBe(true); + expect(media.mql.addListener).toHaveBeenCalledTimes(1); + act(() => media.change(false)); + expect(result.current).toBe(false); + unmount(); + expect(media.mql.removeListener).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/hooks/useLocationSearch.test.tsx b/src/hooks/useLocationSearch.test.tsx new file mode 100644 index 0000000..ea0c6e5 --- /dev/null +++ b/src/hooks/useLocationSearch.test.tsx @@ -0,0 +1,46 @@ +import { act, renderHook } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { useLocationSearch } from "./useLocationSearch"; + +describe("useLocationSearch", () => { + afterEach(() => { + window.history.replaceState(null, "", "/"); + }); + + it("reads the current query string", () => { + window.history.replaceState(null, "", "/react?level=advanced"); + const { result } = renderHook(() => useLocationSearch()); + expect(result.current.search).toBe("?level=advanced"); + }); + + it("replaces the query in place and keeps path and hash", () => { + window.history.replaceState(null, "", "/react#cards"); + const { result } = renderHook(() => useLocationSearch()); + const length = window.history.length; + act(() => result.current.replace("?level=beginner")); + expect(result.current.search).toBe("?level=beginner"); + expect(window.location.pathname).toBe("/react"); + expect(window.location.hash).toBe("#cards"); + expect(window.history.length).toBe(length); + act(() => result.current.replace("")); + expect(result.current.search).toBe(""); + expect(window.location.href.endsWith("/react#cards")).toBe(true); + }); + + it("follows back and forward navigation", () => { + const { result } = renderHook(() => useLocationSearch()); + act(() => { + window.history.replaceState(null, "", "/react?level=intermediate"); + window.dispatchEvent(new PopStateEvent("popstate")); + }); + expect(result.current.search).toBe("?level=intermediate"); + }); + + it("stops listening after unmount", () => { + const { result, unmount } = renderHook(() => useLocationSearch()); + unmount(); + window.history.replaceState(null, "", "/react?level=advanced"); + window.dispatchEvent(new PopStateEvent("popstate")); + expect(result.current.search).toBe(""); + }); +}); diff --git a/src/hooks/useLocationSearch.ts b/src/hooks/useLocationSearch.ts new file mode 100644 index 0000000..adbb934 --- /dev/null +++ b/src/hooks/useLocationSearch.ts @@ -0,0 +1,41 @@ +import { useCallback, useSyncExternalStore } from "react"; + +const CHANGE_EVENT = "visualizejs:locationchange"; + +const subscribe = (onChange: () => void) => { + window.addEventListener("popstate", onChange); + window.addEventListener(CHANGE_EVENT, onChange); + return () => { + window.removeEventListener("popstate", onChange); + window.removeEventListener(CHANGE_EVENT, onChange); + }; +}; + +const getSnapshot = () => window.location.search; +const getServerSnapshot = () => ""; + +/** + * The current query string, kept in sync with the URL. The server and the + * first client render see an empty string so static pages hydrate cleanly; + * the real value is applied right after. `replace` rewrites the query without + * adding a history entry and without touching the path or hash. + */ +export const useLocationSearch = () => { + const search = useSyncExternalStore( + subscribe, + getSnapshot, + getServerSnapshot, + ); + + const replace = useCallback((nextSearch: string) => { + const { pathname, hash } = window.location; + window.history.replaceState( + window.history.state, + "", + `${pathname}${nextSearch}${hash}`, + ); + window.dispatchEvent(new Event(CHANGE_EVENT)); + }, []); + + return { search, replace }; +}; diff --git a/src/hooks/useSandboxMode.test.tsx b/src/hooks/useSandboxMode.test.tsx new file mode 100644 index 0000000..77cdad4 --- /dev/null +++ b/src/hooks/useSandboxMode.test.tsx @@ -0,0 +1,203 @@ +import { act, renderHook } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { STORAGE_KEYS } from "@/content/static/storageKeys"; +import { useSandboxMode } from "@/hooks/useSandboxMode"; +import type { SandboxConfig, StepGenerator } from "@/types/sandbox"; + +const CONFIG: SandboxConfig = { + topicId: "test-topic", + defaultCode: "console.log('a');", + supportedPatterns: [], + maxCodeLines: 3, + maxCodeLength: 60, +}; + +const STORAGE_KEY = `${STORAGE_KEYS.sandboxPrefix}${CONFIG.topicId}`; + +type Step = { label: string }; +type Line = { num: number; text: string }; + +const okGenerator: StepGenerator = (ast, source) => ({ + success: true, + steps: ast.body.map((node) => ({ label: node.type })), + codeLines: source.split("\n").map((text, i) => ({ num: i + 1, text })), +}); + +const failingGenerator: StepGenerator = () => ({ + success: false, + error: { type: "unsupported-pattern", message: "nope", line: 1 }, +}); + +describe("useSandboxMode", () => { + beforeEach(() => vi.useFakeTimers()); + afterEach(() => vi.useRealTimers()); + + it("starts inactive with the default code and no output", () => { + const { result } = renderHook(() => useSandboxMode(CONFIG, okGenerator)); + expect(result.current.isSandboxActive).toBe(false); + expect(result.current.userCode).toBe(CONFIG.defaultCode); + expect(result.current.generatedSteps).toBeNull(); + expect(result.current.generatedCodeLines).toBeNull(); + expect(result.current.error).toBeNull(); + expect(result.current.generationId).toBe(0); + expect(result.current.codeVersion).toBe(0); + }); + + it("restores code saved by a previous visit, trimmed to the limit", () => { + window.localStorage.setItem(STORAGE_KEY, "x".repeat(100)); + const { result } = renderHook(() => useSandboxMode(CONFIG, okGenerator)); + expect(result.current.userCode).toBe("x".repeat(60)); + }); + + it("truncates edits to the maximum length and saves them after a debounce", () => { + const { result } = renderHook(() => useSandboxMode(CONFIG, okGenerator)); + act(() => result.current.setUserCode("y".repeat(70))); + expect(result.current.userCode).toBe("y".repeat(60)); + expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull(); + + act(() => result.current.setUserCode("final")); + act(() => vi.advanceTimersByTime(499)); + expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull(); + act(() => vi.advanceTimersByTime(1)); + expect(window.localStorage.getItem(STORAGE_KEY)).toBe("final"); + }); + + it("does not save after unmount", () => { + const { result, unmount } = renderHook(() => + useSandboxMode(CONFIG, okGenerator), + ); + act(() => result.current.setUserCode("pending")); + unmount(); + act(() => vi.advanceTimersByTime(1000)); + expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull(); + }); + + it("generates steps and code lines from valid code", () => { + const { result } = renderHook(() => useSandboxMode(CONFIG, okGenerator)); + act(() => result.current.setUserCode("a();\nb();")); + let ok = false; + act(() => { + ok = result.current.generateSteps(); + }); + expect(ok).toBe(true); + expect(result.current.error).toBeNull(); + expect(result.current.generatedSteps).toEqual([ + { label: "ExpressionStatement" }, + { label: "ExpressionStatement" }, + ]); + expect(result.current.generatedCodeLines).toEqual([ + { num: 1, text: "a();" }, + { num: 2, text: "b();" }, + ]); + expect(result.current.generationId).toBe(1); + + act(() => { + result.current.generateSteps(); + }); + expect(result.current.generationId).toBe(2); + }); + + it("rejects code with too many lines", () => { + const { result } = renderHook(() => useSandboxMode(CONFIG, okGenerator)); + act(() => result.current.setUserCode("a();\nb();\nc();\nd();")); + let ok = true; + act(() => { + ok = result.current.generateSteps(); + }); + expect(ok).toBe(false); + expect(result.current.error).toEqual({ + type: "code-too-long", + message: "Code is too long (4 lines). Maximum is 3 lines.", + }); + expect(result.current.generatedSteps).toBeNull(); + }); + + it("reports parse errors with their position", () => { + const { result } = renderHook(() => useSandboxMode(CONFIG, okGenerator)); + act(() => result.current.setUserCode("a();\nconst = 1;")); + act(() => { + result.current.generateSteps(); + }); + expect(result.current.error).toMatchObject({ + type: "parse-error", + line: 2, + column: 6, + }); + }); + + it("surfaces generator errors and keeps the previous output untouched", () => { + const { result, rerender } = renderHook( + (generator: StepGenerator) => + useSandboxMode(CONFIG, generator), + { initialProps: okGenerator }, + ); + act(() => { + result.current.generateSteps(); + }); + const steps = result.current.generatedSteps; + const codeLines = result.current.generatedCodeLines; + expect(steps).not.toBeNull(); + expect(result.current.generationId).toBe(1); + + rerender(failingGenerator); + let ok = true; + act(() => { + ok = result.current.generateSteps(); + }); + expect(ok).toBe(false); + expect(result.current.error).toEqual({ + type: "unsupported-pattern", + message: "nope", + line: 1, + }); + expect(result.current.generatedSteps).toBe(steps); + expect(result.current.generatedCodeLines).toBe(codeLines); + expect(result.current.generationId).toBe(1); + }); + + it("clears a previous error on the next successful generation", () => { + const { result } = renderHook(() => useSandboxMode(CONFIG, okGenerator)); + act(() => result.current.setUserCode("const = 1;")); + act(() => { + result.current.generateSteps(); + }); + expect(result.current.error).not.toBeNull(); + act(() => result.current.setUserCode("ok();")); + act(() => { + result.current.generateSteps(); + }); + expect(result.current.error).toBeNull(); + }); + + it("resets to the default code, saves it, and drops generated output", () => { + const { result } = renderHook(() => useSandboxMode(CONFIG, okGenerator)); + act(() => result.current.setUserCode("changed();")); + act(() => { + result.current.generateSteps(); + }); + act(() => result.current.resetCode()); + expect(result.current.userCode).toBe(CONFIG.defaultCode); + expect(window.localStorage.getItem(STORAGE_KEY)).toBe(CONFIG.defaultCode); + expect(result.current.generatedSteps).toBeNull(); + expect(result.current.generatedCodeLines).toBeNull(); + expect(result.current.error).toBeNull(); + expect(result.current.codeVersion).toBe(1); + }); + + it("clears generated output when the sandbox is switched off", () => { + const { result } = renderHook(() => useSandboxMode(CONFIG, okGenerator)); + act(() => result.current.toggleSandbox()); + expect(result.current.isSandboxActive).toBe(true); + act(() => { + result.current.generateSteps(); + }); + expect(result.current.generatedSteps).not.toBeNull(); + + act(() => result.current.toggleSandbox()); + expect(result.current.isSandboxActive).toBe(false); + expect(result.current.generatedSteps).toBeNull(); + expect(result.current.generatedCodeLines).toBeNull(); + expect(result.current.error).toBeNull(); + expect(result.current.userCode).toBe(CONFIG.defaultCode); + }); +}); diff --git a/src/hooks/useSandboxMode.ts b/src/hooks/useSandboxMode.ts index a4abf2f..39abb70 100644 --- a/src/hooks/useSandboxMode.ts +++ b/src/hooks/useSandboxMode.ts @@ -3,7 +3,11 @@ import { useState, useCallback, useEffect, useRef } from "react"; import { STORAGE_KEYS } from "@/content/static/storageKeys"; import { parseUserCode } from "@/lib/sandbox/parser"; -import type { SandboxConfig, SandboxError, StepGenerator } from "@/types/sandbox"; +import type { + SandboxConfig, + SandboxError, + StepGenerator, +} from "@/types/sandbox"; interface SandboxModeReturn { isSandboxActive: boolean; @@ -63,7 +67,9 @@ export function useSandboxMode( ), ); const [generatedSteps, setGeneratedSteps] = useState(null); - const [generatedCodeLines, setGeneratedCodeLines] = useState(null); + const [generatedCodeLines, setGeneratedCodeLines] = useState< + TCodeLine[] | null + >(null); const [error, setError] = useState(null); const [generationId, setGenerationId] = useState(0); const [codeVersion, setCodeVersion] = useState(0); @@ -147,16 +153,14 @@ export function useSandboxMode( }, [config.defaultCode, config.maxCodeLength, config.topicId]); const toggleSandbox = useCallback(() => { - setIsSandboxActive((prev) => { - if (prev) { - // Turning off - clear generated state - setGeneratedSteps(null); - setGeneratedCodeLines(null); - setError(null); - } - return !prev; - }); - }, []); + if (isSandboxActive) { + // Turning off clears everything the last generation produced. + setGeneratedSteps(null); + setGeneratedCodeLines(null); + setError(null); + } + setIsSandboxActive(!isSandboxActive); + }, [isSandboxActive]); // Cleanup timer on unmount useEffect(() => { diff --git a/src/hooks/useSandboxUIState.test.tsx b/src/hooks/useSandboxUIState.test.tsx new file mode 100644 index 0000000..2da74de --- /dev/null +++ b/src/hooks/useSandboxUIState.test.tsx @@ -0,0 +1,119 @@ +import { act, renderHook } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { useSandboxUIState } from "@/hooks/useSandboxUIState"; +import type { SandboxConfig, StepGenerator } from "@/types/sandbox"; + +const CONFIG: SandboxConfig = { + topicId: "ui-topic", + defaultCode: "run();", + supportedPatterns: [], + maxCodeLines: 5, + maxCodeLength: 100, +}; + +const okGenerator: StepGenerator<{ n: number }, string> = () => ({ + success: true, + steps: [{ n: 1 }], + codeLines: ["run();"], +}); + +const failingGenerator: StepGenerator<{ n: number }, string> = () => ({ + success: false, + error: { type: "generation-error", message: "boom" }, +}); + +const cmdEnter = (init: KeyboardEventInit = {}) => { + const event = new KeyboardEvent("keydown", { + key: "Enter", + metaKey: true, + cancelable: true, + ...init, + }); + act(() => { + window.dispatchEvent(event); + }); + return event; +}; + +describe("useSandboxUIState", () => { + it("starts in editing mode without a sandbox", () => { + const { result } = renderHook(() => useSandboxUIState(CONFIG, okGenerator)); + expect(result.current.isEditing).toBe(true); + expect(result.current.usingSandbox).toBe(false); + expect(result.current.showHighlightedCode).toBe(false); + }); + + it("switches to the highlighted view only after a successful generation", () => { + const { result } = renderHook(() => useSandboxUIState(CONFIG, okGenerator)); + act(() => result.current.handleToggleSandbox()); + expect(result.current.sandbox.isSandboxActive).toBe(true); + expect(result.current.usingSandbox).toBe(false); + + act(() => result.current.handleGenerate()); + expect(result.current.isEditing).toBe(false); + expect(result.current.usingSandbox).toBe(true); + expect(result.current.showHighlightedCode).toBe(true); + + act(() => result.current.handleEditCode()); + expect(result.current.isEditing).toBe(true); + expect(result.current.usingSandbox).toBe(true); + expect(result.current.showHighlightedCode).toBe(false); + }); + + it("stays in the editor when generation fails", () => { + const { result } = renderHook(() => + useSandboxUIState(CONFIG, failingGenerator), + ); + act(() => result.current.handleToggleSandbox()); + act(() => result.current.handleGenerate()); + expect(result.current.isEditing).toBe(true); + expect(result.current.sandbox.error?.message).toBe("boom"); + }); + + it("returns to editing mode whenever the sandbox is toggled", () => { + const { result } = renderHook(() => useSandboxUIState(CONFIG, okGenerator)); + act(() => result.current.handleToggleSandbox()); + act(() => result.current.handleGenerate()); + expect(result.current.isEditing).toBe(false); + act(() => result.current.handleToggleSandbox()); + expect(result.current.isEditing).toBe(true); + expect(result.current.sandbox.isSandboxActive).toBe(false); + }); + + it("generates on Cmd/Ctrl+Enter while the editor is open", () => { + const generator = vi.fn(okGenerator); + const { result } = renderHook(() => useSandboxUIState(CONFIG, generator)); + + cmdEnter(); + expect(generator).not.toHaveBeenCalled(); + + act(() => result.current.handleToggleSandbox()); + const event = cmdEnter(); + expect(generator).toHaveBeenCalledTimes(1); + expect(event.defaultPrevented).toBe(true); + expect(result.current.isEditing).toBe(false); + + cmdEnter({ metaKey: false, ctrlKey: true }); + expect(generator).toHaveBeenCalledTimes(1); + + act(() => result.current.handleEditCode()); + cmdEnter({ metaKey: false, ctrlKey: true }); + expect(generator).toHaveBeenCalledTimes(2); + }); + + it("leaves the shortcut alone when the editor already handled it", () => { + const generator = vi.fn(okGenerator); + const { result } = renderHook(() => useSandboxUIState(CONFIG, generator)); + act(() => result.current.handleToggleSandbox()); + const handled = new KeyboardEvent("keydown", { + key: "Enter", + metaKey: true, + cancelable: true, + }); + handled.preventDefault(); + act(() => { + window.dispatchEvent(handled); + }); + expect(generator).not.toHaveBeenCalled(); + }); +}); diff --git a/src/hooks/useSandboxUIState.ts b/src/hooks/useSandboxUIState.ts index ed7c93a..b23742a 100644 --- a/src/hooks/useSandboxUIState.ts +++ b/src/hooks/useSandboxUIState.ts @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useEffect, useRef } from "react"; +import { useState, useCallback, useEffect } from "react"; import { useSandboxMode } from "@/hooks/useSandboxMode"; import type { SandboxConfig, StepGenerator } from "@/types/sandbox"; @@ -12,32 +12,25 @@ import type { SandboxConfig, StepGenerator } from "@/types/sandbox"; * - `handleGenerate` only switches to view mode on success * - `handleEditCode` returns to the editor * - Global `Cmd/Ctrl+Enter` keyboard shortcut (works even when editor isn't focused) - * - * Usage in a visualization component: - * ```ts - * const { sandbox, isEditing, usingSandbox, showHighlightedCode, - * handleToggleSandbox, handleGenerate, handleEditCode } = - * useSandboxUIState(config, generator); - * ``` */ -export function useSandboxUIState( +export const useSandboxUIState = ( config: SandboxConfig, generator: StepGenerator, -) { +) => { const sandbox = useSandboxMode(config, generator); + const { toggleSandbox, generateSteps, isSandboxActive } = sandbox; const [isEditing, setIsEditing] = useState(true); // Reset to editing mode whenever the sandbox is toggled on const handleToggleSandbox = useCallback(() => { - sandbox.toggleSandbox(); + toggleSandbox(); setIsEditing(true); - }, [sandbox]); + }, [toggleSandbox]); // Only switch to view mode when generation succeeds const handleGenerate = useCallback(() => { - const ok = sandbox.generateSteps(); - if (ok) setIsEditing(false); - }, [sandbox]); + if (generateSteps()) setIsEditing(false); + }, [generateSteps]); const handleEditCode = useCallback(() => { setIsEditing(true); @@ -46,42 +39,39 @@ export function useSandboxUIState( // Global Cmd/Ctrl+Enter shortcut for when the editor exists but isn't focused. // If the editor IS focused, CodeMirror's Mod-Enter keybinding handles it // (and calls preventDefault, so we skip it here to avoid double-firing). - const handleGenerateRef = useRef(handleGenerate); - - useEffect(() => { - handleGenerateRef.current = handleGenerate; - }, [handleGenerate]); - useEffect(() => { - if (!sandbox.isSandboxActive || !isEditing) return; - const handler = (e: KeyboardEvent) => { - if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !e.defaultPrevented) { - e.preventDefault(); - handleGenerateRef.current(); + if (!isSandboxActive || !isEditing) return; + const handler = (event: KeyboardEvent) => { + if ( + event.key === "Enter" && + (event.metaKey || event.ctrlKey) && + !event.defaultPrevented + ) { + event.preventDefault(); + handleGenerate(); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); - }, [sandbox.isSandboxActive, isEditing]); + }, [isSandboxActive, isEditing, handleGenerate]); - // Derived state - const usingSandbox = sandbox.isSandboxActive && sandbox.generatedSteps !== null; + const usingSandbox = isSandboxActive && sandbox.generatedSteps !== null; const showHighlightedCode = usingSandbox && !isEditing; return { /** The underlying sandbox mode state (userCode, error, generatedSteps, etc.) */ sandbox, - /** Whether the user is currently editing code (true) or viewing the highlighted result (false) */ + /** Whether the user is editing code (true) or viewing the highlighted result (false) */ isEditing, /** Whether sandbox is active AND has generated steps */ usingSandbox, /** Whether to show highlighted code (usingSandbox && !isEditing) */ showHighlightedCode, - /** Toggle sandbox on/off - always resets to editing mode */ + /** Toggle sandbox on/off, always resets to editing mode */ handleToggleSandbox, - /** Generate steps from current code - switches to view mode on success */ + /** Generate steps from current code, switches to view mode on success */ handleGenerate, /** Return to the code editor */ handleEditCode, }; -} +}; diff --git a/src/hooks/useStepPlayback.test.tsx b/src/hooks/useStepPlayback.test.tsx new file mode 100644 index 0000000..f87e740 --- /dev/null +++ b/src/hooks/useStepPlayback.test.tsx @@ -0,0 +1,370 @@ +import { act, fireEvent, renderHook } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { TopicProgressProvider } from "@/components/progress/TopicProgressContext"; +import { SPEED_TO_DELAY_MS } from "@/components/visualization-ui/TransportControls/constants"; +import { useStepPlayback } from "@/hooks/useStepPlayback"; +import { markTopicCompleted } from "@/lib/progress/topicProgress"; + +vi.mock("@/lib/progress/topicProgress", () => ({ + markTopicCompleted: vi.fn(), +})); + +const withTopic = (topicId: string) => { + const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + return Wrapper; +}; + +const keyDown = (key: string, init: KeyboardEventInit = {}) => { + const event = new KeyboardEvent("keydown", { + key, + bubbles: true, + cancelable: true, + ...init, + }); + act(() => { + window.dispatchEvent(event); + }); + return event; +}; + +describe("useStepPlayback", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe("initial state", () => { + it("starts before the first step at normal speed", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 4 })); + expect(result.current.currentStepIndex).toBe(-1); + expect(result.current.isPlaying).toBe(false); + expect(result.current.speedLevel).toBe(4); + expect(result.current.speedLabel).toBe("1x"); + expect(result.current.canStep).toBe(true); + expect(result.current.canStepBack).toBe(false); + }); + + it("honours an initial step", () => { + const { result } = renderHook(() => + useStepPlayback({ totalSteps: 4, initialStep: 2 }), + ); + expect(result.current.currentStepIndex).toBe(2); + // The initial step is the floor, so there is nothing to step back to yet. + expect(result.current.canStepBack).toBe(false); + act(() => result.current.step()); + expect(result.current.canStepBack).toBe(true); + act(() => result.current.stepBack()); + act(() => result.current.stepBack()); + expect(result.current.currentStepIndex).toBe(2); + }); + + it("cannot step anywhere with no steps", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 0 })); + expect(result.current.canStep).toBe(false); + expect(result.current.canStepBack).toBe(false); + }); + }); + + describe("manual navigation", () => { + it("steps forward and clamps at the last step", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 3 })); + act(() => result.current.step()); + expect(result.current.currentStepIndex).toBe(0); + act(() => result.current.step()); + act(() => result.current.step()); + expect(result.current.currentStepIndex).toBe(2); + expect(result.current.canStep).toBe(false); + act(() => result.current.step()); + expect(result.current.currentStepIndex).toBe(2); + }); + + it("steps back and never goes below the first step", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 3 })); + act(() => result.current.jumpTo(2)); + act(() => result.current.stepBack()); + expect(result.current.currentStepIndex).toBe(1); + act(() => result.current.stepBack()); + act(() => result.current.stepBack()); + expect(result.current.currentStepIndex).toBe(0); + expect(result.current.canStepBack).toBe(false); + }); + + it("resets to the initial step", () => { + const { result } = renderHook(() => + useStepPlayback({ totalSteps: 5, initialStep: 1 }), + ); + act(() => result.current.jumpTo(4)); + act(() => result.current.reset()); + expect(result.current.currentStepIndex).toBe(1); + }); + + it("jumps to a step and clamps out of range targets", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 5 })); + act(() => result.current.jumpTo(3)); + expect(result.current.currentStepIndex).toBe(3); + act(() => result.current.jumpTo(99)); + expect(result.current.currentStepIndex).toBe(4); + act(() => result.current.jumpTo(-10)); + expect(result.current.currentStepIndex).toBe(-1); + }); + + it("stops playback when the reader navigates by hand", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 5 })); + act(() => result.current.togglePlay()); + expect(result.current.isPlaying).toBe(true); + act(() => result.current.step()); + expect(result.current.isPlaying).toBe(false); + + act(() => result.current.togglePlay()); + act(() => result.current.jumpTo(2)); + expect(result.current.isPlaying).toBe(false); + }); + }); + + describe("auto play", () => { + it("starts at step 0 and advances on the speed delay", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 4 })); + act(() => result.current.togglePlay()); + expect(result.current.currentStepIndex).toBe(0); + expect(result.current.isPlaying).toBe(true); + + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[4] - 1)); + expect(result.current.currentStepIndex).toBe(0); + act(() => vi.advanceTimersByTime(1)); + expect(result.current.currentStepIndex).toBe(1); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[4])); + expect(result.current.currentStepIndex).toBe(2); + }); + + it("stops by itself on the last step", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 3 })); + act(() => result.current.togglePlay()); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[4])); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[4])); + expect(result.current.currentStepIndex).toBe(2); + expect(result.current.isPlaying).toBe(false); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[4] * 5)); + expect(result.current.currentStepIndex).toBe(2); + }); + + it("pauses and resumes from the current step", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 5 })); + act(() => result.current.togglePlay()); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[4])); + expect(result.current.currentStepIndex).toBe(1); + + act(() => result.current.togglePlay()); + expect(result.current.isPlaying).toBe(false); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[4] * 3)); + expect(result.current.currentStepIndex).toBe(1); + + act(() => result.current.togglePlay()); + expect(result.current.currentStepIndex).toBe(1); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[4])); + expect(result.current.currentStepIndex).toBe(2); + }); + + it("restarts from the top when played again after finishing", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 3 })); + act(() => result.current.jumpTo(2)); + act(() => result.current.togglePlay()); + expect(result.current.currentStepIndex).toBe(0); + expect(result.current.isPlaying).toBe(true); + }); + + it("uses the selected speed for the delay", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 5 })); + act(() => result.current.setSpeedLevel(6)); + expect(result.current.speedLabel).toBe("2x"); + act(() => result.current.togglePlay()); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[6])); + expect(result.current.currentStepIndex).toBe(1); + + act(() => result.current.setSpeedLevel(1)); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[6])); + expect(result.current.currentStepIndex).toBe(1); + act(() => + vi.advanceTimersByTime(SPEED_TO_DELAY_MS[1] - SPEED_TO_DELAY_MS[6]), + ); + expect(result.current.currentStepIndex).toBe(2); + }); + }); + + describe("reset key", () => { + it("restarts playback when the key changes", () => { + const { result, rerender } = renderHook( + (props: { totalSteps: number; resetKey: string }) => + useStepPlayback(props), + { initialProps: { totalSteps: 5, resetKey: "a" } }, + ); + act(() => result.current.togglePlay()); + act(() => vi.advanceTimersByTime(SPEED_TO_DELAY_MS[4])); + expect(result.current.currentStepIndex).toBe(1); + + rerender({ totalSteps: 3, resetKey: "b" }); + expect(result.current.currentStepIndex).toBe(-1); + expect(result.current.isPlaying).toBe(false); + }); + + it("keeps the position when the key is unchanged", () => { + const { result, rerender } = renderHook( + (props: { totalSteps: number; resetKey: string }) => + useStepPlayback(props), + { initialProps: { totalSteps: 5, resetKey: "a" } }, + ); + act(() => result.current.jumpTo(2)); + rerender({ totalSteps: 5, resetKey: "a" }); + expect(result.current.currentStepIndex).toBe(2); + }); + }); + + describe("keyboard shortcuts", () => { + it("binds Space, the arrows, and R", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 5 })); + + keyDown("ArrowRight"); + keyDown("ArrowRight"); + expect(result.current.currentStepIndex).toBe(1); + + keyDown("ArrowLeft"); + expect(result.current.currentStepIndex).toBe(0); + + const space = keyDown(" "); + expect(result.current.isPlaying).toBe(true); + expect(space.defaultPrevented).toBe(true); + + keyDown("R"); + expect(result.current.currentStepIndex).toBe(-1); + expect(result.current.isPlaying).toBe(false); + + keyDown("ArrowRight"); + keyDown("r"); + expect(result.current.currentStepIndex).toBe(-1); + }); + + it("ignores shortcuts with modifier keys or that were already handled", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 5 })); + keyDown("ArrowRight", { metaKey: true }); + keyDown("ArrowRight", { ctrlKey: true }); + keyDown("ArrowRight", { altKey: true }); + expect(result.current.currentStepIndex).toBe(-1); + + const handled = new KeyboardEvent("keydown", { + key: "ArrowRight", + cancelable: true, + }); + handled.preventDefault(); + act(() => { + window.dispatchEvent(handled); + }); + expect(result.current.currentStepIndex).toBe(-1); + }); + + it("ignores keys typed into form fields, editors, and dialogs", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 5 })); + const input = document.createElement("input"); + const editor = document.createElement("div"); + editor.className = "cm-editor"; + const editorChild = document.createElement("div"); + editor.append(editorChild); + const dialog = document.createElement("div"); + dialog.setAttribute("role", "dialog"); + const dialogButton = document.createElement("button"); + dialog.append(dialogButton); + document.body.append(input, editor, dialog); + + for (const target of [input, editorChild, dialogButton]) { + act(() => { + fireEvent.keyDown(target, { key: "ArrowRight" }); + }); + } + expect(result.current.currentStepIndex).toBe(-1); + + act(() => { + fireEvent.keyDown(document.body, { key: "ArrowRight" }); + }); + expect(result.current.currentStepIndex).toBe(0); + input.remove(); + editor.remove(); + dialog.remove(); + }); + + it("drops focus from a link when a shortcut fires, but not from a button", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 5 })); + const link = document.createElement("a"); + link.href = "/javascript/hoisting"; + const button = document.createElement("button"); + document.body.append(link, button); + try { + link.focus(); + expect(document.activeElement).toBe(link); + act(() => { + fireEvent.keyDown(link, { key: "ArrowRight" }); + }); + expect(result.current.currentStepIndex).toBe(0); + expect(document.activeElement).not.toBe(link); + + button.focus(); + act(() => { + fireEvent.keyDown(button, { key: "ArrowRight" }); + }); + expect(result.current.currentStepIndex).toBe(1); + expect(document.activeElement).toBe(button); + } finally { + link.remove(); + button.remove(); + } + }); + + it("can be switched off", () => { + const { result } = renderHook(() => + useStepPlayback({ totalSteps: 5, keyboardShortcuts: false }), + ); + keyDown("ArrowRight"); + expect(result.current.currentStepIndex).toBe(-1); + }); + + it("removes its listener on unmount", () => { + const added = vi.spyOn(window, "addEventListener"); + const removed = vi.spyOn(window, "removeEventListener"); + const { unmount } = renderHook(() => useStepPlayback({ totalSteps: 5 })); + const keydownAdds = added.mock.calls.filter( + ([type]) => type === "keydown", + ); + expect(keydownAdds).toHaveLength(1); + const handler = keydownAdds[0][1]; + unmount(); + expect(removed).toHaveBeenCalledWith("keydown", handler); + }); + }); + + describe("topic progress", () => { + it("marks the topic completed when the last step is reached", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 3 }), { + wrapper: withTopic("closures"), + }); + expect(markTopicCompleted).not.toHaveBeenCalled(); + act(() => result.current.jumpTo(1)); + expect(markTopicCompleted).not.toHaveBeenCalled(); + act(() => result.current.jumpTo(2)); + expect(markTopicCompleted).toHaveBeenCalledWith("closures"); + }); + + it("does nothing without a topic context or without steps", () => { + const { result } = renderHook(() => useStepPlayback({ totalSteps: 2 })); + act(() => result.current.jumpTo(1)); + expect(markTopicCompleted).not.toHaveBeenCalled(); + + renderHook(() => useStepPlayback({ totalSteps: 0 }), { + wrapper: withTopic("closures"), + }); + expect(markTopicCompleted).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/hooks/useStepPlayback.ts b/src/hooks/useStepPlayback.ts index 4f81237..cf41b7f 100644 --- a/src/hooks/useStepPlayback.ts +++ b/src/hooks/useStepPlayback.ts @@ -1,35 +1,50 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { + SPEED_LABELS, + SPEED_TO_DELAY_MS, +} from "@/components/visualization-ui/TransportControls/constants"; import type { PlaybackSpeedLevel } from "@/components/visualization-ui/TransportControls"; +import { useCurrentTopicId } from "@/components/progress/TopicProgressContext"; +import { markTopicCompleted } from "@/lib/progress/topicProgress"; - -export const SPEED_TO_DELAY_MS: Record = { - 1: 5000, - 2: 2500, - 3: 1800, - 4: 1200, - 5: 700, - 6: 400, -}; - -export const SPEED_LABELS: Record = { - 1: "0.25x", - 2: "0.5x", - 3: "0.75x", - 4: "1x", - 5: "1.5x", - 6: "2x", -}; - +const DEFAULT_SPEED_LEVEL: PlaybackSpeedLevel = 4; interface UseStepPlaybackOptions { totalSteps: number; initialStep?: number; resetKey?: string | number; + /** + * Bind Space, ArrowLeft, ArrowRight, and R to playback while no text field + * is focused. On by default so every topic gets the same shortcuts. + */ + keyboardShortcuts?: boolean; } -interface UseStepPlaybackReturn { +const isTypingTarget = (target: EventTarget | null) => { + if (!(target instanceof HTMLElement)) return false; + if (target.isContentEditable) return true; + const tag = target.tagName; + return ( + tag === "INPUT" || + tag === "TEXTAREA" || + tag === "SELECT" || + Boolean(target.closest(".cm-editor")) || + Boolean(target.closest("[role='dialog'], [role='listbox']")) + ); +}; + +/** + * A link clicked with the mouse keeps focus after navigation, and the first + * keypress would then paint the focus ring on it. Playback shortcuts are not + * link actions, so drop that incidental focus; buttons and inputs keep theirs. + */ +const releaseLinkFocus = (target: EventTarget | null) => { + if (target instanceof HTMLAnchorElement) target.blur(); +}; + +export interface StepPlayback { currentStepIndex: number; isPlaying: boolean; speedLevel: PlaybackSpeedLevel; @@ -44,63 +59,66 @@ interface UseStepPlaybackReturn { jumpTo: (index: number) => void; } -export function useStepPlayback({ +export const useStepPlayback = ({ totalSteps, initialStep = -1, resetKey, -}: UseStepPlaybackOptions): UseStepPlaybackReturn { + keyboardShortcuts = true, +}: UseStepPlaybackOptions): StepPlayback => { const [currentStepIndex, setCurrentStepIndex] = useState(initialStep); - const [isPlaying, setIsPlaying] = useState(false); - const [speedLevel, setSpeedLevel] = useState(4); + const [isPlayingState, setIsPlaying] = useState(false); + const [speedLevel, setSpeedLevel] = + useState(DEFAULT_SPEED_LEVEL); + const topicId = useCurrentTopicId(); const lastStepIndex = totalSteps - 1; - const firstStep = initialStep < 0 ? 0 : initialStep; + const firstStep = Math.max(initialStep, 0); const canStep = currentStepIndex < lastStepIndex; const canStepBack = currentStepIndex > firstStep; - - useEffect(() => { + // Playback ends by itself once the last step is on screen. + const isPlaying = isPlayingState && canStep; + + // A new example (or regenerated sandbox) restarts playback from the top. + // The reset happens during render so no frame sees the previous example's + // index applied to the new example's steps. + const [appliedResetKey, setAppliedResetKey] = useState(resetKey); + if (resetKey !== appliedResetKey) { + setAppliedResetKey(resetKey); setIsPlaying(false); setCurrentStepIndex(initialStep); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [resetKey]); + } + // Auto-advance. The effect re-runs on every step, so the closure always + // holds the current index and no state updater needs to know about it. useEffect(() => { if (!isPlaying) return; const timeoutId = window.setTimeout(() => { - setCurrentStepIndex((prev) => { - if (prev >= lastStepIndex) { - setIsPlaying(false); - return prev; - } - const next = prev + 1; - if (next >= lastStepIndex) { - setIsPlaying(false); - } - return next; - }); + const next = currentStepIndex + 1; + setCurrentStepIndex(next); + if (next >= lastStepIndex) setIsPlaying(false); }, SPEED_TO_DELAY_MS[speedLevel]); return () => window.clearTimeout(timeoutId); }, [currentStepIndex, isPlaying, speedLevel, lastStepIndex]); const togglePlay = useCallback(() => { - setIsPlaying((prev) => { - if (prev) return false; - setCurrentStepIndex((prevStep) => { - if (prevStep < 0 || prevStep >= lastStepIndex) return 0; - return prevStep; - }); - return true; - }); - }, [lastStepIndex]); + if (isPlaying) { + setIsPlaying(false); + return; + } + // Pressing play before the first step, or after the last, starts over. + if (currentStepIndex < 0 || currentStepIndex >= lastStepIndex) { + setCurrentStepIndex(0); + } + setIsPlaying(true); + }, [isPlaying, currentStepIndex, lastStepIndex]); const step = useCallback(() => { setIsPlaying(false); - setCurrentStepIndex((prev) => { - if (prev < 0) return 0; - return Math.min(prev + 1, lastStepIndex); - }); + setCurrentStepIndex((prev) => + Math.min(Math.max(prev + 1, 0), lastStepIndex), + ); }, [lastStepIndex]); const stepBack = useCallback(() => { @@ -116,11 +134,58 @@ export function useStepPlayback({ const jumpTo = useCallback( (index: number) => { setIsPlaying(false); - setCurrentStepIndex(Math.max(initialStep, Math.min(index, lastStepIndex))); + setCurrentStepIndex( + Math.max(initialStep, Math.min(index, lastStepIndex)), + ); }, [initialStep, lastStepIndex], ); + useEffect(() => { + if (!topicId || totalSteps === 0) return; + if (currentStepIndex >= lastStepIndex) { + markTopicCompleted(topicId); + } + }, [topicId, totalSteps, currentStepIndex, lastStepIndex]); + + useEffect(() => { + if (!keyboardShortcuts) return; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.defaultPrevented) return; + if (event.metaKey || event.ctrlKey || event.altKey) return; + if (isTypingTarget(event.target)) return; + + switch (event.key) { + case " ": + event.preventDefault(); + releaseLinkFocus(event.target); + togglePlay(); + break; + case "ArrowRight": + event.preventDefault(); + releaseLinkFocus(event.target); + step(); + break; + case "ArrowLeft": + event.preventDefault(); + releaseLinkFocus(event.target); + stepBack(); + break; + case "r": + case "R": + releaseLinkFocus(event.target); + reset(); + break; + default: + break; + } + }; + + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [keyboardShortcuts, togglePlay, step, stepBack, reset]); + return { currentStepIndex, isPlaying, @@ -135,4 +200,4 @@ export function useStepPlayback({ setSpeedLevel, jumpTo, }; -} +}; diff --git a/src/hooks/useTopicProgress.test.tsx b/src/hooks/useTopicProgress.test.tsx new file mode 100644 index 0000000..988dc70 --- /dev/null +++ b/src/hooks/useTopicProgress.test.tsx @@ -0,0 +1,38 @@ +import { act, renderHook } from "@testing-library/react"; +import { beforeEach, describe, expect, it } from "vitest"; +import { + useIsTopicCompleted, + useTopicProgress, +} from "@/hooks/useTopicProgress"; +import { + clearTopicProgress, + markTopicCompleted, +} from "@/lib/progress/topicProgress"; + +describe("useTopicProgress", () => { + beforeEach(() => { + clearTopicProgress(); + }); + + it("starts empty and updates when a topic completes", () => { + const { result } = renderHook(() => useTopicProgress()); + expect(result.current).toEqual({}); + act(() => markTopicCompleted("closures")); + expect(Object.keys(result.current)).toEqual(["closures"]); + }); + + it("updates every mounted consumer at once", () => { + const first = renderHook(() => useIsTopicCompleted("hooks")); + const second = renderHook(() => useIsTopicCompleted("hooks")); + const other = renderHook(() => useIsTopicCompleted("promises")); + expect(first.result.current).toBe(false); + + act(() => markTopicCompleted("hooks")); + expect(first.result.current).toBe(true); + expect(second.result.current).toBe(true); + expect(other.result.current).toBe(false); + + act(() => clearTopicProgress()); + expect(first.result.current).toBe(false); + }); +}); diff --git a/src/hooks/useTopicProgress.ts b/src/hooks/useTopicProgress.ts new file mode 100644 index 0000000..0f8cd12 --- /dev/null +++ b/src/hooks/useTopicProgress.ts @@ -0,0 +1,20 @@ +"use client"; + +import { useSyncExternalStore } from "react"; +import { + getServerTopicProgress, + readTopicProgress, + subscribeTopicProgress, + type TopicProgressMap, +} from "@/lib/progress/topicProgress"; + +/** Live view of which topics this browser has completed. */ +export const useTopicProgress = (): TopicProgressMap => + useSyncExternalStore( + subscribeTopicProgress, + readTopicProgress, + getServerTopicProgress, + ); + +export const useIsTopicCompleted = (topicId: string): boolean => + Boolean(useTopicProgress()[topicId]); diff --git a/src/lib/categories.test.ts b/src/lib/categories.test.ts new file mode 100644 index 0000000..0dafb27 --- /dev/null +++ b/src/lib/categories.test.ts @@ -0,0 +1,102 @@ +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + CATEGORIES, + CATEGORY_LIST, + CATEGORY_ORDER, + categoryFromPathname, + getCategory, + isCategory, +} from "@/lib/categories"; + +const RGB_TRIPLET = /^\d{1,3} \d{1,3} \d{1,3}$/; + +describe("category registry", () => { + it("orders every registered category exactly once", () => { + expect([...CATEGORY_ORDER].sort()).toEqual(Object.keys(CATEGORIES).sort()); + expect(new Set(CATEGORY_ORDER).size).toBe(CATEGORY_ORDER.length); + }); + + it("lists categories in display order", () => { + expect(CATEGORY_LIST.map((category) => category.id)).toEqual( + CATEGORY_ORDER, + ); + }); + + it("keys each config by its own id and routes to /", () => { + for (const [key, config] of Object.entries(CATEGORIES)) { + expect(config.id).toBe(key); + expect(config.route).toBe(`/${key}`); + } + }); + + it("ships an icon for every category", () => { + for (const config of CATEGORY_LIST) { + expect(config.iconSrc).toBe(`/icons/${config.id}.svg`); + expect(existsSync(resolve("public", config.iconSrc.slice(1)))).toBe(true); + expect(config.iconAlt.trim().length).toBeGreaterThan(0); + } + }); + + it("uses RGB triplets for glow and hero colours", () => { + for (const config of CATEGORY_LIST) { + expect(config.glowRgb).toMatch(RGB_TRIPLET); + expect(config.hero.accent).toMatch(RGB_TRIPLET); + expect(config.hero.accentSoft).toMatch(RGB_TRIPLET); + expect(config.hero.secondary).toMatch(RGB_TRIPLET); + } + }); + + it("fills in all copy and keyword fields", () => { + for (const config of CATEGORY_LIST) { + for (const field of [ + config.label, + config.headingLabel, + config.tagline, + config.description, + config.kicker, + config.docsLabel, + config.indexTitle, + ]) { + expect(field.trim().length).toBeGreaterThan(0); + expect(field).not.toContain("—"); + } + expect(config.keywords.length).toBeGreaterThan(0); + expect(config.indexKeywords.length).toBeGreaterThan(0); + expect(new Set(config.indexKeywords).size).toBe( + config.indexKeywords.length, + ); + } + }); +}); + +describe("getCategory", () => { + it("returns the config for an id", () => { + expect(getCategory("react")).toBe(CATEGORIES.react); + }); +}); + +describe("isCategory", () => { + it("accepts registered ids only", () => { + expect(isCategory("javascript")).toBe(true); + expect(isCategory("ai")).toBe(true); + expect(isCategory("JavaScript")).toBe(false); + expect(isCategory("")).toBe(false); + expect(isCategory("toString")).toBe(false); + }); +}); + +describe("categoryFromPathname", () => { + it("reads the first path segment", () => { + expect(categoryFromPathname("/react")).toBe("react"); + expect(categoryFromPathname("/backend/jwt-authentication")).toBe("backend"); + }); + + it("falls back to javascript for unknown or empty paths", () => { + expect(categoryFromPathname("/")).toBe("javascript"); + expect(categoryFromPathname("")).toBe("javascript"); + expect(categoryFromPathname("/about")).toBe("javascript"); + expect(categoryFromPathname("react")).toBe("javascript"); + }); +}); diff --git a/src/lib/categories.ts b/src/lib/categories.ts new file mode 100644 index 0000000..bb99852 --- /dev/null +++ b/src/lib/categories.ts @@ -0,0 +1,328 @@ +import type { Category } from "@/types"; + +/** + * Category registry. Every place that needs a label, route, colour, or hero + * copy for a category reads it from here. Adding a category means adding one + * entry below, an icon in public/icons, and a route folder under src/app. + */ + +export interface CategoryHeroPalette { + /** RGB triplet, e.g. "247 223 30", used by the category hero CSS variables. */ + accent: string; + accentSoft: string; + secondary: string; +} + +export interface CategoryConfig { + id: Category; + /** Display label, e.g. "JavaScript". */ + label: string; + /** Used in headings like "React concepts". */ + headingLabel: string; + route: string; + /** Short line under the category heading on the landing page. */ + tagline: string; + /** Hero and meta description for the category index page. */ + description: string; + /** Small uppercase label above the hero title. */ + kicker: string; + iconSrc: string; + iconAlt: string; + /** Label for the external documentation link on the topic page. */ + docsLabel: string; + /** Tailwind classes for the gradient-clipped word in titles. */ + titleAccentClass: string; + /** Tailwind classes for the kicker pill. */ + kickerClass: string; + /** Tailwind classes for the icon container. */ + iconShellClass: string; + /** Tailwind classes for the active state of the sidebar category toggle. */ + toggleActiveClass: string; + /** Tailwind classes for a small category badge. */ + badgeClass: string; + /** Tailwind classes for the landing page call to action button. */ + ctaClass: string; + /** RGB triplet used for hover glows on topic cards. */ + glowRgb: string; + hero: CategoryHeroPalette; + /** Search phrases merged into every topic's keywords in this category. */ + keywords: string[]; + /** Keywords for the category index page itself. */ + indexKeywords: string[]; + /** Meta title of the category index page. */ + indexTitle: string; +} + +export const CATEGORY_ORDER: Category[] = [ + "javascript", + "react", + "frameworks", + "backend", + "ai", +]; + +export const CATEGORIES: Record = { + javascript: { + id: "javascript", + label: "JavaScript", + headingLabel: "JavaScript concepts", + route: "/javascript", + tagline: + "Runtime internals you get asked about in interviews and hit in real bugs.", + description: + "Core JavaScript internals and runtime behavior, visualized step by step.", + kicker: "Engine Room View", + iconSrc: "/icons/javascript.svg", + iconAlt: "JavaScript logo", + docsLabel: "MDN", + titleAccentClass: + "bg-gradient-to-r from-amber-200 via-yellow-300 to-yellow-500 bg-clip-text text-transparent", + kickerClass: + "border border-yellow-200/25 bg-yellow-200/10 text-yellow-100/90", + iconShellClass: + "bg-[#f7df1e] p-1.5 shadow-[0_0_28px_rgba(247,223,30,0.46)]", + toggleActiveClass: + "border border-yellow-300/30 bg-yellow-400/15 shadow-[0_0_16px_rgba(251,191,36,0.16)]", + badgeClass: "border-yellow-300/30 bg-yellow-400/10 text-yellow-200", + ctaClass: + "border-yellow-200/30 bg-yellow-200/10 text-yellow-100 hover:border-yellow-200/55 hover:bg-yellow-200/16", + glowRgb: "251 191 36", + hero: { + accent: "247 223 30", + accentSoft: "253 230 138", + secondary: "56 189 248", + }, + keywords: [ + "javascript fundamentals", + "learn javascript visually", + "javascript internals", + ], + indexKeywords: [ + "javascript visualizer", + "javascript visualization", + "javascript concepts", + "javascript fundamentals", + "event loop", + "hoisting", + "closures", + "promises", + "prototype chain", + "this keyword", + "scope chain", + "async await", + "garbage collection javascript", + "type coercion", + "execution context", + ], + indexTitle: "JavaScript Concepts, Visualized", + }, + react: { + id: "react", + label: "React", + headingLabel: "React concepts", + route: "/react", + tagline: + "How React decides what to render, when to re-render, and what it commits to the DOM.", + description: + "React rendering, reconciliation, hooks, and scheduling with interactive diagrams.", + kicker: "Rendering Playground", + iconSrc: "/icons/react.svg", + iconAlt: "React logo", + docsLabel: "react.dev", + titleAccentClass: + "bg-gradient-to-r from-cyan-200 via-sky-200 to-cyan-400 bg-clip-text text-transparent", + kickerClass: "border border-cyan-200/25 bg-cyan-200/10 text-cyan-100/90", + iconShellClass: + "border border-cyan-300/30 bg-[rgba(34,211,238,0.1)] p-1.5 shadow-[0_0_22px_rgba(34,211,238,0.24)]", + toggleActiveClass: + "border border-cyan-300/30 bg-cyan-400/15 shadow-[0_0_16px_rgba(34,211,238,0.16)]", + badgeClass: "border-cyan-300/30 bg-cyan-400/10 text-cyan-200", + ctaClass: + "border-cyan-300/30 bg-cyan-400/10 text-cyan-200 hover:border-cyan-300/55 hover:bg-cyan-300/16", + glowRgb: "34 211 238", + hero: { + accent: "34 211 238", + accentSoft: "103 232 249", + secondary: "192 132 252", + }, + keywords: [ + "react fundamentals", + "react concepts", + "react internals", + "react rendering", + "learn react visually", + ], + indexKeywords: [ + "react visualizer", + "react visualization", + "react concepts", + "react fundamentals", + "react reconciliation", + "virtual DOM", + "react re-rendering", + "react internals", + "react fiber", + "react hooks", + "react diffing algorithm", + "react state batching", + "concurrent rendering", + ], + indexTitle: "React Concepts, Visualized", + }, + frameworks: { + id: "frameworks", + label: "Frameworks", + headingLabel: "Framework internals", + route: "/frameworks", + tagline: + "How Vue, Svelte, and Angular track changes and update the DOM, compared side by side.", + description: + "Reactivity and change detection in Vue, Svelte, and Angular, visualized so the differences are obvious.", + kicker: "Reactivity Lab", + iconSrc: "/icons/frameworks.svg", + iconAlt: "Frameworks icon", + docsLabel: "official docs", + titleAccentClass: + "bg-gradient-to-r from-emerald-200 via-green-300 to-emerald-400 bg-clip-text text-transparent", + kickerClass: + "border border-emerald-200/25 bg-emerald-200/10 text-emerald-100/90", + iconShellClass: + "border border-emerald-300/30 bg-[rgba(52,211,153,0.1)] p-1.5 shadow-[0_0_22px_rgba(52,211,153,0.24)]", + toggleActiveClass: + "border border-emerald-300/30 bg-emerald-400/15 shadow-[0_0_16px_rgba(52,211,153,0.16)]", + badgeClass: "border-emerald-300/30 bg-emerald-400/10 text-emerald-200", + ctaClass: + "border-emerald-300/30 bg-emerald-400/10 text-emerald-200 hover:border-emerald-300/55 hover:bg-emerald-300/16", + glowRgb: "52 211 153", + hero: { + accent: "52 211 153", + accentSoft: "110 231 183", + secondary: "56 189 248", + }, + keywords: [ + "frontend framework internals", + "reactivity explained", + "learn frameworks visually", + ], + indexKeywords: [ + "vue reactivity explained", + "svelte runes explained", + "angular change detection", + "signals explained", + "frontend framework internals", + "vue vs svelte vs angular", + "fine grained reactivity", + ], + indexTitle: "Framework Internals, Visualized", + }, + backend: { + id: "backend", + label: "Backend", + headingLabel: "Backend principles", + route: "/backend", + tagline: + "What happens between a request leaving the browser and a response coming back.", + description: + "HTTP, databases, caching, auth, and rate limiting, traced one step at a time.", + kicker: "Server Side View", + iconSrc: "/icons/backend.svg", + iconAlt: "Backend icon", + docsLabel: "reference", + titleAccentClass: + "bg-gradient-to-r from-violet-200 via-purple-300 to-violet-400 bg-clip-text text-transparent", + kickerClass: + "border border-violet-200/25 bg-violet-200/10 text-violet-100/90", + iconShellClass: + "border border-violet-300/30 bg-[rgba(167,139,250,0.12)] p-1.5 shadow-[0_0_22px_rgba(167,139,250,0.26)]", + toggleActiveClass: + "border border-violet-300/30 bg-violet-400/15 shadow-[0_0_16px_rgba(167,139,250,0.18)]", + badgeClass: "border-violet-300/30 bg-violet-400/10 text-violet-200", + ctaClass: + "border-violet-300/30 bg-violet-400/10 text-violet-200 hover:border-violet-300/55 hover:bg-violet-300/16", + glowRgb: "167 139 250", + hero: { + accent: "167 139 250", + accentSoft: "196 181 253", + secondary: "34 211 238", + }, + keywords: [ + "backend fundamentals", + "system design basics", + "learn backend visually", + ], + indexKeywords: [ + "backend visualizer", + "http request lifecycle", + "database indexing explained", + "caching strategies", + "jwt authentication flow", + "rate limiting explained", + "backend interview questions", + "system design fundamentals", + ], + indexTitle: "Backend Principles, Visualized", + }, + ai: { + id: "ai", + label: "AI", + headingLabel: "AI under the hood", + route: "/ai", + tagline: + "What a language model actually does with your text, from tokens to the next word.", + description: + "Tokenization, embeddings, attention, sampling, and training, shown as the numbers move.", + kicker: "Model Internals", + iconSrc: "/icons/ai.svg", + iconAlt: "AI icon", + docsLabel: "paper", + titleAccentClass: + "bg-gradient-to-r from-pink-200 via-rose-300 to-pink-400 bg-clip-text text-transparent", + kickerClass: "border border-pink-200/25 bg-pink-200/10 text-pink-100/90", + iconShellClass: + "border border-pink-300/30 bg-[rgba(244,114,182,0.12)] p-1.5 shadow-[0_0_22px_rgba(244,114,182,0.26)]", + toggleActiveClass: + "border border-pink-300/30 bg-pink-400/15 shadow-[0_0_16px_rgba(244,114,182,0.18)]", + badgeClass: "border-pink-300/30 bg-pink-400/10 text-pink-200", + ctaClass: + "border-pink-300/30 bg-pink-400/10 text-pink-200 hover:border-pink-300/55 hover:bg-pink-300/16", + glowRgb: "244 114 182", + hero: { + accent: "244 114 182", + accentSoft: "249 168 212", + secondary: "167 139 250", + }, + keywords: [ + "how llms work", + "ai internals explained", + "learn machine learning visually", + ], + indexKeywords: [ + "how do llms work", + "transformer explained", + "attention mechanism visualized", + "tokenization explained", + "embeddings explained", + "backpropagation visualized", + "how chatgpt works under the hood", + "ai visualizer", + ], + indexTitle: "AI Under the Hood, Visualized", + }, +}; + +export const CATEGORY_LIST: CategoryConfig[] = CATEGORY_ORDER.map( + (id) => CATEGORIES[id], +); + +export function getCategory(id: Category): CategoryConfig { + return CATEGORIES[id]; +} + +export function isCategory(value: string): value is Category { + return Object.hasOwn(CATEGORIES, value); +} + +export function categoryFromPathname(pathname: string): Category { + const segment = pathname.split("/")[1] ?? ""; + return isCategory(segment) ? segment : "javascript"; +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 038b8b0..268d318 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,14 +1,12 @@ export const SITE_NAME = "VisualizeJS"; export const SITE_DESCRIPTION = - "Free interactive platform to visualize JavaScript concepts step by step. Explore the event loop, closures, hoisting, promises, scope chain, prototypes, garbage collection, and more with animated demonstrations."; + "Free interactive visualizations of how JavaScript, React, Vue, Svelte, Angular, backend systems, and AI models work under the hood. Step through the event loop, closures, reconciliation, HTTP requests, attention, and more."; export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://visualizejs.com"; export const SOCIAL_IMAGE_ALT = "VisualizeJS - Interactive JavaScript and React visualizations"; export const SOCIAL_IMAGE_WIDTH = 1200; export const SOCIAL_IMAGE_HEIGHT = 630; -export const OPEN_GRAPH_IMAGE_URL = `${SITE_URL}/opengraph-image`; -export const TWITTER_IMAGE_URL = `${SITE_URL}/twitter-image`; export const CREATOR_NAME = "Tornike Nizharadze"; export const CREATOR_LINKEDIN_URL = process.env.NEXT_PUBLIC_CREATOR_LINKEDIN_URL || diff --git a/src/lib/difficulty.test.ts b/src/lib/difficulty.test.ts new file mode 100644 index 0000000..f7864d4 --- /dev/null +++ b/src/lib/difficulty.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from "vitest"; +import { + buildDifficultySearch, + countByDifficulty, + DIFFICULTY_ORDER, + filterByDifficulty, + isDifficulty, + parseDifficultyParam, +} from "./difficulty"; +import type { Topic } from "@/types"; + +const topic = (id: string, difficulty: Topic["difficulty"]): Topic => ({ + id, + title: id, + description: "", + category: "javascript", + difficulty, + docsUrl: "https://example.com", + route: `/javascript/${id}`, +}); + +const LIST = [ + topic("a", "beginner"), + topic("b", "intermediate"), + topic("c", "beginner"), + topic("d", "advanced"), +]; + +describe("isDifficulty", () => { + it("accepts every level in order and rejects anything else", () => { + for (const level of DIFFICULTY_ORDER) + expect(isDifficulty(level)).toBe(true); + expect(isDifficulty("expert")).toBe(false); + expect(isDifficulty("")).toBe(false); + expect(isDifficulty(null)).toBe(false); + expect(isDifficulty("toString")).toBe(false); + }); +}); + +describe("parseDifficultyParam", () => { + it("reads a valid level from a query string", () => { + expect(parseDifficultyParam("?level=advanced")).toBe("advanced"); + expect(parseDifficultyParam("?foo=1&level=beginner")).toBe("beginner"); + }); + + it("returns null for a missing, empty, or unknown level", () => { + expect(parseDifficultyParam("")).toBeNull(); + expect(parseDifficultyParam("?level=")).toBeNull(); + expect(parseDifficultyParam("?level=hard")).toBeNull(); + expect(parseDifficultyParam("?other=beginner")).toBeNull(); + }); +}); + +describe("buildDifficultySearch", () => { + it("round-trips with the parser", () => { + for (const level of DIFFICULTY_ORDER) { + expect(parseDifficultyParam(buildDifficultySearch(level))).toBe(level); + } + expect(buildDifficultySearch(null)).toBe(""); + }); +}); + +describe("countByDifficulty", () => { + it("counts every level, including levels with no topics", () => { + expect(countByDifficulty(LIST)).toEqual({ + beginner: 2, + intermediate: 1, + advanced: 1, + }); + expect(countByDifficulty([])).toEqual({ + beginner: 0, + intermediate: 0, + advanced: 0, + }); + }); +}); + +describe("filterByDifficulty", () => { + it("keeps only the requested level in the original order", () => { + expect(filterByDifficulty(LIST, "beginner").map((t) => t.id)).toEqual([ + "a", + "c", + ]); + expect(filterByDifficulty(LIST, "advanced").map((t) => t.id)).toEqual([ + "d", + ]); + }); + + it("returns the same list when no level is selected", () => { + expect(filterByDifficulty(LIST, null)).toBe(LIST); + }); +}); diff --git a/src/lib/difficulty.ts b/src/lib/difficulty.ts new file mode 100644 index 0000000..9296c73 --- /dev/null +++ b/src/lib/difficulty.ts @@ -0,0 +1,49 @@ +import type { Difficulty, Topic } from "@/types"; + +export const DIFFICULTY_ORDER: readonly Difficulty[] = [ + "beginner", + "intermediate", + "advanced", +]; + +export const DIFFICULTY_LABELS: Record = { + beginner: "Beginner", + intermediate: "Intermediate", + advanced: "Advanced", +}; + +/** Query parameter that carries the selected level on category pages. */ +export const DIFFICULTY_PARAM = "level"; + +export const isDifficulty = (value: unknown): value is Difficulty => + typeof value === "string" && + (DIFFICULTY_ORDER as readonly string[]).includes(value); + +/** Parses a level from a query string; anything unknown means no filter. */ +export const parseDifficultyParam = (search: string): Difficulty | null => { + const value = new URLSearchParams(search).get(DIFFICULTY_PARAM); + return isDifficulty(value) ? value : null; +}; + +/** The query string for a level, or an empty string for no filter. */ +export const buildDifficultySearch = (level: Difficulty | null): string => + level ? `?${DIFFICULTY_PARAM}=${level}` : ""; + +export const countByDifficulty = ( + list: readonly Topic[], +): Record => { + const counts: Record = { + beginner: 0, + intermediate: 0, + advanced: 0, + }; + for (const topic of list) counts[topic.difficulty] += 1; + return counts; +}; + +/** Narrows a list to one level; null returns the list untouched. */ +export const filterByDifficulty = ( + list: readonly Topic[], + level: Difficulty | null, +): readonly Topic[] => + level ? list.filter((topic) => topic.difficulty === level) : list; diff --git a/src/lib/explanationMode.test.ts b/src/lib/explanationMode.test.ts new file mode 100644 index 0000000..31b0f12 --- /dev/null +++ b/src/lib/explanationMode.test.ts @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { STORAGE_KEYS } from "@/content/static/storageKeys"; + +const loadStore = async () => { + vi.resetModules(); + return import("@/lib/explanationMode"); +}; + +describe("explanationMode", () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + it("defaults to detailed", async () => { + const store = await loadStore(); + expect(store.readExplanationMode()).toBe("detailed"); + expect(store.getServerExplanationMode()).toBe("detailed"); + }); + + it("reads a stored simple preference", async () => { + window.localStorage.setItem(STORAGE_KEYS.explanationMode, "simple"); + const store = await loadStore(); + expect(store.readExplanationMode()).toBe("simple"); + }); + + it("treats unknown stored values as detailed", async () => { + window.localStorage.setItem(STORAGE_KEYS.explanationMode, "verbose"); + const store = await loadStore(); + expect(store.readExplanationMode()).toBe("detailed"); + }); + + it("persists writes and notifies subscribers", async () => { + const store = await loadStore(); + const listener = vi.fn(); + const unsubscribe = store.subscribeExplanationMode(listener); + + store.writeExplanationMode("simple"); + expect(store.readExplanationMode()).toBe("simple"); + expect(window.localStorage.getItem(STORAGE_KEYS.explanationMode)).toBe( + "simple", + ); + expect(listener).toHaveBeenCalledTimes(1); + + unsubscribe(); + store.writeExplanationMode("detailed"); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it("re-reads storage when another tab changes the preference", async () => { + const store = await loadStore(); + expect(store.readExplanationMode()).toBe("detailed"); + const listener = vi.fn(); + store.subscribeExplanationMode(listener); + + window.localStorage.setItem(STORAGE_KEYS.explanationMode, "simple"); + window.dispatchEvent( + new StorageEvent("storage", { key: STORAGE_KEYS.explanationMode }), + ); + + expect(listener).toHaveBeenCalledTimes(1); + expect(store.readExplanationMode()).toBe("simple"); + }); + + it("keeps the in-memory value when storage is unavailable", async () => { + const store = await loadStore(); + vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new Error("blocked"); + }); + expect(() => store.writeExplanationMode("simple")).not.toThrow(); + expect(store.readExplanationMode()).toBe("simple"); + }); +}); diff --git a/src/lib/explanationMode.ts b/src/lib/explanationMode.ts new file mode 100644 index 0000000..dfb8e17 --- /dev/null +++ b/src/lib/explanationMode.ts @@ -0,0 +1,56 @@ +import { STORAGE_KEYS } from "@/content/static/storageKeys"; + +/** + * Whether step descriptions show the detailed text or the plain-language + * version. Stored per browser so the choice follows the reader between + * topics. Read through useExplanationMode. + */ +export type ExplanationMode = "detailed" | "simple"; + +const MODE_EVENT = "vjs:explanation-mode"; +const DEFAULT_MODE: ExplanationMode = "detailed"; + +let cache: ExplanationMode | null = null; + +const canUseStorage = () => typeof window !== "undefined"; + +export const readExplanationMode = (): ExplanationMode => { + if (!canUseStorage()) return DEFAULT_MODE; + if (cache) return cache; + try { + const raw = window.localStorage.getItem(STORAGE_KEYS.explanationMode); + cache = raw === "simple" ? "simple" : DEFAULT_MODE; + } catch { + cache = DEFAULT_MODE; + } + return cache; +}; + +export const writeExplanationMode = (mode: ExplanationMode) => { + if (!canUseStorage()) return; + cache = mode; + try { + window.localStorage.setItem(STORAGE_KEYS.explanationMode, mode); + } catch { + // Storage may be unavailable. Keep the in-memory value. + } + window.dispatchEvent(new Event(MODE_EVENT)); +}; + +export const subscribeExplanationMode = (callback: () => void) => { + if (!canUseStorage()) return () => undefined; + const onStorage = (event: StorageEvent) => { + if (event.key === STORAGE_KEYS.explanationMode) { + cache = null; + callback(); + } + }; + window.addEventListener(MODE_EVENT, callback); + window.addEventListener("storage", onStorage); + return () => { + window.removeEventListener(MODE_EVENT, callback); + window.removeEventListener("storage", onStorage); + }; +}; + +export const getServerExplanationMode = (): ExplanationMode => DEFAULT_MODE; diff --git a/src/lib/metadata.test.ts b/src/lib/metadata.test.ts new file mode 100644 index 0000000..298c864 --- /dev/null +++ b/src/lib/metadata.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, it } from "vitest"; +import { CATEGORIES, CATEGORY_LIST } from "@/lib/categories"; +import { CREATOR_NAME, SITE_NAME, SITE_URL } from "@/lib/constants"; +import { + createCategoryMetadata, + createTopicMetadata, + createTopicStructuredData, + getTopicDescription, + getTopicKeywords, +} from "@/lib/metadata"; +import { getTopicById, topics } from "@/lib/topics"; +import type { Topic } from "@/types"; + +const closures = getTopicById("closures")!; + +/** Narrows the structured data pair so each schema can be inspected. */ +const structuredData = (topic: Topic, summary?: string) => { + const [article, breadcrumbs] = createTopicStructuredData(topic, summary); + if (!("headline" in article) || !("itemListElement" in breadcrumbs)) { + throw new Error("Unexpected structured data order"); + } + return { article, breadcrumbs }; +}; + +describe("getTopicKeywords", () => { + it("starts with the title and the id as words", () => { + const keywords = getTopicKeywords(closures); + expect(keywords[0]).toBe("Closures"); + expect(keywords[1]).toBe("closures"); + }); + + it("splits hyphenated ids into words", () => { + const keywords = getTopicKeywords(getTopicById("event-loop")!); + expect(keywords[1]).toBe("event loop"); + }); + + it("includes the category keywords after the topic keywords", () => { + const keywords = getTopicKeywords(closures); + for (const keyword of CATEGORIES.javascript.keywords) { + expect(keywords).toContain(keyword); + } + const firstCategoryIndex = keywords.indexOf( + CATEGORIES.javascript.keywords[0], + ); + expect(firstCategoryIndex).toBeGreaterThan(1); + }); + + it("dedupes and never returns blank entries", () => { + for (const topic of topics) { + const keywords = getTopicKeywords(topic); + expect(new Set(keywords).size).toBe(keywords.length); + expect(keywords.every((keyword) => keyword.trim() === keyword)).toBe( + true, + ); + expect(keywords.every((keyword) => keyword.length > 0)).toBe(true); + expect(keywords.length).toBeGreaterThan(4); + } + }); +}); + +describe("getTopicDescription", () => { + it("has a theory description for every topic", () => { + for (const topic of topics) { + const description = getTopicDescription(topic); + expect(description.trim().length).toBeGreaterThan(40); + expect(description).not.toContain("—"); + } + }); +}); + +describe("createTopicMetadata", () => { + const metadata = createTopicMetadata(closures); + + it("builds the title from the topic and category", () => { + expect(metadata.title).toBe("Closures in JavaScript, Visualized"); + expect(metadata.category).toBe("JavaScript"); + }); + + it("points the canonical, Open Graph, and structured URLs at the route", () => { + expect(metadata.alternates?.canonical).toBe( + `${SITE_URL}/javascript/closures`, + ); + expect(metadata.openGraph?.url).toBe(`${SITE_URL}/javascript/closures`); + }); + + it("uses the theory description everywhere", () => { + const description = getTopicDescription(closures); + expect(metadata.description).toBe(description); + expect(metadata.openGraph?.description).toBe(description); + expect(metadata.twitter?.description).toBe(description); + }); + + it("brands social titles with the site name", () => { + expect(metadata.openGraph?.title).toBe( + `Closures in JavaScript, Visualized | ${SITE_NAME}`, + ); + expect(metadata.twitter?.title).toBe( + `Closures in JavaScript, Visualized | ${SITE_NAME}`, + ); + expect(metadata.openGraph).toMatchObject({ + siteName: SITE_NAME, + type: "article", + locale: "en_US", + }); + expect(metadata.twitter).toMatchObject({ card: "summary_large_image" }); + }); + + it("allows indexing", () => { + expect(metadata.robots).toEqual({ + index: true, + follow: true, + googleBot: { index: true, follow: true }, + }); + }); + + it("carries the curated keywords", () => { + expect(metadata.keywords).toEqual(getTopicKeywords(closures)); + }); +}); + +describe("createTopicStructuredData", () => { + it("returns a TechArticle and a BreadcrumbList", () => { + const { article, breadcrumbs } = structuredData(closures); + expect(article["@type"]).toBe("TechArticle"); + expect(breadcrumbs["@type"]).toBe("BreadcrumbList"); + }); + + it("describes the article with the topic's metadata", () => { + const { article } = structuredData(closures); + expect(article).toMatchObject({ + headline: "Closures in JavaScript, Visualized", + description: getTopicDescription(closures), + url: `${SITE_URL}/javascript/closures`, + inLanguage: "en-US", + keywords: getTopicKeywords(closures).join(", "), + mainEntityOfPage: `${SITE_URL}/javascript/closures`, + about: { name: "Closures", description: closures.description }, + author: { name: CREATOR_NAME }, + publisher: { name: SITE_NAME, url: SITE_URL }, + }); + }); + + it("prefers a supplied summary over the theory description", () => { + const { article } = structuredData(closures, "Short summary."); + expect(article.description).toBe("Short summary."); + }); + + it("maps difficulty to schema.org proficiency levels", () => { + const byDifficulty = new Map( + topics.map((topic) => [topic.difficulty, topic]), + ); + const level = (difficulty: "beginner" | "intermediate" | "advanced") => + structuredData(byDifficulty.get(difficulty)!).article.proficiencyLevel; + expect(level("beginner")).toBe("Beginner"); + expect(level("intermediate")).toBe("Beginner"); + expect(level("advanced")).toBe("Expert"); + }); + + it("builds a three-level breadcrumb trail", () => { + const { breadcrumbs } = structuredData(closures); + expect(breadcrumbs.itemListElement).toEqual([ + { "@type": "ListItem", position: 1, name: "Home", item: SITE_URL }, + { + "@type": "ListItem", + position: 2, + name: "JavaScript Concepts", + item: `${SITE_URL}/javascript`, + }, + { + "@type": "ListItem", + position: 3, + name: "Closures", + item: `${SITE_URL}/javascript/closures`, + }, + ]); + }); +}); + +describe("createCategoryMetadata", () => { + const react = CATEGORIES.react; + const metadata = createCategoryMetadata(react, 7); + + it("uses the index title and mentions the topic count", () => { + expect(metadata.title).toBe(react.indexTitle); + expect(metadata.description).toBe( + `Explore 7 interactive React visualizations. ${react.description}`, + ); + }); + + it("points at the category route", () => { + expect(metadata.alternates?.canonical).toBe(`${SITE_URL}/react`); + expect(metadata.openGraph?.url).toBe(`${SITE_URL}/react`); + expect(metadata.openGraph).toMatchObject({ type: "website" }); + }); + + it("merges index and category keywords without duplicates", () => { + const keywords = metadata.keywords as string[]; + expect(new Set(keywords).size).toBe(keywords.length); + for (const keyword of [...react.indexKeywords, ...react.keywords]) { + expect(keywords).toContain(keyword); + } + }); + + it("works for every category", () => { + for (const category of CATEGORY_LIST) { + const result = createCategoryMetadata(category, 3); + expect(result.title).toBe(category.indexTitle); + expect(result.alternates?.canonical).toBe(`${SITE_URL}${category.route}`); + } + }); +}); diff --git a/src/lib/metadata.ts b/src/lib/metadata.ts index df55ae0..02c9e19 100644 --- a/src/lib/metadata.ts +++ b/src/lib/metadata.ts @@ -1,43 +1,177 @@ import type { Metadata } from "next"; +import { + CATEGORIES, + CATEGORY_LIST, + type CategoryConfig, +} from "@/lib/categories"; +import type { TopicId } from "@/lib/topics"; import type { Topic } from "@/types"; import { CREATOR_LINKEDIN_URL, CREATOR_NAME, - OPEN_GRAPH_IMAGE_URL, SITE_NAME, SITE_URL, - SOCIAL_IMAGE_ALT, - SOCIAL_IMAGE_HEIGHT, - SOCIAL_IMAGE_WIDTH, - TWITTER_IMAGE_URL, } from "@/lib/constants"; -const GLOBAL_KEYWORDS = [ - "VisualizeJS", - "javascript visualizer", - "javascript concepts", - "interactive javascript tutorial", - "javascript interview preparation", - "frontend fundamentals", -]; +const CATEGORY_KEYWORDS: Record = + Object.fromEntries( + CATEGORY_LIST.map((category) => [category.id, category.keywords]), + ) as Record; -const CATEGORY_KEYWORDS: Record = { - javascript: [ - "javascript fundamentals", - "learn javascript visually", - "js runtime", - "js internals", - ], - react: [ - "react fundamentals", - "react concepts", - "react internals", - "react rendering", - "learn react visually", +const TOPIC_KEYWORDS: Record = { + tokenization: [ + "tokenization explained", + "byte pair encoding", + "bpe tokenizer", + "how llm tokenization works", + "subword tokenization", + "token ids explained", + "why do llms count tokens", + ], + embeddings: [ + "embeddings explained", + "word embeddings visualized", + "cosine similarity explained", + "vector representation of text", + "embedding lookup table", + "semantic search embeddings", + "what is an embedding", + ], + attention: [ + "self attention explained", + "attention mechanism visualized", + "query key value explained", + "transformer attention", + "causal mask explained", + "softmax attention weights", + "multi head attention", + ], + "next-token-prediction": [ + "next token prediction explained", + "llm sampling explained", + "temperature top k top p", + "how llms generate text", + "logits to probabilities softmax", + "autoregressive generation", + "greedy decoding vs sampling", + ], + backpropagation: [ + "backpropagation explained", + "gradient descent visualized", + "chain rule neural network", + "how neural networks learn", + "loss function explained", + "forward pass backward pass", + "training loop explained", + ], + "http-request-lifecycle": [ + "http request lifecycle", + "what happens when you type a url", + "dns tcp tls handshake explained", + "http request response cycle", + "how a web server handles a request", + "http headers status codes explained", + "backend request flow", + ], + "database-indexing": [ + "database indexing explained", + "b tree index visualized", + "full table scan vs index", + "how sql indexes work", + "composite index explained", + "index selectivity", + "database performance basics", + ], + "caching-strategies": [ + "caching strategies explained", + "cache aside vs write through", + "lru cache visualized", + "cache invalidation explained", + "ttl cache expiry", + "redis caching pattern", + "cache hit miss ratio", + ], + "jwt-authentication": [ + "jwt authentication explained", + "json web token visualized", + "jwt header payload signature", + "how jwt verification works", + "access token refresh token flow", + "jwt vs session auth", + "stateless authentication", + ], + "rate-limiting": [ + "rate limiting explained", + "token bucket algorithm visualized", + "sliding window rate limiter", + "fixed window vs sliding window", + "429 too many requests", + "api rate limiting strategies", + "leaky bucket vs token bucket", + ], + "vue-reactivity": [ + "vue reactivity explained", + "vue proxy reactivity", + "vue ref vs reactive", + "vue computed caching", + "dependency tracking vue", + "vue 3 reactivity system", + "track and trigger vue", + ], + "svelte-runes": [ + "svelte runes explained", + "svelte 5 signals", + "$state $derived $effect", + "svelte compiler explained", + "fine grained reactivity svelte", + "svelte vs react rendering", + "no virtual dom svelte", + ], + "angular-change-detection": [ + "angular change detection explained", + "zone.js explained", + "onpush change detection", + "angular signals explained", + "angular zoneless", + "angular dirty checking", + "angular rendering internals", + ], + "async-await": [ + "async await explained", + "how async await works under the hood", + "await microtask queue", + "async function execution order", + "promise.all vs sequential await", + "async await vs promises", + "javascript async await visualizer", + ], + "debounce-throttle": [ + "debounce vs throttle", + "debounce explained", + "throttle explained", + "debounce implementation javascript", + "throttle implementation javascript", + "search input debounce", + "scroll handler throttle", + ], + "state-batching": [ + "react state batching explained", + "automatic batching react 18", + "setstate updater function", + "why setstate is asynchronous", + "react update queue", + "multiple setstate one render", + "react 18 batching timeouts", + ], + "concurrent-rendering": [ + "react concurrent rendering explained", + "usetransition explained", + "react lanes priority", + "interruptible rendering react", + "startTransition vs setState", + "react scheduler explained", + "time slicing react", ], -}; - -const TOPIC_KEYWORDS: Record = { "event-loop": [ "javascript event loop explained", "microtask queue vs macrotask queue", @@ -265,20 +399,41 @@ const TOPIC_KEYWORDS: Record = { ], }; -const THEORY_INTENT_KEYWORDS = [ - "theory", - "explained", - "guide", - "deep dive", - "common mistakes", - "faq", - "how it works", - "interview questions", - "examples", - "tutorial", -]; - -const TOPIC_THEORY_DESCRIPTIONS: Record = { +const TOPIC_THEORY_DESCRIPTIONS: Record = { + tokenization: + "Learn how tokenizers split text into subword tokens with byte pair encoding, why token counts matter, and how ids feed the model.", + embeddings: + "Learn how token ids become vectors, why similar meanings land near each other, and how cosine similarity compares embeddings.", + attention: + "Learn how self-attention scores queries against keys, applies softmax and a causal mask, and mixes values so each token sees its context.", + "next-token-prediction": + "Learn how logits become a probability distribution, how temperature and top-p change sampling, and why generation runs one token at a time.", + backpropagation: + "Learn how a forward pass produces a loss, how the chain rule sends gradients backward, and how gradient descent nudges weights.", + "http-request-lifecycle": + "Learn the full path of an HTTP request: DNS, TCP and TLS handshakes, routing, middleware, the database call, and the response.", + "database-indexing": + "Learn how a B-tree index turns a full scan into a few page reads, how composite indexes match query order, and what indexes cost on writes.", + "caching-strategies": + "Learn cache-aside and write-through flows, how TTL and LRU eviction decide what stays, and why invalidation is the hard part.", + "jwt-authentication": + "Learn how a JWT is built from header, payload, and signature, how servers verify it without a session store, and how tampering and expiry are caught.", + "rate-limiting": + "Learn how token bucket, fixed window, and sliding window limiters decide which requests pass, and where each one lets bursts through.", + "vue-reactivity": + "Learn how Vue wraps state in a Proxy, records which effect read which property, and re-runs only those effects when a property changes.", + "svelte-runes": + "Learn how $state, $derived, and $effect form a signal graph and how the Svelte compiler emits direct DOM updates instead of diffing.", + "angular-change-detection": + "Learn how zone.js triggers change detection, how OnPush prunes the check, and how signals let Angular update only the components that read a value.", + "async-await": + "Learn how await suspends an async function, why the rest of the function runs as a microtask, and how sequential awaits differ from Promise.all.", + "debounce-throttle": + "Learn how debounce delays a call until events stop, how throttle caps the call rate, and how both are built from closures and timers.", + "state-batching": + "Learn how React queues state updates, why updater functions see the latest pending value, and how automatic batching produces one render.", + "concurrent-rendering": + "Learn how React assigns priority lanes, renders transitions in interruptible slices, and throws away stale work when an urgent update arrives.", "event-loop": "The JavaScript event loop manages async code using a call stack, microtask queue, and macrotask queue. Learn how setTimeout, Promises, and async/await work with interactive examples.", hoisting: @@ -337,37 +492,43 @@ const TOPIC_THEORY_DESCRIPTIONS: Record = { "useEffect runs side effects after React commits DOM updates and the browser paints. Learn dependency array behavior, cleanup timing, mount/unmount patterns, and common useEffect recipes.", }; +/** schema.org TechArticle only defines Beginner and Expert. */ +const PROFICIENCY_LEVELS: Record = { + beginner: "Beginner", + intermediate: "Beginner", + advanced: "Expert", +}; + function dedupeKeywords(...keywordGroups: string[][]): string[] { - return [...new Set(keywordGroups.flat().map((k) => k.trim()).filter(Boolean))]; + return [ + ...new Set( + keywordGroups + .flat() + .map((k) => k.trim()) + .filter(Boolean), + ), + ]; } +/** + * Curated keywords only. Search engines ignore the meta keywords tag, so + * generated "X explained", "X faq" combinations added nothing but bytes. + * The list still feeds the TechArticle keywords in structured data. + */ export function getTopicKeywords(topic: Topic): string[] { - const categoryLabel = topic.category === "javascript" ? "JavaScript" : "React"; - const lowerTitle = topic.title.toLowerCase(); - const lowerCategory = categoryLabel.toLowerCase(); - return dedupeKeywords( - GLOBAL_KEYWORDS, + [topic.title, topic.id.replace(/-/g, " ")], + TOPIC_KEYWORDS[topic.id as TopicId], CATEGORY_KEYWORDS[topic.category], - [topic.title, topic.id.replace(/-/g, " "), `${topic.title} explained`], - TOPIC_KEYWORDS[topic.id] ?? [], - THEORY_INTENT_KEYWORDS.map((intent) => `${topic.title} ${intent}`), - [ - `what is ${lowerTitle} in ${lowerCategory}`, - `${lowerTitle} ${lowerCategory} explained`, - `${lowerTitle} interview questions`, - `${lowerTitle} visualizer`, - `${lowerTitle} visualization`, - ], ); } export function getTopicDescription(topic: Topic): string { - return TOPIC_THEORY_DESCRIPTIONS[topic.id] ?? topic.description; + return TOPIC_THEORY_DESCRIPTIONS[topic.id as TopicId]; } export function createTopicMetadata(topic: Topic): Metadata { - const categoryLabel = topic.category === "javascript" ? "JavaScript" : "React"; + const categoryLabel = CATEGORIES[topic.category].label; const title = `${topic.title} in ${categoryLabel}, Visualized`; const canonicalUrl = `${SITE_URL}${topic.route}`; const keywords = getTopicKeywords(topic); @@ -396,28 +557,18 @@ export function createTopicMetadata(topic: Topic): Metadata { siteName: SITE_NAME, type: "article", locale: "en_US", - images: [ - { - url: OPEN_GRAPH_IMAGE_URL, - width: SOCIAL_IMAGE_WIDTH, - height: SOCIAL_IMAGE_HEIGHT, - alt: SOCIAL_IMAGE_ALT, - }, - ], }, twitter: { card: "summary_large_image", title: `${title} | ${SITE_NAME}`, description, - images: [TWITTER_IMAGE_URL], }, }; } export function createTopicStructuredData(topic: Topic, summary?: string) { - const categoryLabel = topic.category === "javascript" ? "JavaScript" : "React"; - const categoryRoute = - topic.category === "javascript" ? "/javascript" : "/react"; + const categoryLabel = CATEGORIES[topic.category].label; + const categoryRoute = CATEGORIES[topic.category].route; const canonicalUrl = `${SITE_URL}${topic.route}`; const articleSchema = { @@ -428,8 +579,7 @@ export function createTopicStructuredData(topic: Topic, summary?: string) { url: canonicalUrl, inLanguage: "en-US", keywords: getTopicKeywords(topic).join(", "), - proficiencyLevel: - topic.difficulty === "beginner" ? "Beginner" : "Intermediate", + proficiencyLevel: PROFICIENCY_LEVELS[topic.difficulty], about: { "@type": "Thing", name: topic.title, @@ -470,3 +620,34 @@ export function createTopicStructuredData(topic: Topic, summary?: string) { return [articleSchema, breadcrumbSchema]; } + +export function createCategoryMetadata( + category: CategoryConfig, + topicCount: number, +): Metadata { + const title = category.indexTitle; + const description = `Explore ${topicCount} interactive ${category.label} visualizations. ${category.description}`; + const canonicalUrl = `${SITE_URL}${category.route}`; + + return { + title, + description, + keywords: dedupeKeywords(category.indexKeywords, category.keywords), + alternates: { + canonical: canonicalUrl, + }, + openGraph: { + title: `${title} | ${SITE_NAME}`, + description, + url: canonicalUrl, + siteName: SITE_NAME, + type: "website", + locale: "en_US", + }, + twitter: { + card: "summary_large_image", + title: `${title} | ${SITE_NAME}`, + description, + }, + }; +} diff --git a/src/lib/progress/topicProgress.test.ts b/src/lib/progress/topicProgress.test.ts new file mode 100644 index 0000000..3e962be --- /dev/null +++ b/src/lib/progress/topicProgress.test.ts @@ -0,0 +1,153 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { STORAGE_KEYS } from "@/content/static/storageKeys"; + +// The store caches in module scope, so every test gets a fresh module. +const loadStore = async () => { + vi.resetModules(); + return import("@/lib/progress/topicProgress"); +}; + +describe("topicProgress", () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("reads an empty map when nothing is stored", async () => { + const store = await loadStore(); + expect(store.readTopicProgress()).toEqual({}); + }); + + it("reads what a previous session stored", async () => { + window.localStorage.setItem( + STORAGE_KEYS.topicProgress, + JSON.stringify({ closures: { completedAt: "2026-01-01T00:00:00.000Z" } }), + ); + const store = await loadStore(); + expect(store.readTopicProgress()).toEqual({ + closures: { completedAt: "2026-01-01T00:00:00.000Z" }, + }); + }); + + it("falls back to an empty map when storage holds bad JSON", async () => { + window.localStorage.setItem(STORAGE_KEYS.topicProgress, "{not json"); + const store = await loadStore(); + expect(store.readTopicProgress()).toEqual({}); + }); + + it.each(["null", "5", "[]", '"text"'])( + "falls back to an empty map when storage holds %s instead of an object", + async (raw) => { + window.localStorage.setItem(STORAGE_KEYS.topicProgress, raw); + const store = await loadStore(); + expect(store.readTopicProgress()).toEqual({}); + expect(() => store.markTopicCompleted("closures")).not.toThrow(); + expect(store.readTopicProgress().closures).toBeDefined(); + }, + ); + + it("marks a topic completed with an ISO timestamp and persists it", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-09-19T10:00:00.000Z")); + const store = await loadStore(); + store.markTopicCompleted("event-loop"); + expect(store.readTopicProgress()).toEqual({ + "event-loop": { completedAt: "2026-09-19T10:00:00.000Z" }, + }); + expect( + JSON.parse(window.localStorage.getItem(STORAGE_KEYS.topicProgress)!), + ).toEqual({ "event-loop": { completedAt: "2026-09-19T10:00:00.000Z" } }); + }); + + it("keeps the first completion time when a topic is completed again", async () => { + const store = await loadStore(); + store.markTopicCompleted("closures"); + const first = store.readTopicProgress().closures.completedAt; + const listener = vi.fn(); + store.subscribeTopicProgress(listener); + store.markTopicCompleted("closures"); + expect(store.readTopicProgress().closures.completedAt).toBe(first); + expect(listener).not.toHaveBeenCalled(); + }); + + it("returns a new map object on every change so React sees a new snapshot", async () => { + const store = await loadStore(); + const before = store.readTopicProgress(); + store.markTopicCompleted("hooks"); + const after = store.readTopicProgress(); + expect(after).not.toBe(before); + expect(store.readTopicProgress()).toBe(after); + }); + + it("notifies subscribers on mark and clear, and stops after unsubscribe", async () => { + const store = await loadStore(); + const listener = vi.fn(); + const unsubscribe = store.subscribeTopicProgress(listener); + + store.markTopicCompleted("promises"); + expect(listener).toHaveBeenCalledTimes(1); + + store.clearTopicProgress(); + expect(listener).toHaveBeenCalledTimes(2); + expect(store.readTopicProgress()).toEqual({}); + + unsubscribe(); + store.markTopicCompleted("promises"); + expect(listener).toHaveBeenCalledTimes(2); + }); + + it("refreshes from storage when another tab changes the key", async () => { + const store = await loadStore(); + expect(store.readTopicProgress()).toEqual({}); + const listener = vi.fn(); + store.subscribeTopicProgress(listener); + + window.localStorage.setItem( + STORAGE_KEYS.topicProgress, + JSON.stringify({ hoisting: { completedAt: "x" } }), + ); + window.dispatchEvent( + new StorageEvent("storage", { key: STORAGE_KEYS.topicProgress }), + ); + + expect(listener).toHaveBeenCalledTimes(1); + expect(store.readTopicProgress()).toEqual({ + hoisting: { completedAt: "x" }, + }); + }); + + it("ignores storage events for other keys", async () => { + const store = await loadStore(); + store.markTopicCompleted("closures"); + const listener = vi.fn(); + store.subscribeTopicProgress(listener); + + window.dispatchEvent( + new StorageEvent("storage", { key: "something-else" }), + ); + + expect(listener).not.toHaveBeenCalled(); + expect(store.readTopicProgress().closures).toBeDefined(); + }); + + it("keeps the in-memory copy when storage writes fail", async () => { + const store = await loadStore(); + vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new Error("QuotaExceededError"); + }); + const listener = vi.fn(); + store.subscribeTopicProgress(listener); + + expect(() => store.markTopicCompleted("closures")).not.toThrow(); + expect(store.readTopicProgress().closures).toBeDefined(); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it("serves an empty map on the server", async () => { + const store = await loadStore(); + expect(store.getServerTopicProgress()).toEqual({}); + }); +}); diff --git a/src/lib/progress/topicProgress.ts b/src/lib/progress/topicProgress.ts new file mode 100644 index 0000000..6f323c5 --- /dev/null +++ b/src/lib/progress/topicProgress.ts @@ -0,0 +1,80 @@ +import { STORAGE_KEYS } from "@/content/static/storageKeys"; + +/** + * Topic completion is stored per browser in localStorage. A topic counts as + * completed once the reader reaches the final step of its visualization. + * Components subscribe through useTopicProgress, which listens for the + * custom event below so every mounted consumer updates at once. + */ + +export type TopicProgressMap = Record; + +const PROGRESS_EVENT = "vjs:topic-progress"; +const EMPTY: TopicProgressMap = {}; + +let cache: TopicProgressMap | null = null; + +const canUseStorage = () => typeof window !== "undefined"; + +/** Storage can hold anything; only a plain object counts as a progress map. */ +const isProgressMap = (value: unknown): value is TopicProgressMap => + typeof value === "object" && value !== null && !Array.isArray(value); + +export const readTopicProgress = (): TopicProgressMap => { + if (!canUseStorage()) return EMPTY; + if (cache) return cache; + try { + const raw = window.localStorage.getItem(STORAGE_KEYS.topicProgress); + const parsed: unknown = raw ? JSON.parse(raw) : {}; + cache = isProgressMap(parsed) ? parsed : {}; + } catch { + cache = {}; + } + return cache; +}; + +const writeTopicProgress = (next: TopicProgressMap) => { + cache = next; + try { + window.localStorage.setItem( + STORAGE_KEYS.topicProgress, + JSON.stringify(next), + ); + } catch { + // Storage may be unavailable in private mode. Keep the in-memory copy. + } + window.dispatchEvent(new Event(PROGRESS_EVENT)); +}; + +export const markTopicCompleted = (topicId: string) => { + if (!canUseStorage()) return; + const current = readTopicProgress(); + if (current[topicId]) return; + writeTopicProgress({ + ...current, + [topicId]: { completedAt: new Date().toISOString() }, + }); +}; + +export const clearTopicProgress = () => { + if (!canUseStorage()) return; + writeTopicProgress({}); +}; + +export const subscribeTopicProgress = (callback: () => void) => { + if (!canUseStorage()) return () => undefined; + const onStorage = (event: StorageEvent) => { + if (event.key === STORAGE_KEYS.topicProgress) { + cache = null; + callback(); + } + }; + window.addEventListener(PROGRESS_EVENT, callback); + window.addEventListener("storage", onStorage); + return () => { + window.removeEventListener(PROGRESS_EVENT, callback); + window.removeEventListener("storage", onStorage); + }; +}; + +export const getServerTopicProgress = (): TopicProgressMap => EMPTY; diff --git a/src/lib/sandbox/configs.test.ts b/src/lib/sandbox/configs.test.ts new file mode 100644 index 0000000..18c7e89 --- /dev/null +++ b/src/lib/sandbox/configs.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { + SANDBOX_CONFIGS, + getSandboxConfig, + isSandboxEnabled, +} from "@/lib/sandbox/configs"; +import { generateEventLoopSteps } from "@/lib/sandbox/generators/event-loop"; +import { parseUserCode } from "@/lib/sandbox/parser"; +import { isTopicId } from "@/lib/topics"; + +describe("sandbox configs", () => { + it("keys every config by a real topic id that matches its topicId", () => { + expect(Object.keys(SANDBOX_CONFIGS).length).toBeGreaterThan(0); + for (const [key, config] of Object.entries(SANDBOX_CONFIGS)) { + expect(config.topicId).toBe(key); + expect(isTopicId(key)).toBe(true); + } + }); + + it("ships default code that fits its own limits", () => { + for (const config of Object.values(SANDBOX_CONFIGS)) { + expect(config.defaultCode.length).toBeLessThanOrEqual( + config.maxCodeLength, + ); + expect(config.defaultCode.split("\n").length).toBeLessThanOrEqual( + config.maxCodeLines, + ); + expect(config.supportedPatterns.length).toBeGreaterThan(0); + } + }); + + it("ships event loop default code that parses and generates steps", () => { + const parsed = parseUserCode(SANDBOX_CONFIGS["event-loop"].defaultCode); + expect(parsed.success).toBe(true); + if (!parsed.success) return; + const result = generateEventLoopSteps( + parsed.ast, + SANDBOX_CONFIGS["event-loop"].defaultCode, + ); + expect(result.success).toBe(true); + }); +}); + +describe("isSandboxEnabled", () => { + it("is true only for topics with a config", () => { + expect(isSandboxEnabled("event-loop")).toBe(true); + expect(isSandboxEnabled("closures")).toBe(false); + expect(isSandboxEnabled("")).toBe(false); + }); + + it("ignores properties inherited from Object.prototype", () => { + expect(isSandboxEnabled("toString")).toBe(false); + expect(isSandboxEnabled("constructor")).toBe(false); + }); +}); + +describe("getSandboxConfig", () => { + it("returns the config or null", () => { + expect(getSandboxConfig("event-loop")).toBe(SANDBOX_CONFIGS["event-loop"]); + expect(getSandboxConfig("closures")).toBeNull(); + expect(getSandboxConfig("toString")).toBeNull(); + }); +}); diff --git a/src/lib/sandbox/configs.ts b/src/lib/sandbox/configs.ts index 735602b..07d2790 100644 --- a/src/lib/sandbox/configs.ts +++ b/src/lib/sandbox/configs.ts @@ -52,9 +52,9 @@ console.log('End');`, }; export function isSandboxEnabled(topicId: string): boolean { - return topicId in SANDBOX_CONFIGS; + return Object.hasOwn(SANDBOX_CONFIGS, topicId); } export function getSandboxConfig(topicId: string): SandboxConfig | null { - return SANDBOX_CONFIGS[topicId] ?? null; + return isSandboxEnabled(topicId) ? SANDBOX_CONFIGS[topicId] : null; } diff --git a/src/lib/sandbox/editor-theme.ts b/src/lib/sandbox/editor-theme.ts index 7eec736..2e8ec9a 100644 --- a/src/lib/sandbox/editor-theme.ts +++ b/src/lib/sandbox/editor-theme.ts @@ -87,4 +87,7 @@ const highlightStyle = HighlightStyle.define([ { tag: tags.regexp, color: "#f472b6" }, ]); -export const visualizeJsTheme = [editorTheme, syntaxHighlighting(highlightStyle)]; +export const visualizeJsTheme = [ + editorTheme, + syntaxHighlighting(highlightStyle), +]; diff --git a/src/lib/sandbox/generatorUtils.test.ts b/src/lib/sandbox/generatorUtils.test.ts new file mode 100644 index 0000000..217ac25 --- /dev/null +++ b/src/lib/sandbox/generatorUtils.test.ts @@ -0,0 +1,211 @@ +import * as acorn from "acorn"; +import type { CallExpression, ExpressionStatement, Node } from "acorn"; +import { describe, expect, it } from "vitest"; +import { + callbackLabel, + createError, + escapeHtml, + expressionToLabel, + extractCallbackFromArg, + extractConsoleLogArg, + getCallbackBody, + getLineRange, + resolveNodeToString, +} from "@/lib/sandbox/generatorUtils"; + +const parse = (source: string) => + acorn.parse(source, { ecmaVersion: 2023, locations: true }); + +const firstExpression = (source: string) => + (parse(source).body[0] as ExpressionStatement).expression; + +const firstCall = (source: string) => + firstExpression(source) as unknown as CallExpression & { + callee: Node; + arguments: Node[]; + }; + +describe("getLineRange", () => { + it("lists every line a node spans, inclusive", () => { + const source = "setTimeout(() => {\n a();\n b();\n}, 0);"; + expect(getLineRange(parse(source).body[0])).toEqual([1, 2, 3, 4]); + }); + + it("returns a single line for a one-line node", () => { + expect(getLineRange(parse("x;").body[0])).toEqual([1]); + }); + + it("returns nothing when the node has no location info", () => { + const node = acorn.parse("x;", { ecmaVersion: 2023 }).body[0]; + expect(getLineRange(node)).toEqual([]); + }); +}); + +describe("expressionToLabel", () => { + it("returns the source text when it fits", () => { + const source = "console.log('short');"; + expect(expressionToLabel(firstExpression(source), source)).toBe( + "console.log('short')", + ); + }); + + it("truncates long source with an ellipsis at the limit", () => { + const source = + "someFunction('a very long argument that keeps going and going');"; + const label = expressionToLabel(firstExpression(source), source, 20); + expect(label).toBe("someFunction('a v..."); + expect(label).toHaveLength(20); + }); +}); + +describe("callbackLabel", () => { + it("wraps trimmed body source in an arrow", () => { + expect(callbackLabel(" console.log('x') ")).toBe( + "() => console.log('x')", + ); + }); + + it("truncates long bodies", () => { + expect(callbackLabel("console.log('a much longer body here')", 12)).toBe( + "() => console.l...", + ); + }); +}); + +describe("getCallbackBody", () => { + it("returns block statements for a block body", () => { + const call = firstCall("run(() => { a(); b(); });"); + const body = getCallbackBody(call.arguments[0]); + expect(body?.map((node) => node.type)).toEqual([ + "ExpressionStatement", + "ExpressionStatement", + ]); + }); + + it("wraps an expression body in an array", () => { + const call = firstCall("run(() => a());"); + const body = getCallbackBody(call.arguments[0]); + expect(body?.map((node) => node.type)).toEqual(["CallExpression"]); + }); + + it("supports function expressions", () => { + const call = firstCall("run(function () { a(); });"); + expect(getCallbackBody(call.arguments[0])).toHaveLength(1); + }); + + it("returns null for non-function arguments", () => { + const call = firstCall("run(42);"); + expect(getCallbackBody(call.arguments[0])).toBeNull(); + }); +}); + +describe("extractCallbackFromArg", () => { + it("builds a label, body, and line range", () => { + const source = "setTimeout(() => {\n a();\n b();\n}, 0);"; + const call = firstCall(source); + const info = extractCallbackFromArg(call.arguments[0], source); + expect(info).toEqual({ + label: "() => a(); b();", + body: expect.any(Array), + bodyLines: [2, 3], + }); + expect(info?.body).toHaveLength(2); + }); + + it("returns null when the argument is not a function", () => { + const source = "setTimeout('a', 0);"; + expect( + extractCallbackFromArg(firstCall(source).arguments[0], source), + ).toBeNull(); + }); +}); + +describe("resolveNodeToString", () => { + it("prints literals by value", () => { + const source = "42;"; + expect(resolveNodeToString(firstExpression(source), source)).toBe("42"); + const text = "'hello';"; + expect(resolveNodeToString(firstExpression(text), text)).toBe("hello"); + }); + + it("substitutes identifiers from scope and keeps unknown names", () => { + const source = "name;"; + const node = firstExpression(source); + expect(resolveNodeToString(node, source, { name: "Ann" })).toBe("Ann"); + expect(resolveNodeToString(node, source)).toBe("name"); + }); + + it("interpolates template literals using scope", () => { + const source = "`Hi ${name}, you are ${age}`;"; + expect( + resolveNodeToString(firstExpression(source), source, { name: "Bo" }), + ).toBe("Hi Bo, you are age"); + }); + + it("falls back to the source text for other expressions", () => { + const source = "a + b;"; + expect(resolveNodeToString(firstExpression(source), source)).toBe("a + b"); + }); +}); + +describe("extractConsoleLogArg", () => { + it("returns the first argument of console.log", () => { + const source = "console.log('Start');"; + expect(extractConsoleLogArg(firstCall(source), source)).toBe("Start"); + }); + + it("resolves identifiers through scope", () => { + const source = "console.log(msg);"; + expect(extractConsoleLogArg(firstCall(source), source, { msg: "hi" })).toBe( + "hi", + ); + }); + + it("returns an empty string for console.log()", () => { + const source = "console.log();"; + expect(extractConsoleLogArg(firstCall(source), source)).toBe(""); + }); + + it("returns null for other calls", () => { + const warn = "console.warn('x');"; + expect(extractConsoleLogArg(firstCall(warn), warn)).toBeNull(); + const plain = "log('x');"; + expect(extractConsoleLogArg(firstCall(plain), plain)).toBeNull(); + const member = "logger.log('x');"; + expect(extractConsoleLogArg(firstCall(member), member)).toBeNull(); + }); +}); + +describe("createError", () => { + it("omits line and column when they are not given", () => { + expect(createError("parse-error", "bad")).toEqual({ + type: "parse-error", + message: "bad", + }); + }); + + it("includes line and column when given, even zero", () => { + expect(createError("unsupported-pattern", "bad", 3, 0)).toEqual({ + type: "unsupported-pattern", + message: "bad", + line: 3, + column: 0, + }); + }); +}); + +describe("escapeHtml", () => { + it("escapes every character that can break out of markup", () => { + expect(escapeHtml(`&`)).toBe( + "<a href="x" title='y'>&</a>", + ); + }); + + it("leaves safe text alone", () => { + expect(escapeHtml("plain text 123")).toBe("plain text 123"); + }); + + it("escapes ampersands first so entities are not double encoded", () => { + expect(escapeHtml("<")).toBe("&lt;"); + }); +}); diff --git a/src/lib/sandbox/generatorUtils.ts b/src/lib/sandbox/generatorUtils.ts index 1f78608..38e39ae 100644 --- a/src/lib/sandbox/generatorUtils.ts +++ b/src/lib/sandbox/generatorUtils.ts @@ -38,7 +38,11 @@ export function getLineRange(node: Node): number[] { /* ------------------------------------------------------------------ */ /** Create a short label from a node's source text (truncated to `maxLen`). */ -export function expressionToLabel(node: Node, source: string, maxLen = 45): string { +export function expressionToLabel( + node: Node, + source: string, + maxLen = 45, +): string { const raw = source.slice(node.start, node.end); if (raw.length <= maxLen) return raw; return raw.slice(0, maxLen - 3) + "..."; @@ -138,8 +142,10 @@ export function extractConsoleLogArg( const callee = node.callee; if ( callee.type === "MemberExpression" && - ((callee as unknown as MemberExpression).object as Identifier).name === "console" && - ((callee as unknown as MemberExpression).property as Identifier).name === "log" + ((callee as unknown as MemberExpression).object as Identifier).name === + "console" && + ((callee as unknown as MemberExpression).property as Identifier).name === + "log" ) { if (node.arguments.length === 0) return ""; return resolveNodeToString(node.arguments[0], source, scope); @@ -158,7 +164,12 @@ export function createError( line?: number, column?: number, ): SandboxError { - return { type, message, ...(line != null ? { line } : {}), ...(column != null ? { column } : {}) }; + return { + type, + message, + ...(line != null ? { line } : {}), + ...(column != null ? { column } : {}), + }; } /** Escape untrusted text before interpolating into HTML strings. */ diff --git a/src/lib/sandbox/generators/event-loop.test.ts b/src/lib/sandbox/generators/event-loop.test.ts new file mode 100644 index 0000000..315858f --- /dev/null +++ b/src/lib/sandbox/generators/event-loop.test.ts @@ -0,0 +1,419 @@ +import { describe, expect, it } from "vitest"; +import { SANDBOX_CONFIGS } from "@/lib/sandbox/configs"; +import { + generateEventLoopSteps, + type EventLoopStep, +} from "@/lib/sandbox/generators/event-loop"; +import { parseUserCode } from "@/lib/sandbox/parser"; + +const run = (source: string) => { + const parsed = parseUserCode(source); + if (!parsed.success) throw new Error(parsed.error.message); + return generateEventLoopSteps(parsed.ast, source); +}; + +const steps = (source: string): EventLoopStep[] => { + const result = run(source); + if (!result.success) throw new Error(result.error.message); + return result.steps; +}; + +const output = (source: string) => steps(source).at(-1)?.consoleOutput ?? []; + +const failure = (source: string) => { + const result = run(source); + if (result.success) throw new Error("expected generation to fail"); + return result.error; +}; + +/** Every step is a snapshot: shape checks that hold for any program. */ +const expectWellFormed = (source: string) => { + const result = run(source); + expect(result.success).toBe(true); + if (!result.success) return; + const lineCount = source.split("\n").length; + expect(result.codeLines).toHaveLength(lineCount); + result.codeLines.forEach((line, index) => { + expect(line.num).toBe(index + 1); + }); + + const list = result.steps; + expect(list.length).toBeGreaterThan(0); + for (let i = 0; i < list.length; i += 1) { + const step = list[i]; + expect(step.descriptionHtml.trim().length).toBeGreaterThan(0); + expect([...step.doneLines]).toEqual( + [...step.doneLines].sort((a, b) => a - b), + ); + expect(new Set(step.doneLines).size).toBe(step.doneLines.length); + for (const line of step.doneLines) { + expect(line).toBeGreaterThanOrEqual(1); + expect(line).toBeLessThanOrEqual(lineCount); + } + if (step.activeLine !== null) { + expect(step.activeLine).toBeGreaterThanOrEqual(1); + expect(step.activeLine).toBeLessThanOrEqual(lineCount); + } + expect(["idle", "checking", "running"]).toContain(step.loopLabel); + if (step.loopLabel === "idle") expect(step.loopActive).toBe(false); + if (step.loopLabel !== "idle") expect(step.loopActive).toBe(true); + + const next = list[i + 1]; + if (next) { + // Console output only grows, one line at a time. + expect(next.consoleOutput.slice(0, step.consoleOutput.length)).toEqual( + step.consoleOutput, + ); + expect( + next.consoleOutput.length - step.consoleOutput.length, + ).toBeLessThanOrEqual(1); + // Snapshots never share arrays, so playback can move in both directions. + expect(next.stack).not.toBe(step.stack); + expect(next.consoleOutput).not.toBe(step.consoleOutput); + expect(next.doneLines).not.toBe(step.doneLines); + } + } + + const last = list[list.length - 1]; + expect(last.stack).toEqual([]); + expect(last.webApis).toEqual([]); + expect(last.taskQueue).toEqual([]); + expect(last.microtaskQueue).toEqual([]); + expect(last.loopLabel).toBe("idle"); + expect(last.descriptionHtml).toContain("Done!"); + expect(last.doneLines).toEqual( + Array.from({ length: lineCount }, (_, index) => index + 1), + ); +}; + +describe("generateEventLoopSteps", () => { + describe("default sandbox program", () => { + const source = SANDBOX_CONFIGS["event-loop"].defaultCode; + + it("logs synchronous code, then microtasks, then the timer", () => { + expect(output(source)).toEqual([ + "Start", + "End", + "Promise 1", + "Promise 2", + "Timeout", + ]); + }); + + it("summarises the output order in the final step", () => { + const last = steps(source).at(-1)!; + expect(last.descriptionHtml).toContain( + "Start → End → Promise 1 → Promise 2 → Timeout", + ); + }); + + it("produces well formed snapshots", () => { + expectWellFormed(source); + }); + + it("shows the timer in Web APIs while it is pending", () => { + const withTimer = steps(source).filter((step) => + step.webApis.includes("Timer (0ms)"), + ); + expect(withTimer.length).toBe(1); + expect(withTimer[0].descriptionHtml).toContain("Web APIs"); + }); + + it("moves the timer callback into the task queue before any microtask runs", () => { + const list = steps(source); + const queued = list.findIndex((step) => step.taskQueue.length === 1); + const firstMicroRun = list.findIndex((step) => + step.consoleOutput.includes("Promise 1"), + ); + expect(queued).toBeGreaterThan(-1); + expect(queued).toBeLessThan(firstMicroRun); + expect(list[queued].taskQueue[0]).toBe("() => console.log('Timeout');"); + }); + + it("keeps the loop idle during synchronous execution and active while draining", () => { + const list = steps(source); + const endIndex = list.findIndex((step) => + step.consoleOutput.includes("End"), + ); + for (const step of list.slice(0, endIndex + 1)) { + expect(step.loopLabel).toBe("idle"); + } + const checking = list.find((step) => step.loopLabel === "checking"); + expect(checking?.descriptionHtml).toContain("Event Loop"); + }); + }); + + describe("ordering semantics", () => { + it("runs microtasks before tasks even when the timer was scheduled first", () => { + expect( + output( + "setTimeout(() => console.log('T'), 0);\nPromise.resolve().then(() => console.log('M'));", + ), + ).toEqual(["M", "T"]); + }); + + it("treats queueMicrotask and promise callbacks as one FIFO queue", () => { + expect( + output( + "Promise.resolve().then(() => console.log('P'));\nqueueMicrotask(() => console.log('Q'));", + ), + ).toEqual(["P", "Q"]); + expect( + output( + "queueMicrotask(() => console.log('Q'));\nPromise.resolve().then(() => console.log('P'));", + ), + ).toEqual(["Q", "P"]); + }); + + it("interleaves promise chains the way the real microtask queue does", () => { + const source = [ + "Promise.resolve().then(() => console.log('A1')).then(() => console.log('A2'));", + "Promise.resolve().then(() => console.log('B1'));", + ].join("\n"); + expect(output(source)).toEqual(["A1", "B1", "A2"]); + }); + + it("queues the next link of a chain only after the previous one ran", () => { + const source = + "Promise.resolve()\n .then(() => console.log('one'))\n .then(() => console.log('two'));"; + const list = steps(source); + const chained = list.find((step) => + step.descriptionHtml.includes( + "Promise chaining queues another callback", + ), + ); + expect(chained).toBeDefined(); + expect(chained?.consoleOutput).toEqual(["one"]); + expect(chained?.microtaskQueue).toEqual(["() => console.log('two')"]); + expect(output(source)).toEqual(["one", "two"]); + }); + + it("runs timers in FIFO order", () => { + expect( + output( + "setTimeout(() => console.log('T1'), 100);\nsetTimeout(() => console.log('T2'), 0);", + ), + ).toEqual(["T1", "T2"]); + }); + + it("drains microtasks scheduled from inside a task before the next task", () => { + const source = [ + "setTimeout(() => {", + " Promise.resolve().then(() => console.log('inner'));", + " console.log('timeout 1');", + "}, 0);", + "setTimeout(() => console.log('timeout 2'), 0);", + ].join("\n"); + expect(output(source)).toEqual(["timeout 1", "inner", "timeout 2"]); + expectWellFormed(source); + }); + + it("runs a microtask scheduled from a microtask before any task", () => { + const source = [ + "setTimeout(() => console.log('task'), 0);", + "queueMicrotask(() => {", + " queueMicrotask(() => console.log('nested'));", + " console.log('outer');", + "});", + ].join("\n"); + expect(output(source)).toEqual(["outer", "nested", "task"]); + }); + }); + + describe("statement handling", () => { + it("hoists function declarations and shows calls on the stack", () => { + const source = [ + "function greet() {", + " console.log('hi');", + "}", + "greet();", + ].join("\n"); + const list = steps(source); + expect(list[0].descriptionHtml).toContain("hoisted"); + expect(list[0].stack).toEqual(["function greet()"]); + const call = list.find((step) => step.stack[0] === "greet()"); + expect(call?.activeLine).toBe(4); + const inner = list.find((step) => step.stack.length === 2); + expect(inner?.stack).toEqual(["greet()", "console.log('hi')"]); + expect(output(source)).toEqual(["hi"]); + }); + + it("resolves parameters through template literals", () => { + const source = [ + "function greet(name) {", + " console.log(`Hi ${name}`);", + "}", + "greet('Bob');", + "greet('Ann');", + ].join("\n"); + expect(output(source)).toEqual(["Hi Bob", "Hi Ann"]); + }); + + it("hints when a function is declared but never called", () => { + const last = steps("function unused() {}\nconsole.log('x');").at(-1)!; + expect(last.descriptionHtml).toContain("declared but never called"); + expect(last.descriptionHtml).toContain("unused()"); + }); + + it("does not hint when every function was called", () => { + const last = steps("function f() {}\nf();").at(-1)!; + expect(last.descriptionHtml).not.toContain("Hint"); + }); + + it("handles arrow callbacks with expression bodies", () => { + expect(output("setTimeout(() => console.log('x'), 0);")).toEqual(["x"]); + }); + + it("treats variable declarations as synchronous work with a short label", () => { + const list = steps("const total = 1 + 2;"); + expect(list[0].stack).toEqual(["const total = 1 + 2"]); + expect(list[0].activeLine).toBe(1); + }); + + it("truncates long variable declaration labels", () => { + const list = steps( + "const aVeryLongVariableName = 'some fairly long string value';", + ); + expect(list[0].stack[0].endsWith("...")).toBe(true); + expect(list[0].stack[0].length).toBe(40); + }); + + it("treats unknown expressions as synchronous work", () => { + const list = steps("1 + 1;"); + expect(list[0].descriptionHtml).toContain("1 + 1"); + expect(list[0].stack).toEqual(["1 + 1"]); + }); + + it("skips empty statements without emitting steps", () => { + expect(steps("console.log('a');;").length).toBe( + steps("console.log('a');").length, + ); + }); + + it("copes with setTimeout and queueMicrotask without a callback", () => { + const list = steps("setTimeout();\nqueueMicrotask();"); + expect(list.some((step) => step.taskQueue[0] === "() => { ... }")).toBe( + true, + ); + expect( + list.some((step) => step.microtaskQueue[0] === "() => { ... }"), + ).toBe(true); + expect(list.at(-1)?.consoleOutput).toEqual([]); + }); + + it("reports no output when nothing was logged", () => { + expect(steps("const a = 1;").at(-1)?.descriptionHtml).toContain( + "(no output)", + ); + }); + + it("prints an empty string for console.log()", () => { + expect(output("console.log();")).toEqual([""]); + }); + }); + + describe("line tracking", () => { + it("marks a callback body pending while it runs and done afterwards", () => { + const source = [ + "setTimeout(() => {", + " console.log('a');", + "}, 0);", + ].join("\n"); + const list = steps(source); + const executing = list.find( + (step) => step.activeLine === 2 && step.consoleOutput.includes("a"), + ); + expect(executing).toBeDefined(); + expect(executing?.doneLines).not.toContain(2); + expect(list.at(-1)?.doneLines).toEqual([1, 2, 3]); + }); + + it("marks the whole promise chain done once it is scheduled", () => { + const source = + "Promise.resolve()\n .then(() => console.log('a'));\nconsole.log('b');"; + const list = steps(source); + const sync = list.find((step) => step.consoleOutput.includes("b")); + expect(sync?.doneLines).toEqual([1, 2]); + }); + }); + + describe("errors", () => { + it("rejects an empty program", () => { + const error = failure(""); + expect(error.type).toBe("generation-error"); + expect(error.message).toContain("No statements found"); + }); + + it("rejects a program that only has comments", () => { + expect(failure("// nothing here").type).toBe("generation-error"); + }); + + it("rejects unsupported statements with their line", () => { + const error = failure("console.log('a');\nif (true) {}"); + expect(error.type).toBe("unsupported-pattern"); + expect(error.line).toBe(2); + expect(error.message).toContain("Unsupported statement: IfStatement"); + }); + + it("suggests Promise.resolve for a misspelled promise method", () => { + const error = failure("Promise.resolv().then(() => {});"); + expect(error.type).toBe("unsupported-pattern"); + expect(error.message).toBe( + "Promise.resolv() is not a function. Did you mean Promise.resolve()?", + ); + }); + + it("rejects .then() on anything but Promise.resolve()", () => { + const error = failure("fetch('/api').then(() => {});"); + expect(error.type).toBe("unsupported-pattern"); + expect(error.message).toContain( + ".then() is only supported on Promise.resolve() chains", + ); + }); + + it("surfaces unsupported statements found inside a callback", () => { + const error = failure("setTimeout(() => {\n while (true) {}\n}, 0);"); + expect(error.type).toBe("unsupported-pattern"); + expect(error.line).toBe(2); + expect(error.message).toContain("WhileStatement"); + }); + }); + + describe("html safety", () => { + const payload = ""; + const source = `console.log('${payload}');`; + + it("escapes user text in every description while keeping console output raw", () => { + const list = steps(source); + for (const step of list) { + expect(step.descriptionHtml).not.toContain(" { + const list = steps(`const s = "a & b";`); + expect(list[0].descriptionHtml).toContain(""a & b""); + }); + }); + + it("produces well formed snapshots for a mixed program", () => { + expectWellFormed( + [ + "function work(label) {", + " console.log(`start ${label}`);", + " queueMicrotask(() => console.log(`micro ${label}`));", + "}", + "work('one');", + "setTimeout(() => {", + " work('two');", + "}, 10);", + "Promise.resolve().then(() => console.log('then'));", + "console.log('sync end');", + ].join("\n"), + ); + }); +}); diff --git a/src/lib/sandbox/generators/event-loop.ts b/src/lib/sandbox/generators/event-loop.ts index 9fc1c33..d27cac7 100644 --- a/src/lib/sandbox/generators/event-loop.ts +++ b/src/lib/sandbox/generators/event-loop.ts @@ -19,16 +19,12 @@ import { resolveNodeToString, } from "@/lib/sandbox/generatorUtils"; import type { SandboxError } from "@/types/sandbox"; +import type { SourceLine } from "@/types/visualization"; /* ------------------------------------------------------------------ */ -/* Types matching EventLoop.tsx */ +/* Types matching the event-loop visualization */ /* ------------------------------------------------------------------ */ -export interface SourceLine { - num: number; - text: string; -} - export interface EventLoopStep { descriptionHtml: string; activeLine: number | null; @@ -68,7 +64,7 @@ interface SimState { taskQueue: ScheduledCallback[]; microtaskQueue: ScheduledCallback[]; consoleOutput: string[]; - doneLines: number[]; + doneLines: Set; steps: EventLoopStep[]; /** User-defined functions (name → declaration node) */ functions: Map; @@ -81,11 +77,17 @@ type RuntimeScope = Record; /* ------------------------------------------------------------------ */ function isSetTimeout(node: CallExpression): boolean { - return node.callee.type === "Identifier" && (node.callee as Identifier).name === "setTimeout"; + return ( + node.callee.type === "Identifier" && + (node.callee as Identifier).name === "setTimeout" + ); } function isQueueMicrotask(node: CallExpression): boolean { - return node.callee.type === "Identifier" && (node.callee as Identifier).name === "queueMicrotask"; + return ( + node.callee.type === "Identifier" && + (node.callee as Identifier).name === "queueMicrotask" + ); } function isPromiseResolveThen(node: CallExpression): boolean { @@ -116,7 +118,8 @@ function detectPromiseMethodTypo(node: CallExpression): string | null { while ( current.callee.type === "MemberExpression" && (current.callee as MemberExpression).property.type === "Identifier" && - ((current.callee as MemberExpression).property as Identifier).name === "then" + ((current.callee as MemberExpression).property as Identifier).name === + "then" ) { const obj = (current.callee as MemberExpression).object; if (obj.type !== "CallExpression") break; @@ -153,11 +156,14 @@ function isUserFunctionCall(node: CallExpression, state: SimState): boolean { /* Step emitters */ /* ------------------------------------------------------------------ */ -function pushStep(state: SimState, partial: Partial & { descriptionHtml: string }) { +function pushStep( + state: SimState, + partial: Partial & { descriptionHtml: string }, +) { state.steps.push({ descriptionHtml: partial.descriptionHtml, activeLine: partial.activeLine ?? null, - doneLines: [...state.doneLines], + doneLines: [...state.doneLines].sort((a, b) => a - b), stack: [...state.stack], webApis: [...state.webApis], taskQueue: state.taskQueue.map((c) => c.label), @@ -169,9 +175,22 @@ function pushStep(state: SimState, partial: Partial & { descripti } function markDone(state: SimState, lines: number[]) { - for (const l of lines) { - if (!state.doneLines.includes(l)) state.doneLines.push(l); - } + for (const line of lines) state.doneLines.add(line); +} + +/** Un-mark lines so a callback body shows as active while it executes. */ +function markPending(state: SimState, lines: number[]) { + for (const line of lines) state.doneLines.delete(line); +} + +/** Loop indicator state for a step that runs synchronously. */ +function syncLoopState( + isInsideCallback: boolean, +): Pick { + return { + loopActive: isInsideCallback, + loopLabel: isInsideCallback ? "running" : "idle", + }; } /* ------------------------------------------------------------------ */ @@ -192,11 +211,17 @@ function collectThenChain( function walk(n: CallExpression) { if (n.callee.type === "MemberExpression") { const obj = (n.callee as MemberExpression).object; - if (obj.type === "CallExpression" && isPromiseResolveThen(obj as CallExpression)) { + if ( + obj.type === "CallExpression" && + isPromiseResolveThen(obj as CallExpression) + ) { walk(obj as CallExpression); } } - if ((((n.callee as MemberExpression).property) as Identifier).name === "then" && n.arguments.length > 0) { + if ( + ((n.callee as MemberExpression).property as Identifier).name === "then" && + n.arguments.length > 0 + ) { const cb = extractCallbackFromArg(n.arguments[0], source); if (cb) { entries.push({ callback: cb, callNode: n }); @@ -236,8 +261,7 @@ function processCallExpression( pushStep(state, { descriptionHtml: `Call Stack executes ${safeLabel}. Output: ${safeLogArg}.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); state.stack.pop(); markDone(state, lines); @@ -250,7 +274,9 @@ function processCallExpression( expr.arguments[1]?.type === "Literal" ? String((expr.arguments[1] as Literal).value) : "0"; - const cb = expr.arguments[0] ? extractCallbackFromArg(expr.arguments[0], source) : null; + const cb = expr.arguments[0] + ? extractCallbackFromArg(expr.arguments[0], source) + : null; const stLabel = `setTimeout(cb, ${delay})`; const safeStLabel = escapeHtml(stLabel); @@ -258,8 +284,7 @@ function processCallExpression( pushStep(state, { descriptionHtml: `${safeStLabel} is pushed to the Call Stack.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); state.stack.pop(); @@ -267,8 +292,7 @@ function processCallExpression( pushStep(state, { descriptionHtml: `setTimeout delegates to Web APIs, and a timer starts.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); state.webApis.pop(); @@ -283,8 +307,7 @@ function processCallExpression( pushStep(state, { descriptionHtml: `Timer completes, so callback moves into the Task Queue.`, activeLine: null, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); markDone(state, lines); @@ -294,7 +317,9 @@ function processCallExpression( // queueMicrotask(cb) if (isQueueMicrotask(expr)) { - const cb = expr.arguments[0] ? extractCallbackFromArg(expr.arguments[0], source) : null; + const cb = expr.arguments[0] + ? extractCallbackFromArg(expr.arguments[0], source) + : null; const qmLabel = "queueMicrotask(cb)"; const safeQmLabel = escapeHtml(qmLabel); @@ -302,8 +327,7 @@ function processCallExpression( pushStep(state, { descriptionHtml: `${safeQmLabel} is pushed to the Call Stack.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); state.stack.pop(); @@ -318,8 +342,7 @@ function processCallExpression( pushStep(state, { descriptionHtml: `Callback is queued in the Microtask Queue (same priority as Promise callbacks).`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); markDone(state, lines); @@ -338,8 +361,7 @@ function processCallExpression( pushStep(state, { descriptionHtml: `${safePromiseLabel} is pushed to the Call Stack.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); state.stack.pop(); @@ -359,8 +381,7 @@ function processCallExpression( pushStep(state, { descriptionHtml: `Promise resolves and the first callback enters the Microtask Queue (higher priority than tasks).`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); markDone(state, allLines); @@ -386,13 +407,12 @@ function processCallExpression( pushStep(state, { descriptionHtml: `${safeFnLabel} is called and pushed to the Call Stack.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); // Un-mark function body lines so they show as active when executing const fnBodyLines = fnDecl.body.body.flatMap(getLineRange); - state.doneLines = state.doneLines.filter((l) => !fnBodyLines.includes(l)); + markPending(state, fnBodyLines); for (const stmt of fnDecl.body.body) { const err = processStatement( @@ -457,8 +477,7 @@ function processStatement( pushStep(state, { descriptionHtml: `${safeFnLabel} declaration is hoisted and registered.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); state.stack.pop(); markDone(state, lines); @@ -489,8 +508,7 @@ function processStatement( pushStep(state, { descriptionHtml: `Call Stack executes ${safeLabel}.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); state.stack.pop(); markDone(state, lines); @@ -516,8 +534,7 @@ function processStatement( pushStep(state, { descriptionHtml: `Call Stack executes ${safeLabel}.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); state.stack.pop(); markDone(state, lines); @@ -533,8 +550,7 @@ function processStatement( pushStep(state, { descriptionHtml: `Call Stack executes ${safeShortLabel}.`, activeLine: line, - loopActive: isInsideCallback, - loopLabel: isInsideCallback ? "running" : "idle", + ...syncLoopState(isInsideCallback), }); state.stack.pop(); markDone(state, lines); @@ -590,7 +606,7 @@ function drainQueues(state: SimState, source: string): SandboxError | null { }); state.stack.pop(); - state.doneLines = state.doneLines.filter((l) => !micro.bodyLines.includes(l)); + markPending(state, micro.bodyLines); for (const stmt of micro.bodyStatements) { const err = processStatement(stmt, source, state, true, micro.scope); @@ -639,7 +655,7 @@ function drainQueues(state: SimState, source: string): SandboxError | null { }); state.stack.pop(); - state.doneLines = state.doneLines.filter((l) => !task.bodyLines.includes(l)); + markPending(state, task.bodyLines); for (const stmt of task.bodyStatements) { const err = processStatement(stmt, source, state, true, task.scope); @@ -680,7 +696,7 @@ export function generateEventLoopSteps( taskQueue: [], microtaskQueue: [], consoleOutput: [], - doneLines: [], + doneLines: new Set(), steps: [], functions: new Map(), }; @@ -699,12 +715,16 @@ export function generateEventLoopSteps( const uncalledFunctions: string[] = []; for (const [name] of state.functions) { // Check if the function was actually called (appears in any step's stack) - const wasCalled = state.steps.some((s) => s.stack.some((item) => item === `${name}()`)); + const wasCalled = state.steps.some((s) => + s.stack.some((item) => item === `${name}()`), + ); if (!wasCalled) uncalledFunctions.push(name); } - const allLines = codeLines.map((l) => l.num); - state.doneLines = allLines; + markDone( + state, + codeLines.map((line) => line.num), + ); const outputList = state.consoleOutput.join(" → "); const safeOutputList = escapeHtml(outputList || "(no output)"); diff --git a/src/lib/sandbox/parser.test.ts b/src/lib/sandbox/parser.test.ts new file mode 100644 index 0000000..c71a38f --- /dev/null +++ b/src/lib/sandbox/parser.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { parseUserCode } from "@/lib/sandbox/parser"; + +describe("parseUserCode", () => { + it("returns a Program with locations for valid code", () => { + const result = parseUserCode( + "console.log('hi');\nsetTimeout(() => {}, 0);", + ); + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.ast.type).toBe("Program"); + expect(result.ast.body).toHaveLength(2); + expect(result.ast.body[1].loc?.start.line).toBe(2); + }); + + it("parses an empty program", () => { + const result = parseUserCode(""); + expect(result.success).toBe(true); + if (result.success) expect(result.ast.body).toEqual([]); + }); + + it("accepts modern syntax up to ES2023", () => { + const result = parseUserCode( + "const { a = 1, ...rest } = obj;\nconst x = a ?? rest?.b;\nclass C { #p = 1; static s = 2; }", + ); + expect(result.success).toBe(true); + }); + + it("rejects module syntax because the sandbox parses scripts", () => { + const result = parseUserCode("import x from 'y';"); + expect(result.success).toBe(false); + }); + + it("reports the line and column of a syntax error", () => { + const result = parseUserCode("console.log('a');\nconst = 5;"); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.line).toBe(2); + expect(result.error.column).toBe(6); + expect(result.error.message).toBe("Unexpected token"); + }); + + it("strips the (line:column) suffix from the message", () => { + const result = parseUserCode("foo("); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.message).not.toMatch(/\(\d+:\d+\)/); + expect(result.error.message.trim()).toBe(result.error.message); + }); +}); diff --git a/src/lib/socialImage.tsx b/src/lib/socialImage.tsx new file mode 100644 index 0000000..33c1d99 --- /dev/null +++ b/src/lib/socialImage.tsx @@ -0,0 +1,101 @@ +import { ImageResponse } from "next/og"; +import { + SITE_NAME, + SOCIAL_IMAGE_HEIGHT, + SOCIAL_IMAGE_WIDTH, +} from "@/lib/constants"; + +export const SOCIAL_IMAGE_SIZE = { + width: SOCIAL_IMAGE_WIDTH, + height: SOCIAL_IMAGE_HEIGHT, +}; + +interface SocialImageOptions { + /** Small uppercase line above the title. */ + kicker: string; + title: string; + subtitle: string; + /** RGB triplet, e.g. "247 223 30", used for the kicker and the glow. */ + accentRgb?: string; +} + +const rgb = (triplet: string, alpha = 1) => + `rgba(${triplet.split(" ").join(", ")}, ${alpha})`; + +/** + * One layout for every Open Graph image on the site: the home page, each + * category, and each topic. Satori (next/og) only supports flexbox, so every + * box declares display: flex. + */ +export const renderSocialImage = ({ + kicker, + title, + subtitle, + accentRgb = "103 232 249", +}: SocialImageOptions) => + new ImageResponse( +
    +
    + {kicker} +
    +
    +
    24 ? 64 : 84, + fontWeight: 800, + lineHeight: 1.05, + maxWidth: 1040, + }} + > + {title} +
    +
    + {subtitle} +
    +
    +
    + {SITE_NAME} + visualizejs.com +
    +
    , + SOCIAL_IMAGE_SIZE, + ); diff --git a/src/lib/topics.test.ts b/src/lib/topics.test.ts new file mode 100644 index 0000000..49c26b2 --- /dev/null +++ b/src/lib/topics.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, it } from "vitest"; +import { CATEGORIES, CATEGORY_ORDER } from "@/lib/categories"; +import { + getAdjacentTopics, + getTopicById, + getTopicsByCategory, + groupTopics, + isTopicId, + topics, +} from "@/lib/topics"; +import type { Topic } from "@/types"; + +const makeTopic = (id: string, group?: string): Topic => ({ + id, + title: id, + category: "frameworks", + description: `${id} description.`, + difficulty: "beginner", + docsUrl: "https://example.com", + route: `/frameworks/${id}`, + ...(group ? { group } : {}), +}); + +describe("topic registry", () => { + it("has at least one topic", () => { + expect(topics.length).toBeGreaterThan(0); + }); + + it("uses unique kebab-case ids", () => { + const ids = topics.map((topic) => topic.id); + expect(new Set(ids).size).toBe(ids.length); + for (const id of ids) { + expect(id).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/); + } + }); + + it("derives every route from the category and id", () => { + for (const topic of topics) { + expect(topic.route).toBe(`/${topic.category}/${topic.id}`); + } + }); + + it("only uses registered categories, and every category has topics", () => { + for (const topic of topics) { + expect(CATEGORY_ORDER).toContain(topic.category); + } + for (const category of CATEGORY_ORDER) { + expect(getTopicsByCategory(category).length).toBeGreaterThan(0); + } + }); + + it("gives every topic a title, description, difficulty, and docs link", () => { + for (const topic of topics) { + expect(topic.title.trim().length).toBeGreaterThan(0); + expect(topic.description.trim().length).toBeGreaterThan(40); + expect(topic.description.trim().endsWith(".")).toBe(true); + expect(["beginner", "intermediate", "advanced"]).toContain( + topic.difficulty, + ); + expect(() => new URL(topic.docsUrl)).not.toThrow(); + expect(topic.docsUrl.startsWith("https://")).toBe(true); + } + }); + + it("keeps user-facing copy free of em dashes and emojis", () => { + for (const topic of topics) { + const copy = `${topic.title} ${topic.description} ${topic.group ?? ""}`; + expect(copy).not.toContain("—"); + expect(copy).not.toMatch(/\p{Extended_Pictographic}/u); + } + }); + + it("only sets toolbar and group to known values", () => { + for (const topic of topics) { + if (topic.toolbar !== undefined) { + expect(["selector", "simple"]).toContain(topic.toolbar); + } + if (topic.group !== undefined) { + expect(topic.group.trim().length).toBeGreaterThan(0); + } + } + }); +}); + +describe("isTopicId", () => { + it("accepts registered ids and rejects everything else", () => { + expect(isTopicId("event-loop")).toBe(true); + expect(isTopicId("closures")).toBe(true); + expect(isTopicId("Event-Loop")).toBe(false); + expect(isTopicId("")).toBe(false); + expect(isTopicId("not-a-topic")).toBe(false); + }); +}); + +describe("getTopicById", () => { + it("returns the registry entry with its derived route", () => { + const topic = getTopicById("closures"); + expect(topic?.category).toBe("javascript"); + expect(topic?.route).toBe("/javascript/closures"); + }); + + it("returns undefined for unknown ids", () => { + expect(getTopicById("missing")).toBeUndefined(); + }); +}); + +describe("getTopicsByCategory", () => { + it("preserves registry order within a category", () => { + for (const category of CATEGORY_ORDER) { + const expected = topics.filter((topic) => topic.category === category); + expect(getTopicsByCategory(category)).toEqual(expected); + } + }); + + it("covers every topic exactly once across all categories", () => { + const seen = CATEGORY_ORDER.flatMap((category) => + getTopicsByCategory(category).map((topic) => topic.id), + ); + expect(seen.sort()).toEqual(topics.map((topic) => topic.id).sort()); + }); +}); + +describe("groupTopics", () => { + it("returns a single null-labelled group when nothing is grouped", () => { + const list = [makeTopic("a"), makeTopic("b")]; + expect(groupTopics(list)).toEqual([{ label: null, topics: list }]); + }); + + it("returns an empty list for no topics", () => { + expect(groupTopics([])).toEqual([]); + }); + + it("puts ungrouped topics first, then groups in first-appearance order", () => { + const vue1 = makeTopic("vue-1", "Vue"); + const svelte = makeTopic("svelte-1", "Svelte"); + const flat = makeTopic("flat"); + const vue2 = makeTopic("vue-2", "Vue"); + + expect(groupTopics([vue1, svelte, flat, vue2])).toEqual([ + { label: null, topics: [flat] }, + { label: "Vue", topics: [vue1, vue2] }, + { label: "Svelte", topics: [svelte] }, + ]); + }); + + it("groups the frameworks category by framework", () => { + const groups = groupTopics(getTopicsByCategory("frameworks")); + const labels = groups.map((group) => group.label); + expect(labels).toEqual(["Vue", "Svelte", "Angular"]); + for (const group of groups) { + expect(group.topics.length).toBeGreaterThan(0); + } + }); +}); + +describe("getAdjacentTopics", () => { + it("has no previous topic for the first topic in a category", () => { + const [first, second] = getTopicsByCategory("javascript"); + expect(getAdjacentTopics(first.id)).toEqual({ + previous: null, + next: second, + }); + }); + + it("has no next topic for the last topic in a category", () => { + const list = getTopicsByCategory("ai"); + const last = list[list.length - 1]; + expect(getAdjacentTopics(last.id)).toEqual({ + previous: list[list.length - 2], + next: null, + }); + }); + + it("returns both neighbours for a topic in the middle", () => { + const list = getTopicsByCategory("react"); + const { previous, next } = getAdjacentTopics(list[1].id); + expect(previous).toBe(list[0]); + expect(next).toBe(list[2]); + }); + + it("never crosses into another category", () => { + for (const topic of topics) { + const { previous, next } = getAdjacentTopics(topic.id); + if (previous) expect(previous.category).toBe(topic.category); + if (next) expect(next.category).toBe(topic.category); + } + }); + + it("returns nulls for an unknown topic", () => { + expect(getAdjacentTopics("missing")).toEqual({ + previous: null, + next: null, + }); + }); + + it("walks a whole category by following next links", () => { + for (const category of CATEGORY_ORDER) { + const list = getTopicsByCategory(category); + const walked: string[] = []; + let current: Topic | null = list[0]; + while (current) { + walked.push(current.id); + current = getAdjacentTopics(current.id).next; + } + expect(walked).toEqual(list.map((topic) => topic.id)); + expect(CATEGORIES[category]).toBeDefined(); + } + }); +}); diff --git a/src/lib/topics.ts b/src/lib/topics.ts index f368352..44fd012 100644 --- a/src/lib/topics.ts +++ b/src/lib/topics.ts @@ -1,33 +1,38 @@ -import type { Topic } from "@/types"; +import type { Category, Topic, TopicDefinition } from "@/types"; -export const topics: Topic[] = [ +/** + * Every topic on the site, in the order it appears within its category. + * `route` is derived from category and id so the two can never disagree. + * TopicId is derived from this list, which lets the theory, keyword, and + * component registries require an entry for every topic at compile time. + */ +const TOPIC_DEFINITIONS = [ /* ── Ordered by importance / popularity ── */ { id: "event-loop", + toolbar: "simple", title: "Event Loop", category: "javascript", - route: "/javascript/event-loop", description: "See how the JavaScript event loop works with an interactive visualization. Watch the call stack, microtask queue, and callback queue process setTimeout, Promises, and async/await step by step.", difficulty: "intermediate", - docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop", + docsUrl: + "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop", }, { id: "hoisting", title: "Hoisting", category: "javascript", - route: "/javascript/hoisting", description: "See how JavaScript hoisting works for var, let, const, and function declarations. Interactive visualization of the creation phase vs execution phase and the temporal dead zone.", difficulty: "beginner", - docsUrl: - "https://developer.mozilla.org/en-US/docs/Glossary/Hoisting", + docsUrl: "https://developer.mozilla.org/en-US/docs/Glossary/Hoisting", }, { id: "execution-context", + toolbar: "simple", title: "Execution Context", category: "javascript", - route: "/javascript/execution-context", description: "See how JavaScript execution contexts work with an interactive visualization. Step through the global and function context stack, creation phase, scope chain, and variable environments.", difficulty: "intermediate", @@ -36,20 +41,18 @@ export const topics: Topic[] = [ }, { id: "closures", + toolbar: "simple", title: "Closures", category: "javascript", - route: "/javascript/closures", description: "See how JavaScript closures work with an interactive visualization. Watch inner functions capture outer scope variables, understand lexical scoping, and learn closure patterns for interviews.", difficulty: "intermediate", - docsUrl: - "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures", + docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures", }, { id: "promises", title: "Promises", category: "javascript", - route: "/javascript/promises", description: "See how JavaScript Promises work with an interactive visualization. Step through pending, fulfilled, and rejected states, promise chaining, microtask scheduling, and async/await flow.", difficulty: "intermediate", @@ -60,7 +63,6 @@ export const topics: Topic[] = [ id: "this-keyword", title: "this Keyword", category: "javascript", - route: "/javascript/this-keyword", description: "See how the JavaScript this keyword works with an interactive visualization. Step through implicit, explicit, new, and arrow function binding rules with call, apply, and bind examples.", difficulty: "intermediate", @@ -71,18 +73,15 @@ export const topics: Topic[] = [ id: "scope-chain", title: "Scope Chain", category: "javascript", - route: "/javascript/scope-chain", description: "See how the JavaScript scope chain works with an interactive visualization. Trace variable lookups across global, function, and block scopes from inner to outer step by step.", difficulty: "intermediate", - docsUrl: - "https://developer.mozilla.org/en-US/docs/Glossary/Scope", + docsUrl: "https://developer.mozilla.org/en-US/docs/Glossary/Scope", }, { id: "type-coercion", title: "Type Coercion", category: "javascript", - route: "/javascript/type-coercion", description: "See how JavaScript type coercion works with an interactive visualization. Compare == vs === behavior, explore truthy/falsy values, and see edge cases with NaN, null, and undefined.", difficulty: "beginner", @@ -93,7 +92,6 @@ export const topics: Topic[] = [ id: "destructuring", title: "Destructuring", category: "javascript", - route: "/javascript/destructuring", description: "See how JavaScript destructuring works with an interactive visualization. Unpack arrays by position, extract object properties by name, use default values, nested patterns, and parameter destructuring.", difficulty: "beginner", @@ -104,7 +102,6 @@ export const topics: Topic[] = [ id: "spread-rest", title: "Spread & Rest", category: "javascript", - route: "/javascript/spread-rest", description: "See how JavaScript spread and rest syntax works with an interactive visualization. Watch spread expand arrays/objects, rest collect arguments, and rest in destructuring patterns.", difficulty: "beginner", @@ -115,7 +112,6 @@ export const topics: Topic[] = [ id: "prototypal-inheritance", title: "Prototypal Inheritance", category: "javascript", - route: "/javascript/prototypal-inheritance", description: "See how JavaScript prototypal inheritance works with an interactive visualization. Trace prototype chain lookups, __proto__ delegation, Object.create, and the instanceof operator step by step.", difficulty: "advanced", @@ -126,7 +122,6 @@ export const topics: Topic[] = [ id: "reference-value", title: "Reference vs Value", category: "javascript", - route: "/javascript/reference-value", description: "See how pass by value vs pass by reference works in JavaScript with an interactive visualization. Watch primitives copy, objects share references, and compare shallow copy, deep copy, and structuredClone.", difficulty: "beginner", @@ -137,7 +132,6 @@ export const topics: Topic[] = [ id: "heap-stack", title: "Heap & Stack", category: "javascript", - route: "/javascript/heap-stack", description: "See how the JavaScript memory model works with an interactive visualization. Watch primitives live on the stack, objects get allocated on the heap, and call frames get created and destroyed.", difficulty: "advanced", @@ -148,7 +142,6 @@ export const topics: Topic[] = [ id: "garbage-collection", title: "Garbage Collection", category: "javascript", - route: "/javascript/garbage-collection", description: "See how JavaScript garbage collection works with an interactive visualization. Watch the mark-and-sweep algorithm, explore memory leak patterns with closures, timers, and DOM nodes, plus WeakRef.", difficulty: "advanced", @@ -159,7 +152,6 @@ export const topics: Topic[] = [ id: "generators", title: "Generators & Iterators", category: "javascript", - route: "/javascript/generators", description: "See how JavaScript generators work with an interactive visualization. Watch generator functions pause with yield and resume with .next(), step through the iterator protocol and two-way data flow.", difficulty: "advanced", @@ -170,7 +162,6 @@ export const topics: Topic[] = [ id: "event-delegation", title: "Event Delegation", category: "javascript", - route: "/javascript/event-delegation", description: "See how DOM events propagate through the tree with an interactive visualization. Watch capture and bubble phases, stopPropagation, and the delegation pattern where one parent handler replaces many child handlers.", difficulty: "beginner", @@ -181,19 +172,36 @@ export const topics: Topic[] = [ id: "modules-imports", title: "Modules & Imports", category: "javascript", - route: "/javascript/modules-imports", description: "See how JavaScript ES modules work with an interactive visualization. Watch the module graph build, import bindings resolve, live references update, and circular dependencies get handled step by step.", difficulty: "intermediate", docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules", }, + { + id: "async-await", + title: "Async/Await", + category: "javascript", + description: + "See how async/await works with an interactive visualization. Watch an async function pause at await, hand control back to the caller, resume from the microtask queue, and compare sequential awaits with Promise.all.", + difficulty: "intermediate", + docsUrl: + "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function", + }, + { + id: "debounce-throttle", + title: "Debounce & Throttle", + category: "javascript", + description: + "See how debounce and throttle control event handlers with an interactive visualization. Watch a burst of input events on a timeline, see debounce wait for silence, throttle fire at a fixed rate, and the timers that make it work.", + difficulty: "beginner", + docsUrl: "https://developer.mozilla.org/en-US/docs/Glossary/Debounce", + }, /* ── React ── */ { id: "virtual-dom", title: "Virtual DOM", category: "react", - route: "/react/virtual-dom", description: "See how React's virtual DOM works with an interactive visualization. Watch JSX compile to React.createElement calls, build a virtual DOM object tree, and diff to find real DOM updates.", difficulty: "beginner", @@ -203,7 +211,6 @@ export const topics: Topic[] = [ id: "reconciliation", title: "Reconciliation", category: "react", - route: "/react/reconciliation", description: "See how React reconciliation works with an interactive visualization. Watch the virtual DOM diffing algorithm compare element trees: same-type updates, cross-type remounts, and key-based list matching.", difficulty: "intermediate", @@ -213,7 +220,6 @@ export const topics: Topic[] = [ id: "context-propagation", title: "Context Propagation", category: "react", - route: "/react/context-propagation", description: "See how React Context works with an interactive visualization. Watch provider value storage, consumer subscription, value propagation through the component tree, and why all consumers re-render.", difficulty: "intermediate", @@ -223,7 +229,6 @@ export const topics: Topic[] = [ id: "fiber-tree", title: "Fiber Tree", category: "react", - route: "/react/fiber-tree", description: "See how React's fiber tree works with an interactive visualization. Watch beginWork go down and completeWork go up as the work loop processes each fiber as a unit of work.", difficulty: "advanced", @@ -233,7 +238,6 @@ export const topics: Topic[] = [ id: "hooks", title: "Hooks", category: "react", - route: "/react/hooks", description: "See how React hooks work with an interactive visualization. Watch hooks stored as a linked list on each fiber, understand why call order matters, and what breaks with conditional hooks.", difficulty: "intermediate", @@ -243,7 +247,6 @@ export const topics: Topic[] = [ id: "render-cycle", title: "Render Cycle", category: "react", - route: "/react/render-cycle", description: "See how React's render cycle works with an interactive visualization. Step through the render phase (pure, interruptible diffing) and the commit phase (synchronous DOM mutations, effects, paint).", difficulty: "advanced", @@ -253,7 +256,6 @@ export const topics: Topic[] = [ id: "memoization", title: "Memoization", category: "react", - route: "/react/memoization", description: "See how React.memo, useMemo, and useCallback prevent unnecessary work with an interactive visualization. Step through dependency checks, cache hits and misses, and which components skip re-renders.", difficulty: "intermediate", @@ -263,7 +265,6 @@ export const topics: Topic[] = [ id: "suspense", title: "Suspense", category: "react", - route: "/react/suspense", description: "See how React Suspense handles async boundaries with an interactive visualization. Step through fallback rendering, promise throwing, and content revealing to understand loading states.", difficulty: "intermediate", @@ -273,7 +274,6 @@ export const topics: Topic[] = [ id: "server-components", title: "Server Components", category: "react", - route: "/react/server-components", description: "See how React Server Components split rendering between server and client with an interactive visualization. Step through the server render pass, payload serialization, and client hydration.", difficulty: "advanced", @@ -283,7 +283,6 @@ export const topics: Topic[] = [ id: "error-boundaries", title: "Error Boundaries", category: "react", - route: "/react/error-boundaries", description: "See how React error boundaries work with an interactive visualization. Watch errors propagate up the component tree, getDerivedStateFromError trigger, fallback UI render, and recovery patterns step by step.", difficulty: "intermediate", @@ -294,26 +293,209 @@ export const topics: Topic[] = [ id: "use-effect-lifecycle", title: "useEffect Lifecycle", category: "react", - route: "/react/use-effect-lifecycle", description: "See how React useEffect works with an interactive visualization. Step through the render-commit-paint-effect timeline, dependency array comparisons, cleanup function timing, and common patterns.", difficulty: "beginner", docsUrl: "https://react.dev/reference/react/useEffect", }, -]; + { + id: "state-batching", + title: "State Batching", + category: "react", + description: + "See how React batches state updates with an interactive visualization. Watch multiple setState calls queue up, updater functions run in order against the pending value, and one render commit for all of them, including inside timeouts and promises.", + difficulty: "intermediate", + docsUrl: "https://react.dev/learn/queueing-a-series-of-state-updates", + }, + { + id: "concurrent-rendering", + title: "Concurrent Rendering", + category: "react", + description: + "See how React concurrent rendering works with an interactive visualization. Watch a transition render at low priority, an urgent input interrupt it, React discard the stale work and restart, and the final commit land without blocking typing.", + difficulty: "advanced", + docsUrl: "https://react.dev/reference/react/useTransition", + }, + /* ── Frameworks ── */ + { + id: "vue-reactivity", + group: "Vue", + title: "Vue Reactivity", + category: "frameworks", + description: + "See how Vue reactivity works with an interactive visualization. Watch a Proxy intercept property reads to track dependencies, writes trigger the effects that depend on them, and computed values cache until a dependency changes.", + difficulty: "intermediate", + docsUrl: "https://vuejs.org/guide/extras/reactivity-in-depth.html", + }, + { + id: "svelte-runes", + group: "Svelte", + title: "Svelte Runes", + category: "frameworks", + description: + "See how Svelte 5 runes work with an interactive visualization. Watch $state create a signal, $derived build a dependency graph, $effect subscribe, and the compiler turn a component into surgical DOM updates with no virtual DOM.", + difficulty: "intermediate", + docsUrl: "https://svelte.dev/docs/svelte/what-are-runes", + }, + { + id: "angular-change-detection", + group: "Angular", + title: "Angular Change Detection", + category: "frameworks", + description: + "See how Angular change detection works with an interactive visualization. Compare zone.js triggering a full tree check, OnPush skipping unchanged subtrees, and signals marking only the exact components that read a changed value.", + difficulty: "advanced", + docsUrl: "https://angular.dev/guide/signals", + }, + /* ── Backend ── */ + { + id: "http-request-lifecycle", + title: "HTTP Request Lifecycle", + category: "backend", + description: + "See what happens when a browser makes an HTTP request with an interactive visualization. Follow DNS resolution, the TCP and TLS handshakes, the request hitting a server route, middleware, the database, and the response coming back.", + difficulty: "beginner", + docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview", + }, + { + id: "database-indexing", + title: "Database Indexing", + category: "backend", + description: + "See how database indexes speed up queries with an interactive visualization. Compare a full table scan against a B-tree lookup, watch the tree descend to a leaf, and see why writes get slower as indexes pile up.", + difficulty: "intermediate", + docsUrl: "https://use-the-index-luke.com/sql/anatomy", + }, + { + id: "caching-strategies", + title: "Caching Strategies", + category: "backend", + description: + "See how caching works with an interactive visualization. Watch cache-aside hits and misses, write-through updates, TTL expiry, LRU eviction, and the stale data problem that makes invalidation hard.", + difficulty: "intermediate", + docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching", + }, + { + id: "jwt-authentication", + title: "JWT Authentication", + category: "backend", + description: + "See how JWT authentication works with an interactive visualization. Watch login produce a signed token, the header, payload, and signature get encoded, a protected route verify the signature, and an expired or tampered token get rejected.", + difficulty: "intermediate", + docsUrl: "https://datatracker.ietf.org/doc/html/rfc7519", + }, + { + id: "rate-limiting", + title: "Rate Limiting", + category: "backend", + description: + "See how rate limiting protects an API with an interactive visualization. Watch a token bucket refill and drain, a fixed window reset at the boundary, a sliding window smooth out bursts, and requests get accepted or rejected with 429.", + difficulty: "intermediate", + docsUrl: + "https://cloud.google.com/architecture/rate-limiting-strategies-techniques", + }, + /* ── AI ── */ + { + id: "tokenization", + title: "Tokenization", + category: "ai", + description: + "See how a language model turns text into tokens with an interactive visualization. Watch byte pair encoding merge characters into subwords, map tokens to ids, and see why token counts differ from word counts.", + difficulty: "beginner", + docsUrl: "https://huggingface.co/learn/llm-course/chapter2/4", + }, + { + id: "embeddings", + title: "Embeddings", + category: "ai", + description: + "See how embeddings turn tokens into vectors with an interactive visualization. Watch a lookup table map token ids to numbers, compare vectors with cosine similarity, and see how meaning becomes geometry.", + difficulty: "beginner", + docsUrl: + "https://developers.google.com/machine-learning/crash-course/embeddings", + }, + { + id: "attention", + title: "Attention", + category: "ai", + description: + "See how self-attention works with an interactive visualization. Watch queries score keys, softmax turn scores into weights, the causal mask hide future tokens, and weighted values mix into a new representation.", + difficulty: "intermediate", + docsUrl: "https://arxiv.org/abs/1706.03762", + }, + { + id: "next-token-prediction", + title: "Next Token Prediction", + category: "ai", + description: + "See how a language model picks the next token with an interactive visualization. Watch logits become probabilities, compare greedy, temperature, top-k, and top-p sampling, and follow the autoregressive loop token by token.", + difficulty: "intermediate", + docsUrl: "https://huggingface.co/blog/how-to-generate", + }, + { + id: "backpropagation", + title: "Backpropagation", + category: "ai", + description: + "See how neural networks learn with an interactive visualization. Step through a forward pass, the loss, gradients flowing backward through the chain rule, and a gradient descent weight update on a tiny network.", + difficulty: "advanced", + docsUrl: "https://cs231n.github.io/optimization-2/", + }, +] as const satisfies readonly TopicDefinition[]; -export function getTopicsByCategory(category: "javascript" | "react") { - return topics.filter((topic) => topic.category === category); -} +export type TopicId = (typeof TOPIC_DEFINITIONS)[number]["id"]; + +export const topics: readonly Topic[] = TOPIC_DEFINITIONS.map((definition) => ({ + ...definition, + route: `/${definition.category}/${definition.id}`, +})); + +export const isTopicId = (value: string): value is TopicId => + topics.some((topic) => topic.id === value); -export function getTopicById(id: string) { - return topics.find((topic) => topic.id === id); +export const getTopicsByCategory = (category: Category): Topic[] => + topics.filter((topic) => topic.category === category); + +export interface TopicGroup { + /** Group label, or null for topics that have no group. */ + label: string | null; + topics: Topic[]; } -export function getTopicOrThrow(id: string): Topic { - const topic = getTopicById(id); +/** + * Topics split by their `group`, in first-appearance order. Ungrouped topics + * come first under a null label so a category without groups yields a single + * entry and callers can render the flat case and the grouped case alike. + */ +export const groupTopics = (list: readonly Topic[]): TopicGroup[] => { + const groups = new Map(); + for (const topic of list) { + const key = topic.group ?? null; + const bucket = groups.get(key); + if (bucket) bucket.push(topic); + else groups.set(key, [topic]); + } + return [...groups.entries()] + .sort(([a], [b]) => (a === null ? -1 : b === null ? 1 : 0)) + .map(([label, topics]) => ({ label, topics })); +}; + +export const getTopicById = (id: string): Topic | undefined => + topics.find((topic) => topic.id === id); + +/** Previous and next topics within the same category, in registry order. */ +export const getAdjacentTopics = ( + topicId: string, +): { previous: Topic | null; next: Topic | null } => { + const topic = getTopicById(topicId); if (!topic) { - throw new Error(`Missing topic configuration for ${id}`); + return { previous: null, next: null }; } - return topic; -} + const siblings = getTopicsByCategory(topic.category); + const index = siblings.findIndex((entry) => entry.id === topicId); + return { + previous: index > 0 ? siblings[index - 1] : null, + next: + index >= 0 && index < siblings.length - 1 ? siblings[index + 1] : null, + }; +}; diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts new file mode 100644 index 0000000..e0a3873 --- /dev/null +++ b/src/lib/utils.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { cn, noop } from "@/lib/utils"; + +describe("cn", () => { + it("joins class names and drops falsy values", () => { + expect(cn("a", false, undefined, null, "b", 0 && "c")).toBe("a b"); + }); + + it("supports arrays and object syntax", () => { + expect(cn(["a", { b: true, c: false }], { d: true })).toBe("a b d"); + }); + + it("lets later Tailwind utilities win over conflicting earlier ones", () => { + expect(cn("p-2 text-slate-400", "p-4")).toBe("text-slate-400 p-4"); + expect(cn("bg-red-500", "bg-blue-500")).toBe("bg-blue-500"); + }); + + it("keeps non-conflicting utilities", () => { + expect(cn("px-2", "py-4")).toBe("px-2 py-4"); + }); + + it("returns an empty string with no input", () => { + expect(cn()).toBe(""); + }); +}); + +describe("noop", () => { + it("returns undefined and ignores arguments", () => { + expect(noop()).toBeUndefined(); + }); +}); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 054ad44..c381713 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,8 +1,8 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } -export const noop = () => undefined +export const noop = () => undefined; diff --git a/src/lib/visualization-helpers.test.ts b/src/lib/visualization-helpers.test.ts new file mode 100644 index 0000000..01c6b22 --- /dev/null +++ b/src/lib/visualization-helpers.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { + chainHighlightClass, + chainLabelClass, + createKindBadgeClass, + createKindLabel, +} from "@/lib/visualization-helpers"; + +describe("createKindBadgeClass", () => { + it("looks up the class for a kind", () => { + const badge = createKindBadgeClass<"lookup" | "block">({ + lookup: "violet", + block: "cyan", + }); + expect(badge("lookup")).toBe("violet"); + expect(badge("block")).toBe("cyan"); + }); +}); + +describe("createKindLabel", () => { + it("looks up the label for a kind", () => { + const label = createKindLabel<"a" | "b">({ a: "Alpha", b: "Beta" }); + expect(label("b")).toBe("Beta"); + }); +}); + +describe("chain highlight helpers", () => { + it("returns a distinct class per highlight state", () => { + const states = ["active", "searching", "found", "none", undefined] as const; + const boxes = states.map((state) => chainHighlightClass(state)); + const labels = states.map((state) => chainLabelClass(state)); + expect(new Set(boxes.slice(0, 3)).size).toBe(3); + expect(new Set(labels.slice(0, 3)).size).toBe(3); + }); + + it("treats none and undefined the same", () => { + expect(chainHighlightClass("none")).toBe(chainHighlightClass(undefined)); + expect(chainLabelClass("none")).toBe(chainLabelClass(undefined)); + }); + + it("colours states consistently between box and label", () => { + expect(chainHighlightClass("active")).toContain("amber"); + expect(chainLabelClass("active")).toContain("amber"); + expect(chainHighlightClass("searching")).toContain("violet"); + expect(chainLabelClass("searching")).toContain("violet"); + expect(chainHighlightClass("found")).toContain("emerald"); + expect(chainLabelClass("found")).toContain("emerald"); + }); +}); diff --git a/src/lib/visualization/syntax.test.ts b/src/lib/visualization/syntax.test.ts new file mode 100644 index 0000000..fb301fe --- /dev/null +++ b/src/lib/visualization/syntax.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from "vitest"; +import { + TOKEN_CLASS_MAP, + tokenize, + type Token, +} from "@/lib/visualization/syntax"; + +const types = (text: string) => tokenize(text).map((token) => token.type); +const values = (text: string) => tokenize(text).map((token) => token.value); +const joined = (tokens: Token[]) => tokens.map((token) => token.value).join(""); + +describe("tokenize", () => { + it("round-trips the input text exactly", () => { + const samples = [ + "const fn = outer();", + " console.log(`Hi ${name}`); // greet", + "if (a >= 2 && b !== 'x') { return null; }", + "let total = 0.5 + 12;", + "", + " ", + "\t\tfoo($bar_1)", + ]; + for (const sample of samples) { + expect(joined(tokenize(sample))).toBe(sample); + } + }); + + it("returns no tokens for an empty string", () => { + expect(tokenize("")).toEqual([]); + }); + + it("classifies keywords, builtins, and builtin methods", () => { + expect(tokenize("const")).toEqual([{ type: "keyword", value: "const" }]); + expect(tokenize("console")).toEqual([ + { type: "builtin", value: "console" }, + ]); + expect(tokenize("log")).toEqual([{ type: "function", value: "log" }]); + expect(tokenize("Promise.then")).toEqual([ + { type: "builtin", value: "Promise" }, + { type: "punctuation", value: "." }, + { type: "function", value: "then" }, + ]); + }); + + it("treats unknown identifiers as plain text", () => { + expect(tokenize("myVariable_2$")).toEqual([ + { type: "plain", value: "myVariable_2$" }, + ]); + }); + + it("keeps whitespace runs as a single plain token", () => { + expect(tokenize("a b")).toEqual([ + { type: "plain", value: "a" }, + { type: "plain", value: " " }, + { type: "plain", value: "b" }, + ]); + }); + + it("tokenizes single, double, and backtick strings including escapes", () => { + expect(values("'it\\'s'")).toEqual(["'it\\'s'"]); + expect(types("'it\\'s'")).toEqual(["string"]); + expect(tokenize('"a" + `b`')).toEqual([ + { type: "string", value: '"a"' }, + { type: "plain", value: " " }, + { type: "punctuation", value: "+" }, + { type: "plain", value: " " }, + { type: "string", value: "`b`" }, + ]); + }); + + it("swallows the rest of the line for an unterminated string", () => { + expect(tokenize("'open string")).toEqual([ + { type: "string", value: "'open string" }, + ]); + }); + + it("treats everything after // as one comment token", () => { + expect(tokenize("x = 1; // set 'x' to 1")).toEqual([ + { type: "plain", value: "x" }, + { type: "plain", value: " " }, + { type: "punctuation", value: "=" }, + { type: "plain", value: " " }, + { type: "number", value: "1" }, + { type: "punctuation", value: ";" }, + { type: "plain", value: " " }, + { type: "comment", value: "// set 'x' to 1" }, + ]); + }); + + it("does not mistake a division for a comment", () => { + expect(types("a / b")).toEqual([ + "plain", + "plain", + "punctuation", + "plain", + "plain", + ]); + }); + + it("reads integers and decimals as numbers", () => { + expect(tokenize("42 3.14")).toEqual([ + { type: "number", value: "42" }, + { type: "plain", value: " " }, + { type: "number", value: "3.14" }, + ]); + }); + + it("marks the arrow as a keyword and other symbols as punctuation", () => { + expect(tokenize("() => {}")).toEqual([ + { type: "punctuation", value: "(" }, + { type: "punctuation", value: ")" }, + { type: "plain", value: " " }, + { type: "keyword", value: "=>" }, + { type: "plain", value: " " }, + { type: "punctuation", value: "{" }, + { type: "punctuation", value: "}" }, + ]); + expect(tokenize("a>=b")).toEqual([ + { type: "plain", value: "a" }, + { type: "punctuation", value: ">" }, + { type: "punctuation", value: "=" }, + { type: "plain", value: "b" }, + ]); + }); + + it("returns the cached token array for repeated lines", () => { + const first = tokenize("const cached = true;"); + const second = tokenize("const cached = true;"); + expect(second).toBe(first); + }); + + it("evicts the oldest entry once the cache is full", () => { + const line = "let evictMe = 1;"; + const original = tokenize(line); + for (let i = 0; i < 2000; i += 1) tokenize(`let filler${i} = ${i};`); + const again = tokenize(line); + expect(again).toEqual(original); + expect(again).not.toBe(original); + }); +}); + +describe("TOKEN_CLASS_MAP", () => { + it("gives every visible token type a class and plain text none", () => { + expect(TOKEN_CLASS_MAP.plain).toBe(""); + for (const [type, className] of Object.entries(TOKEN_CLASS_MAP)) { + if (type === "plain") continue; + expect(className).toMatch(/^tok-[a-z]+$/); + } + }); +}); diff --git a/src/lib/visualization/syntax.ts b/src/lib/visualization/syntax.ts index aaeca24..4e8be86 100644 --- a/src/lib/visualization/syntax.ts +++ b/src/lib/visualization/syntax.ts @@ -1,4 +1,3 @@ - export type TokenType = | "keyword" | "builtin" @@ -15,51 +14,154 @@ export interface Token { } const KEYWORDS = new Set([ - "let", "const", "var", "function", "return", "if", "else", "new", - "typeof", "instanceof", "class", "extends", "import", "export", - "default", "from", "async", "await", "yield", "for", "while", - "do", "switch", "case", "break", "continue", "try", "catch", - "finally", "throw", "true", "false", "null", "undefined", "this", - "void", "delete", "in", "of", + "let", + "const", + "var", + "function", + "return", + "if", + "else", + "new", + "typeof", + "instanceof", + "class", + "extends", + "import", + "export", + "default", + "from", + "async", + "await", + "yield", + "for", + "while", + "do", + "switch", + "case", + "break", + "continue", + "try", + "catch", + "finally", + "throw", + "true", + "false", + "null", + "undefined", + "this", + "void", + "delete", + "in", + "of", ]); const BUILTIN_OBJECTS = new Set([ - "console", "Promise", "Array", "Object", "String", "Number", - "Boolean", "Math", "JSON", "Date", "RegExp", "Map", "Set", - "WeakMap", "WeakSet", "WeakRef", "Symbol", "Error", "TypeError", - "ReferenceError", "SyntaxError", "RangeError", "window", - "document", "globalThis", + "console", + "Promise", + "Array", + "Object", + "String", + "Number", + "Boolean", + "Math", + "JSON", + "Date", + "RegExp", + "Map", + "Set", + "WeakMap", + "WeakSet", + "WeakRef", + "Symbol", + "Error", + "TypeError", + "ReferenceError", + "SyntaxError", + "RangeError", + "window", + "document", + "globalThis", ]); const BUILTIN_METHODS = new Set([ - "log", "warn", "error", "info", "debug", - "setTimeout", "setInterval", "clearTimeout", "clearInterval", - "resolve", "reject", "then", "catch", "finally", "all", "race", - "parse", "stringify", - "push", "pop", "shift", "unshift", "map", "filter", - "reduce", "forEach", "find", "includes", "indexOf", "slice", - "keys", "values", "entries", "assign", "freeze", - "require", "fetch", - "structuredClone", "isArray", - "deref", "fill", + "log", + "warn", + "error", + "info", + "debug", + "setTimeout", + "setInterval", + "clearTimeout", + "clearInterval", + "resolve", + "reject", + "then", + "catch", + "finally", + "all", + "race", + "parse", + "stringify", + "push", + "pop", + "shift", + "unshift", + "map", + "filter", + "reduce", + "forEach", + "find", + "includes", + "indexOf", + "slice", + "keys", + "values", + "entries", + "assign", + "freeze", + "require", + "fetch", + "structuredClone", + "isArray", + "deref", + "fill", ]); -export function tokenize(text: string): Token[] { +const isWhitespace = (code: number) => + code === 32 || code === 9 || code === 10 || code === 13; +const isDigit = (code: number) => code >= 48 && code <= 57; +const isIdentifierStart = (code: number) => + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122) || + code === 95 || + code === 36; +const isIdentifierPart = (code: number) => + isIdentifierStart(code) || isDigit(code); + +const classifyWord = (word: string): TokenType => { + if (KEYWORDS.has(word)) return "keyword"; + if (BUILTIN_OBJECTS.has(word)) return "builtin"; + if (BUILTIN_METHODS.has(word)) return "function"; + return "plain"; +}; + +const tokenizeUncached = (text: string): Token[] => { const tokens: Token[] = []; let i = 0; while (i < text.length) { - if (/\s/.test(text[i])) { + const code = text.charCodeAt(i); + + if (isWhitespace(code)) { const start = i; - while (i < text.length && /\s/.test(text[i])) i++; + while (i < text.length && isWhitespace(text.charCodeAt(i))) i++; tokens.push({ type: "plain", value: text.slice(start, i) }); continue; } if (text[i] === "/" && text[i + 1] === "/") { tokens.push({ type: "comment", value: text.slice(i) }); - i = text.length; - continue; + break; } if (text[i] === "'" || text[i] === '"' || text[i] === "`") { @@ -75,27 +177,23 @@ export function tokenize(text: string): Token[] { continue; } - if (/\d/.test(text[i])) { + if (isDigit(code)) { const start = i; - while (i < text.length && /[\d.]/.test(text[i])) i++; + while ( + i < text.length && + (isDigit(text.charCodeAt(i)) || text[i] === ".") + ) { + i++; + } tokens.push({ type: "number", value: text.slice(start, i) }); continue; } - if (/[a-zA-Z_$]/.test(text[i])) { + if (isIdentifierStart(code)) { const start = i; - while (i < text.length && /[a-zA-Z0-9_$]/.test(text[i])) i++; + while (i < text.length && isIdentifierPart(text.charCodeAt(i))) i++; const word = text.slice(start, i); - - if (KEYWORDS.has(word)) { - tokens.push({ type: "keyword", value: word }); - } else if (BUILTIN_OBJECTS.has(word)) { - tokens.push({ type: "builtin", value: word }); - } else if (BUILTIN_METHODS.has(word)) { - tokens.push({ type: "function", value: word }); - } else { - tokens.push({ type: "plain", value: word }); - } + tokens.push({ type: classifyWord(word), value: word }); continue; } @@ -110,7 +208,28 @@ export function tokenize(text: string): Token[] { } return tokens; -} +}; + +/** + * Source lines are static strings repeated across many renders, so the token + * arrays are cached by text. The cache is bounded so a sandbox session that + * keeps generating new lines cannot grow it without limit. + */ +const TOKEN_CACHE_LIMIT = 2000; +const tokenCache = new Map(); + +export const tokenize = (text: string): Token[] => { + const cached = tokenCache.get(text); + if (cached) return cached; + + const tokens = tokenizeUncached(text); + if (tokenCache.size >= TOKEN_CACHE_LIMIT) { + const oldest = tokenCache.keys().next().value; + if (oldest !== undefined) tokenCache.delete(oldest); + } + tokenCache.set(text, tokens); + return tokens; +}; export const TOKEN_CLASS_MAP: Record = { keyword: "tok-kw", diff --git a/src/lib/visualization/uiCopy.ts b/src/lib/visualization/uiCopy.ts index ac4dba0..194b35f 100644 --- a/src/lib/visualization/uiCopy.ts +++ b/src/lib/visualization/uiCopy.ts @@ -6,4 +6,5 @@ export const VISUALIZATION_PANEL_TITLES = { export const VISUALIZATION_EMPTY_STATES = { consoleOutput: "No output yet.", stepDescription: "Press Play or Step to begin.", + waiting: "waiting", } as const; diff --git a/src/test/conventions/topicContent.test.ts b/src/test/conventions/topicContent.test.ts new file mode 100644 index 0000000..48bfa9e --- /dev/null +++ b/src/test/conventions/topicContent.test.ts @@ -0,0 +1,226 @@ +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + auditExample, + flattenExamples, + type AuditExample, + type TopicDataModule, +} from "../../../scripts/lib/stepAudit"; +import { CATEGORIES } from "@/lib/categories"; +import { topics } from "@/lib/topics"; +import { VISUALIZATION_PANEL_TITLES } from "@/lib/visualization/uiCopy"; + +/** + * Guards the authoring conventions from CLAUDE.md across every topic: the + * folder shape, the client directive, step data shape, plain-language copy + * for AI topics, and the ban on em dashes and emojis in reader-facing text. + */ + +const VISUALIZATIONS_ROOT = resolve( + process.cwd(), + "src/components/visualizations", +); +const COMPONENTS_ROOT = resolve(process.cwd(), "src/components"); +const EM_DASH = "—"; +const EMOJI = /\p{Extended_Pictographic}/u; +const HARD_LINE_LIMIT = 300; + +const escapeRegExp = (text: string) => + text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +/** Matches any shared panel title written as a string literal. */ +const PANEL_TITLE_LITERAL = new RegExp( + `["'](${Object.values(VISUALIZATION_PANEL_TITLES).map(escapeRegExp).join("|")})["']`, +); + +/** Lines in a file, not counting the trailing newline Prettier adds. */ +const countLines = (content: string) => + content.split("\n").length - (content.endsWith("\n") ? 1 : 0); + +/** + * Topics whose examples are not driven by numbered source lines. They still + * carry steps with descriptions, but there is no code panel to audit. + */ +const NON_SOURCE_TOPICS = new Set(["hoisting"]); + +interface StepLike { + descriptionHtml: string; + simpleHtml?: string; + activeLine: number | null; + doneLines: number[]; +} + +const topicDirs = readdirSync(VISUALIZATIONS_ROOT).filter((entry) => + statSync(join(VISUALIZATIONS_ROOT, entry)).isDirectory(), +); + +const loadModule = async (topicId: string): Promise => { + const file = join(VISUALIZATIONS_ROOT, topicId, "data.ts"); + return (await import(/* @vite-ignore */ file)) as TopicDataModule; +}; + +const loadExamples = async (topicId: string): Promise => + flattenExamples(await loadModule(topicId)); + +/** Steps grouped by example id, whether or not the example has source lines. */ +const loadSteps = async ( + topicId: string, +): Promise<{ id: string; steps: StepLike[] }[]> => { + const mod = await loadModule(topicId); + if (mod.STEPS) return [{ id: "default", steps: mod.STEPS }]; + return (mod.EXAMPLES ?? []).flatMap((example) => { + if (example.variants) { + return example.variants.map((variant) => ({ + id: `${example.id}/${variant.methodId}`, + steps: variant.steps as StepLike[], + })); + } + const steps = (example as { steps?: StepLike[] }).steps; + return steps ? [{ id: example.id, steps }] : []; + }); +}; + +const listFiles = (dir: string, matcher: RegExp): string[] => + readdirSync(dir).flatMap((entry) => { + const full = join(dir, entry); + if (statSync(full).isDirectory()) return listFiles(full, matcher); + return matcher.test(entry) ? [full] : []; + }); + +describe("topic folders", () => { + it("exist for every registered topic with the required files", () => { + for (const topic of topics) { + const dir = join(VISUALIZATIONS_ROOT, topic.id); + for (const file of ["index.tsx", "data.ts", "types.ts"]) { + expect(existsSync(join(dir, file)), `${topic.id}/${file}`).toBe(true); + } + } + }); + + it("have no orphan folders without a registry entry", () => { + const ids = new Set(topics.map((topic) => topic.id)); + for (const dir of topicDirs) { + expect(ids.has(dir), `orphan folder ${dir}`).toBe(true); + } + }); + + it("declare the client directive and use named exports", () => { + for (const topic of topics) { + const source = readFileSync( + join(VISUALIZATIONS_ROOT, topic.id, "index.tsx"), + "utf8", + ); + expect(source.startsWith('"use client"'), topic.id).toBe(true); + expect(source, topic.id).not.toMatch(/export default/); + } + }); +}); + +describe("component file size", () => { + it(`keeps every component file at or under ${HARD_LINE_LIMIT} lines`, () => { + const oversized = listFiles(COMPONENTS_ROOT, /\.tsx$/) + .filter((file) => !file.endsWith(".test.tsx")) + .map((file) => ({ + file: file.slice(COMPONENTS_ROOT.length + 1), + lines: countLines(readFileSync(file, "utf8")), + })) + .filter((entry) => entry.lines > HARD_LINE_LIMIT); + expect(oversized).toEqual([]); + }); +}); + +describe("panel titles", () => { + it("come from uiCopy rather than string literals", () => { + const offenders = listFiles(VISUALIZATIONS_ROOT, /\.tsx?$/) + .filter((file) => !/\.test\.tsx?$/.test(file)) + .filter((file) => PANEL_TITLE_LITERAL.test(readFileSync(file, "utf8"))); + expect(offenders).toEqual([]); + }); +}); + +describe.each(topics.map((topic) => [topic.id, topic.category] as const))( + "step data for %s", + (topicId, category) => { + it("has at least one example with steps", async () => { + const groups = await loadSteps(topicId); + expect(groups.length).toBeGreaterThan(0); + const ids = groups.map((group) => group.id); + expect(new Set(ids).size).toBe(ids.length); + for (const group of groups) { + expect(group.steps.length, group.id).toBeGreaterThan(0); + } + }); + + it("either drives a source panel or is listed as a non-source topic", async () => { + const examples = await loadExamples(topicId); + expect(examples.length > 0).toBe(!NON_SOURCE_TOPICS.has(topicId)); + }); + + it("numbers code lines consecutively from 1", async () => { + const examples = await loadExamples(topicId); + const ids = examples.map((example) => example.id); + expect(new Set(ids).size).toBe(ids.length); + for (const example of examples) { + expect(example.steps.length, example.id).toBeGreaterThan(0); + expect(example.codeLines.length, example.id).toBeGreaterThan(0); + example.codeLines.forEach((line, index) => { + expect(line.num, `${example.id} line ${index}`).toBe(index + 1); + expect(typeof line.text).toBe("string"); + }); + } + }); + + it("shapes every source-driven step like BaseStep", async () => { + for (const example of await loadExamples(topicId)) { + example.steps.forEach((step: StepLike, index) => { + const tag = `${example.id} #${index + 1}`; + expect(typeof step.descriptionHtml, tag).toBe("string"); + expect(step.descriptionHtml.trim().length, tag).toBeGreaterThan(0); + expect( + step.activeLine === null || Number.isInteger(step.activeLine), + tag, + ).toBe(true); + expect(Array.isArray(step.doneLines), tag).toBe(true); + expect(new Set(step.doneLines).size, tag).toBe(step.doneLines.length); + for (const line of step.doneLines) { + expect(Number.isInteger(line), tag).toBe(true); + } + }); + } + }); + + it("passes the step audit without errors", async () => { + for (const example of await loadExamples(topicId)) { + expect(auditExample(example).errors).toEqual([]); + } + }); + + it("keeps step copy free of em dashes and emojis", async () => { + for (const example of await loadSteps(topicId)) { + example.steps.forEach((step, index) => { + const tag = `${example.id} #${index + 1}`; + const copy = JSON.stringify(step); + expect(copy, tag).not.toContain(EM_DASH); + expect(copy, tag).not.toMatch(EMOJI); + }); + } + }); + + if (category === "ai") { + it("carries a plain-language simpleHtml on every step", async () => { + for (const example of await loadSteps(topicId)) { + example.steps.forEach((step, index) => { + const tag = `${example.id} #${index + 1}`; + expect(typeof step.simpleHtml, tag).toBe("string"); + expect(step.simpleHtml?.trim().length, tag).toBeGreaterThan(0); + }); + } + }); + } + + it("belongs to a registered category", () => { + expect(CATEGORIES[category]).toBeDefined(); + }); + }, +); diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..188f475 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,47 @@ +import "@testing-library/jest-dom/vitest"; +import { cleanup } from "@testing-library/react"; +import { afterEach } from "vitest"; + +// Recent Node versions define an experimental global localStorage getter. +// Vitest leaves globals it finds untouched, so the jsdom Storage never reaches +// window.localStorage. Bridge the real jsdom storages onto the test global +// without reading the Node getter, which logs an ExperimentalWarning. +const dom = (globalThis as { jsdom?: { window: Window } }).jsdom; +if (dom) { + for (const key of ["localStorage", "sessionStorage"] as const) { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, key); + if (!descriptor || descriptor.get) { + Object.defineProperty(globalThis, key, { + value: dom.window[key], + configurable: true, + writable: true, + }); + } + } +} + +// jsdom has no matchMedia. Default to a query that never matches, which is +// what a touch device looks like; tests that need hover stub it themselves. +if (typeof window.matchMedia !== "function") { + Object.defineProperty(window, "matchMedia", { + configurable: true, + writable: true, + value: (query: string): MediaQueryList => ({ + matches: false, + media: query, + onchange: null, + addEventListener: () => undefined, + removeEventListener: () => undefined, + addListener: () => undefined, + removeListener: () => undefined, + dispatchEvent: () => false, + }), + }); +} + +// Vitest only auto-cleans React Testing Library when globals are on. Cleaning +// up here keeps every test's DOM and storage isolated from the last one. +afterEach(() => { + cleanup(); + window.localStorage.clear(); +}); diff --git a/src/types/index.ts b/src/types/index.ts index f07f00b..a14ccdb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,13 +1,29 @@ -export type Category = "javascript" | "react"; +export type Category = "javascript" | "react" | "frameworks" | "backend" | "ai"; export type Difficulty = "beginner" | "intermediate" | "advanced"; -export interface Topic { +/** Which skeleton the page shell shows while the toolbar portal mounts. */ +export type ToolbarVariant = "selector" | "simple"; + +/** A topic as authored in the registry. */ +export interface TopicDefinition { id: string; title: string; category: Category; - route: string; description: string; difficulty: Difficulty; docsUrl: string; + /** Defaults to "selector"; topics without an example picker use "simple". */ + toolbar?: ToolbarVariant; + /** + * Sub-heading the topic is listed under on its category page and in the + * sidebar, for example "Vue" inside Frameworks. Topics without a group + * are listed flat. + */ + group?: string; +} + +/** A topic with its derived route. */ +export interface Topic extends TopicDefinition { + route: string; } diff --git a/src/types/visualization.ts b/src/types/visualization.ts index b896cfd..423d700 100644 --- a/src/types/visualization.ts +++ b/src/types/visualization.ts @@ -8,6 +8,35 @@ export interface SourceLine { text: string; } +/** Fields every step in a source-driven visualization carries. */ +export interface BaseStep { + /** Trusted HTML authored in the topic's data file, shown in the step pill. */ + descriptionHtml: string; + /** + * Optional plain-language version of descriptionHtml for readers who are + * new to the subject. When any step in a topic sets it, the toolbar offers + * a Simple / Detailed toggle. + */ + simpleHtml?: string; + activeLine: number | null; + doneLines: number[]; +} + +/** An entry in the example selector. */ +export interface ExampleOption { + id: string; + title: string; + description: string; +} + +/** A selectable example: source lines plus the steps that walk through them. */ +export interface SourceExample< + TStep extends BaseStep = BaseStep, +> extends ExampleOption { + codeLines: SourceLine[]; + steps: TStep[]; +} + /** * Highlight state used by scope-chain-style visualizations * (ScopeChain, PrototypalInheritance). diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..5a8be43 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,35 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, + test: { + environment: "jsdom", + setupFiles: ["./src/test/setup.ts"], + include: ["src/**/*.test.{ts,tsx}", "scripts/**/*.test.ts"], + restoreMocks: true, + clearMocks: true, + unstubGlobals: true, + coverage: { + provider: "v8", + include: [ + "src/lib/**", + "src/hooks/**", + "src/components/**", + "scripts/lib/**", + ], + // Topic visualizations and shadcn primitives are visual; the shared + // logic they build on is what the suite covers. + exclude: [ + "src/**/*.test.{ts,tsx}", + "src/test/**", + "src/components/visualizations/**", + "src/components/ui/**", + ], + }, + }, +});