diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1adf7c1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI +on: + push: + branches: [main] + pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +permissions: + contents: read +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24.21.0 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm check + - run: pnpm test + - run: pnpm build + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: browser-results + path: test-results/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..046b8e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +node_modules +dist +.void +.wrangler +.void-wrangler.jsonc +.env +.env.* +!.env.example +*.tsbuildinfo +.DS_Store +playwright-report/ +test-results/ +.fence-fixtures-*/ + +.impeccable/questions/ +.keepup/map.html diff --git a/.impeccable/design.json b/.impeccable/design.json new file mode 100644 index 0000000..72eb5c5 --- /dev/null +++ b/.impeccable/design.json @@ -0,0 +1,204 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-10-22T05:37:28.158120+00:00", + "title": "Design System: Strokes", + "extensions": { + "colorMeta": { + "primary": { + "role": "primary", + "displayName": "Primary", + "canonical": "#2348d1", + "tonalRamp": [ + "oklch(15% 0.2 265)", + "oklch(27% 0.2 265)", + "oklch(39% 0.2 265)", + "oklch(51% 0.2 265)", + "oklch(63% 0.2 265)", + "oklch(75% 0.2 265)", + "oklch(87% 0.2 265)", + "oklch(95% 0.2 265)" + ] + }, + "primary-deep": { + "role": "primary", + "displayName": "Primary Deep", + "canonical": "#17349e", + "tonalRamp": [ + "oklch(15% 0.18 265)", + "oklch(27% 0.18 265)", + "oklch(39% 0.18 265)", + "oklch(51% 0.18 265)", + "oklch(63% 0.18 265)", + "oklch(75% 0.18 265)", + "oklch(87% 0.18 265)", + "oklch(95% 0.18 265)" + ] + }, + "ground": { + "role": "neutral", + "displayName": "Ground", + "canonical": "#edf1f6", + "tonalRamp": [ + "oklch(15% 0.015 265)", + "oklch(27% 0.015 265)", + "oklch(39% 0.015 265)", + "oklch(51% 0.015 265)", + "oklch(63% 0.015 265)", + "oklch(75% 0.015 265)", + "oklch(87% 0.015 265)", + "oklch(95% 0.015 265)" + ] + }, + "surface": { + "role": "neutral", + "displayName": "Surface", + "canonical": "#ffffff", + "tonalRamp": [ + "oklch(15% 0 265)", + "oklch(27% 0 265)", + "oklch(39% 0 265)", + "oklch(51% 0 265)", + "oklch(63% 0 265)", + "oklch(75% 0 265)", + "oklch(87% 0 265)", + "oklch(95% 0 265)" + ] + }, + "ink": { + "role": "neutral", + "displayName": "Ink", + "canonical": "#15223b", + "tonalRamp": [ + "oklch(15% 0.04 265)", + "oklch(27% 0.04 265)", + "oklch(39% 0.04 265)", + "oklch(51% 0.04 265)", + "oklch(63% 0.04 265)", + "oklch(75% 0.04 265)", + "oklch(87% 0.04 265)", + "oklch(95% 0.04 265)" + ] + }, + "muted": { + "role": "neutral", + "displayName": "Muted", + "canonical": "#526079", + "tonalRamp": [ + "oklch(15% 0.04 265)", + "oklch(27% 0.04 265)", + "oklch(39% 0.04 265)", + "oklch(51% 0.04 265)", + "oklch(63% 0.04 265)", + "oklch(75% 0.04 265)", + "oklch(87% 0.04 265)", + "oklch(95% 0.04 265)" + ] + }, + "rule": { + "role": "neutral", + "displayName": "Rule", + "canonical": "#c9d3e3", + "tonalRamp": [ + "oklch(15% 0.025 265)", + "oklch(27% 0.025 265)", + "oklch(39% 0.025 265)", + "oklch(51% 0.025 265)", + "oklch(63% 0.025 265)", + "oklch(75% 0.025 265)", + "oklch(87% 0.025 265)", + "oklch(95% 0.025 265)" + ] + }, + "guide": { + "role": "primary", + "displayName": "Guide", + "canonical": "#c6d2f6", + "tonalRamp": [ + "oklch(15% 0.08 265)", + "oklch(27% 0.08 265)", + "oklch(39% 0.08 265)", + "oklch(51% 0.08 265)", + "oklch(63% 0.08 265)", + "oklch(75% 0.08 265)", + "oklch(87% 0.08 265)", + "oklch(95% 0.08 265)" + ] + }, + "grid": { + "role": "neutral", + "displayName": "Grid", + "canonical": "#dce3ee", + "tonalRamp": [ + "oklch(15% 0.02 265)", + "oklch(27% 0.02 265)", + "oklch(39% 0.02 265)", + "oklch(51% 0.02 265)", + "oklch(63% 0.02 265)", + "oklch(75% 0.02 265)", + "oklch(87% 0.02 265)", + "oklch(95% 0.02 265)" + ] + }, + "primary-soft": { + "role": "primary", + "displayName": "Primary Soft", + "canonical": "#e3e9ff", + "tonalRamp": [ + "oklch(15% 0.055 265)", + "oklch(27% 0.055 265)", + "oklch(39% 0.055 265)", + "oklch(51% 0.055 265)", + "oklch(63% 0.055 265)", + "oklch(75% 0.055 265)", + "oklch(87% 0.055 265)", + "oklch(95% 0.055 265)" + ] + }, + "primary-pressed": { + "role": "primary", + "displayName": "Primary Pressed", + "canonical": "#d3ddff", + "tonalRamp": [ + "oklch(15% 0.07 265)", + "oklch(27% 0.07 265)", + "oklch(39% 0.07 265)", + "oklch(51% 0.07 265)", + "oklch(63% 0.07 265)", + "oklch(75% 0.07 265)", + "oklch(87% 0.07 265)", + "oklch(95% 0.07 265)" + ] + } + }, + "shadows": [], + "motion": [], + "breakpoints": [ + { + "name": "phone", + "value": "760px" + } + ] + }, + "components": [ + { + "name": "Reveal button", + "kind": "button", + "html": "", + "css": "button { font: 600 16px system-ui, sans-serif; min-height:48px; padding:10px 16px; border:1px solid #2348d1; border-radius:8px; background:#2348d1; color:white; cursor:pointer; } button:hover { background:#17349e; } button:focus-visible { outline:3px solid #15223b; outline-offset:4px; } button:disabled { opacity:.5; cursor:wait; }" + } + ], + "narrative": { + "northStar": "Printmaker's proof", + "overview": "A cool white working field, cobalt ink, registration corners, and explicit directed-stroke proofs. The user delegated the visual choice on 2026-10-22. The reference metaphor explains aligned drawing and repeated attempts; it introduces no printing terminology into learner instructions.", + "rules": [], + "dos": [ + "Do prioritize the pad under ordinary indoor and daylight phone use.", + "Do keep human review and development-fixture status honest and readable.", + "Do use semantic HTML and SVG for all instructions, controls, and drawing." + ], + "donts": [ + "Don't add streaks, scores, mastery claims, a lesson catalog, or persistent history.", + "Don't use imagery or animation where it would compete with the drawing task." + ] + } +} diff --git a/.impeccable/surfaces/src-features-practice-browser-practice-tsx.md b/.impeccable/surfaces/src-features-practice-browser-practice-tsx.md new file mode 100644 index 0000000..f33994c --- /dev/null +++ b/.impeccable/surfaces/src-features-practice-browser-practice-tsx.md @@ -0,0 +1,26 @@ +--- +version: 1 +slug: "src-features-practice-browser-practice-tsx" +primary_target: "src/features/practice/components/practice.tsx" +related_targets: [] +--- + + + +# Practice surface + +Mode: Operate. Scope: the single-letter practice page and its responsive controls. + +The user delegated direction and composition on 2026-10-22. Keep the square pad large enough for finger drawing and the control row adjacent. Numbered reference hints appear on Reveal; use the same checked-in directed paths as the faint guide. Avoid duplicated hint panels. + +Composition reference: /Users/jxsh/.codex/generated_images/01a161ab-fc56-7072-819f-9052df880277/exec-23f5a8b9-c24e-4c90-92c7-ab78138bc58f.png. The other two generated compositions are alternatives only. These are preview-only mockups created with the built-in image tool; no raster asset belongs in the app. + +Implementation inventory: semantic HTML for labels, instructions, feedback and buttons; SVG for authored lesson geometry, guide, directed hints and learner ink; CSS for registration corners, responsive layout and states. No photos, textures, raster text, icon library, or external fonts needed. Ignore invented completion/audio affordances in the generated comps. Do not copy their proposed teaching order as reviewed evidence. + +Open gates: original lesson review, calibrated matching, and actual iPhone/Android/stylus checks. A tracing-only development lesson must say review pending and must not imply automatic grading or checked completion. diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..8d1c98e --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,21 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "useTabs": true, + "ignorePatterns": [ + "node_modules", + ".agents", + ".claude", + ".void", + "dist", + "test-results", + "playwright-report" + ], + "overrides": [ + { + "files": ["*.css"], + "options": { + "useTabs": false + } + } + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..879a443 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,63 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": [ + "typescript", + "react", + "jsx-a11y" + ], + "jsPlugins": [ + "./scripts/oxlint-boundaries.mjs", + "oxlint-tailwindcss" + ], + "categories": { + "correctness": "error" + }, + "rules": { + "strokes-boundaries/no-platform-in-domain": "error", + "typescript/no-explicit-any": "error", + "typescript/no-non-null-assertion": "error", + "typescript/consistent-type-imports": "error", + "tailwindcss/no-conflicting-classes": "error", + "tailwindcss/no-unknown-classes": "error", + "tailwindcss/no-dynamic-classes": "error", + "tailwindcss/no-duplicate-classes": "error", + "tailwindcss/no-deprecated-classes": "error" + }, + "overrides": [ + { + "files": [ + "src/**/domain/**/*.{ts,tsx}", + "src/pages/**/*.{ts,tsx}", + ".fence-fixtures-*/domain/*.ts" + ], + "rules": { + "no-restricted-globals": [ + "error", + "window", + "document", + "navigator", + "localStorage", + "sessionStorage", + "fetch", + "PointerEvent", + "ResizeObserver", + "require" + ] + } + } + ], + "ignorePatterns": [ + "node_modules", + ".agents", + ".claude", + ".void", + "dist", + "test-results", + "playwright-report" + ], + "settings": { + "tailwindcss": { + "entryPoint": "src/styles.css" + } + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..99e2f7d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["oxc.oxc-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9969ccf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "biome.enabled": false, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "never" + } +} diff --git a/AGENTS.md b/AGENTS.md index ee417c7..6401abc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,27 +1,48 @@ -# Strokes - -Read [README.md](README.md) for the product, investigation evidence, data ownership, and first implementation slice. - -Project repository and issue tracker: [joshpitzalis/TimTim](https://github.com/joshpitzalis/TimTim). Publish implementation specifications to GitHub Issues with the `ready-for-agent` label after required test-seam confirmation. Create that label if absent. - -## Build reference - -Use [rat-stack](https://ratstack.sh/llms.txt) as the engineering reference. Search its rules and skills before code changes. Read the owning rule and matching skill, then inspect the installed package instructions and pinned source for Effect, XState, and infrastructure work. - -Owner decisions: React for the UI; no Foldkit. Void owns the framework and deployment; use it instead of Alchemy. Effect owns external work and typed failures. Retain rat-stack's boundary and fence patterns while following these explicit framework choices. - -## Product boundaries - -Start with mobile tracing of one reviewed Hindi letter. Keep the input loop local and geometry pure. Author original lesson templates; Skritter's code and data are research references only. - -The checked-in lesson is authoritative for the teaching sequence. React state owns the temporary attempt. Add durable progress only after naming its writer and authoritative store. - -## Enforcement - -The folder currently contains documentation only. The rat-stack fence is required but is not installed or verified yet. Establish exact dependency pins, strict type checking with Effect diagnostics, import-boundary lint, tests, commit hooks, and CI with the scaffold. Derive commands from its package scripts. - -Keep provider implementations outside domain code. Framework routes adapt shared contracts when server actions are introduced. Keep browser APIs in browser modules. Report violations of these patterns explicitly and fix their cause; preserve checks rather than bypassing them. - -## Verification - -Use geometry properties for the matcher and browser interaction checks for drawing, clearing, resizing, and cancellation. Validate mobile behavior on real devices. Report local checks, deployment, and device verification separately. +# Project conventions + +TypeScript web app using Effect. Follow the installed Effect version and existing project conventions. + +When local checks pass but CI fails, follow [the CI debugging guide](docs/ci-debugging.md). + +## Feature ownership + +- Build self-contained vertical slices. Keep feature-specific domain code, use cases, services, UI, tests, and supporting code together. +- Removing functionality should mean deleting its feature module and removing minimal, explicit registration. Document unavoidable external dependencies or cleanup in the feature README. +- Expose a small public API; do not import another feature's internals. Extract shared code only when it is genuinely shared. +- Preserve applied database migrations; deleting code must not implicitly delete persisted data or external resources. +- Colocate tests in a `__tests__/` folder inside each feature, such as `src/features/installation/__tests__/`. Keep feature-specific fixtures and test helpers there too. +- When a feature starts as a single file, give it a feature folder when adding tests so its implementation and tests stay together. +- Verify changed behavior with appropriate checks. + +## Structure and responsibilities + +Keep named features under `src/features/`. Start small: a whole feature can begin as one file, such as `src/features/installation.ts`. As responsibilities grow, use this structure: + +```text +src/features/installation/ + index.ts # Public API and feature wiring + domain/ # Entities, value objects, schemas, and their invariants + use-case/ # Business actions; coordinate domain rules and services + services/ # Effect service contracts, implementations, and Layers + +src/main.ts # Compose Layers and start the app +``` + +- `domain/` owns entities, value objects, their schemas, invariants, and business errors. Keep rules intrinsic to those models here; purity alone does not make a function domain code. Domain code must not depend on HTTP, databases, use cases, or service implementations. +- `use-case/` owns application actions and their supporting algorithms. Put exercise-specific operations such as character matching and its tolerance policy in the owning use case's `helpers/`, even when they are pure functions. +- Use cases depend on service contracts and domain rules. Keep SQL and external API details in services. Expose intended public operations through the owning use case's `index.ts`; external callers use that API, while internal files may import sibling helpers directly. +- Keep each service's contract, implementation, and Layer together by default. Acquire external resources through Effect, never as import-time side effects. +- Use these names: `use-case/` and `services/`. + +## Grow files incrementally + +- Aim for roughly 200 lines per file; this is a soft limit. +- Any module—feature, use case, service, or utility—may start as one file. +- As it grows, replace the file with a same-named folder containing `index.ts`. Extract the most semantically isolated concept into a descriptively named sibling file; repeat as needed. +- Example: `utils.ts` → `utils/index.ts` + `utils/formatCurrency.ts`. +- The index may retain cohesive implementation; it need not be only re-exports. Avoid redundant nested folders and arbitrary splits. +- Preserve public exports and update imports according to the project's module-resolution rules. +- Use cases and services may each own `helpers/` for named supporting operations and `lib/` for files such as `errors.ts`, `utils.ts`, and `constants.ts`. These files follow the owning module's dependency boundaries. +- Create only needed files and folders. Keep supporting code local; do not scaffold empty directories or speculative abstractions. + +Verify changed behavior with appropriate checks and keep tests near their owner. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..741f80b --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,111 @@ +--- +name: Strokes +description: A registered drawing frame for Hindi handwriting practice +colors: + primary: "#2348d1" + primary-deep: "#17349e" + ground: "#edf1f6" + surface: "#ffffff" + ink: "#15223b" + muted: "#526079" + rule: "#c9d3e3" + guide: "#c6d2f6" + grid: "#dce3ee" + primary-soft: "#e3e9ff" + primary-pressed: "#d3ddff" +typography: + body: + fontFamily: "system-ui, sans-serif" + fontSize: "1rem" + lineHeight: 1.5 + title: + fontFamily: "system-ui, sans-serif" + fontSize: "1.75rem" + fontWeight: 700 + lineHeight: 1.15 + title-phone: + fontSize: "1.5rem" + pronunciation: + fontSize: "2rem" + lineHeight: 1.15 + section: + fontSize: "1.125rem" + fontWeight: 650 + lineHeight: 1.35 + feedback: + fontSize: "0.9375rem" + small: + fontSize: "0.875rem" + caption: + fontSize: "0.75rem" + glyph: + fontSize: "4rem" + lineHeight: 1 + glyph-phone: + fontSize: "3.5rem" + lineHeight: 1 +rounded: + control: "8px" + notice: "6px" +spacing: + small: "8px" + group: "16px" + section: "32px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.surface}" + rounded: "{rounded.control}" + padding: "10px 16px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" + button-secondary: + backgroundColor: "{colors.surface}" + textColor: "{colors.primary}" + rounded: "{rounded.control}" + padding: "10px 16px" + button-secondary-hover: + backgroundColor: "{colors.primary-soft}" + button-secondary-active: + backgroundColor: "{colors.primary-pressed}" +--- + +# Design System: Strokes + +## Overview + +**Creative North Star: "Printmaker's proof"** + +A cool white working field, cobalt ink, registration corners, and a faint whole-letter reference. The user delegated the visual choice on 2026-10-22. The reference metaphor explains aligned drawing and repeated attempts; it introduces no printing terminology into learner instructions. + +## Colors + +White owns the writing surface; a cool neutral ground separates the workspace from its surroundings. Cobalt marks active ink and actions. Dark ink and muted blue-gray support readable instructions. Faint blue guides are reference geometry, never status text. + +## Typography + +Use system sans for controls and instructions, with readable fixed rem sizes. Pronunciation uses the title size on phones. The lesson glyph and reference paths carry the visual identity. Use the platform Devanagari fallback for the glyph label; the actual teaching paths are authored SVG geometry, never a font outline. + +## Layout + +The square writing surface leads. A compact lesson header precedes it; instructions and labeled controls remain adjacent. On wide screens, an instruction rail sits beside the drawing frame. At the phone breakpoint (760px), the rail moves below the pad and controls stay reachable. Avoid a dashboard, lesson catalog, or decorative card grid. + +## Elevation & Depth + +Flat working surfaces with registration marks and subtle border contrast. No decorative glows, glass, or simulated paper texture. Depth must not confuse the writing boundary. + +## Shapes + +Keep the pad geometrically square with modest corner treatment. Controls have consistent gently rounded corners and generous touch areas. Registration marks remain outside the input bounds. + +## Components + +The pad is a real SVG with a shared lesson coordinate system. Reference geometry is faint; learner ink is cobalt. The whole-letter guide stays faint throughout practice, with no numbered strokes or direction arrows. Buttons use text labels, visible focus, hover and pressed states. They have a minimum touch height of 48px; preparation disables them visibly. Feedback uses readable text with a stable position to avoid layout movement during drawing. + +## Do's and Don'ts + +- Do prioritize the pad under ordinary indoor and daylight phone use. +- Do keep human review and development-fixture status honest and readable. +- Do use semantic HTML and SVG for all instructions, controls, and drawing. +- Don't add streaks, scores, mastery claims, a lesson catalog, or persistent history. +- Don't use imagery or animation where it would compete with the drawing task. diff --git a/GLOSSARY.md b/GLOSSARY.md new file mode 100644 index 0000000..c059b45 --- /dev/null +++ b/GLOSSARY.md @@ -0,0 +1,17 @@ +# Hindi letter practice + +Practice writing a prompted Hindi letter and assess the finished drawing. + +## Language + +**Reference trace**: +The example drawing that defines the target letter form for an exercise. A different acceptable letter form requires its own reference trace. +_Avoid_: Stroke sequence when describing the assessment target + +**Letter attempt**: +A learner's complete drawing of one prompted letter, potentially made with several strokes. +_Avoid_: Stroke when referring to the whole letter + +**Stroke**: +One uninterrupted drawing gesture, from touching the writing area to lifting the finger or pen. + diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..d14751a --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,46 @@ +# Product + + + +## Platform + +web + +## Users + +A Hindi learner practicing isolated letters in a mobile browser, using a finger or stylus. Mouse input supports the same exercise on desktop. The approved specification also identifies the learner as a TypeScript developer who wants an understandable starting point. Age and whether an adult guides practice remain undecided. + +## Product Purpose + +Strokes makes one Hindi letter available for tracing, retrying, and checking the whole character. Success means reliable local interaction and honest feedback against original, reviewed lesson content. + +## Operating Context + +The writing surface must work on a phone, allow page scrolling outside the pad, recover from interrupted gestures, and preserve alignment across display sizes. Actual iPhone Safari and Android Chrome checks are required separately from browser emulation. + +## Capabilities and Constraints + +- Void and React own the app; deterministic geometry stays independent. Effect owns parsing boundaries, external work, and typed failures when needed. +- The checked-in lesson owns the reference geometry. React owns the temporary attempt. There are no accounts, uploads, durable progress, analytics, or database. +- One original letter comes first. The specific letter and reviewed shape are open; any development fixture must carry an explicit unreviewed label. +- Check compares the whole character to its guide; Retry resets the exercise. The faint whole-letter guide stays visible throughout practice. +- The overall character shape matters; stroke order and direction are unrestricted. Tolerances remain provisional until original finger and stylus recordings support calibration. + +## Brand Commitments + +The learner-facing product name is Strokes. TimTim names the repository. The owner delegated the visual direction on 2026-10-22; DESIGN.md records the selected drawing-frame system. + +## Evidence on Hand + +The owner approved the product and test seams in SPEC.md and GitHub issue #1 on 2026-10-22. The Skritter research informs a next-stroke approach but provides no reusable implementation or lesson data. Reviewed Hindi content, calibrated matcher fixtures, and physical-device evidence are not yet available. + +## Product Principles + +- Keep practice local and understandable. +- Make the expected action and current step clear. +- Separate learner retries from application failures. +- Claim completion only after the required evidence exists. + +## Accessibility & Inclusion + +Use labeled semantic controls, visible keyboard focus, readable feedback beyond color, and adjacent pad instructions. Handwriting itself requires a pointing device in this version. A pronunciation label is text; audio is outside scope. diff --git a/README.md b/README.md index cfe3f26..a0f0f18 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,3 @@ -# Strokes +# TimTim A small Hindi handwriting practice app for mobile browsers. - -Status: investigation and implementation plan. No app scaffold, dependencies, automated fence, or deployment exists yet. - -Use React and Void. Use rat-stack as the engineering reference: Effect for external work and typed failures, and compiler, lint, tests, and hooks for enforcement. The owner's explicit choices replace Foldkit with React and Alchemy with Void deployment. - -The implementation specification is [SPEC.md](SPEC.md), published as [GitHub issue #1](https://github.com/joshpitzalis/TimTim/issues/1). The owner confirmed the test seams and four-ticket breakdown on 2026-10-22. Implementation tickets [#2](https://github.com/joshpitzalis/TimTim/issues/2), [#3](https://github.com/joshpitzalis/TimTim/issues/3), [#4](https://github.com/joshpitzalis/TimTim/issues/4), and [#5](https://github.com/joshpitzalis/TimTim/issues/5) are labeled `ready-for-agent` and linked in dependency order; #2 can start immediately. - -## What the Skritter investigation found - -The open Chrome tab was readable through native accessibility. Chrome did not expose a debugging endpoint, so REA could not attach to the authenticated page. The study session was left unchanged. - -I downloaded the public scripts linked by `https://skritter.com/study` and ran REA 6.7.0 static analysis on `application.js`. REA parsed the selected bundle successfully: 401,723 AST nodes, zero parse failures. Its graph did not recover the bundle's internal modules. The following details come from focused manual inspection of the named `require.register` blocks in those same downloaded bytes, alongside REA's artifact evidence. - -| Question | Finding | Source in the bundle | -| --- | --- | --- | -| How does drawing work? | The widget collects points between pointer-down and pointer-up equivalents, smooths the visible line, and uses CreateJS canvas layers. | `components/prompt/canvas/StudyPromptCanvasComponent.js`, `enableInput`, `onInputMove` | -| Does it recognize any handwritten character? | This code checks input against candidates at the current position in the known character's stroke sequence. | `utils/recognizer.js`, `getResults`; `collections/PromptStrokeCollection.js` | -| What does it compare? | Initial direction, corner count, and distance between bounding-box centers. Parameters can skip individual checks. | `utils/recognizer.js`, `runChecks`, `checkAngle`, `checkCorners`, `checkDistance` | -| How are corners found? | A ShortStraw routine derives corners from the input points. | `models/PromptStrokeModel.js`, `updateCorners`; `utils/shortstraw.js` | -| Why does accepted writing look clean? | A successful attempt can animate a transformed reference shape into its target shape. A raw-input preference has a separate display branch. | `components/prompt/part-rune/StudyPromptPartRuneComponent.js`, `handlePromptCanvasInputUp` | -| Is only one reference sequence possible? | The stroke collection tracks several targets and keeps those most compatible with accepted strokes. | `collections/PromptStrokeCollection.js`, `getExpectedTargets` | - -These are static code findings from the legacy web bundle. They do not prove which branches ran in your session or describe Skritter's native apps. No trained recognizer was needed to explain this particular code path. - -REA evidence: `ev_ac4cc745a3f06625034762dfdcc80b0b49437d7c2367df61f361a94018d9353b`. -The source hashes, selection, and limitations are in [research/rea-summary.json](research/rea-summary.json). Full temporary evidence is at `/private/tmp/skritter-rea-evidence.json`. - -## The smallest useful implementation - -Build a trace-and-retry exercise before automatic grading. The first screen has one letter, a faint guide, a square writing area, Clear, Reveal, and Retry. Add Next when there is a second reviewed letter. - -Use SVG for this version. Store each pen-down-to-pen-up gesture as an ordered list of points, then render it as a polyline or path. A shared `viewBox` keeps the guide, input, and reference geometry aligned as the screen changes size. Canvas is also viable; SVG makes this small exercise easy to inspect. - -The flow is: - -```text -finger / mouse / pen - | - v -points in the writing area's coordinate system - | - v -visible ink -> release -> optional stroke check - | - accept or retry -``` - -Pointer Events support mouse, pen, and touch through one event family. Use pointer capture during a gesture. Apply `touch-action: none` only to the writing area. Handle cancellation and capture loss, accept only the active pointer, and keep page scrolling available outside the pad. Map client coordinates into SVG coordinates rather than assuming CSS pixels equal drawing coordinates. See [Pointer Events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events). - -## Data ownership - -- A checked-in lesson file owns the glyph label, pronunciation label, coordinate bounds, and ordered reference strokes. The content author is its writer. -- A reference stroke is a directed centerline: ordered points from its start to its end. The same points can drive tracing, hints, and checking. -- The React exercise state owns the current attempt, active pointer, and accepted-stroke position. Pointer handlers write it. Rendered paths are derived from that state. -- Reference and learner coordinates share fixed bounds. Preserve position and scale relative to the letter; independently centering each stroke would accept strokes in the wrong place. -- Attempts are temporary in the first slice. There is no database, localStorage history, or progress claim to maintain. - -An ordinary font can supply a visual glyph, but its filled outlines do not provide a teaching sequence or directed pen paths. Author a few original templates, preferably by recording a Hindi teacher drawing each letter. Review pen lifts, direction, the headline, and acceptable variations before calling the app a writing teacher. - -Start with isolated letters. Devanagari vowel signs and conjuncts require more than treating a word as unrelated Unicode code points. See [W3C Devanagari requirements](https://www.w3.org/International/ilreq/devanagari/). - -## Add checking after tracing works - -Check one expected stroke locally after release. Keep the geometry function pure and independent from React, Void, and storage. - -1. Reject empty, non-finite, and degenerate input before scoring. -2. Resample input and reference by distance along each path, so sampling speed has less influence. -3. Compare corresponding positions, start and end locations, and path length. Direction matters; a reversed stroke should produce a retry. -4. Derive tolerances from recorded finger and stylus attempts. Do not copy Skritter's pixel thresholds into a different coordinate system. -5. On acceptance, show the reference stroke and advance. On rejection, retain the same expected stroke and show a useful hint. - -This is a proposed simple matcher, not a claim that Skritter uses this exact algorithm. It is sufficient to experiment with known letters. It does not recognize arbitrary handwriting or grade penmanship. - -Useful checks include equivalent resampling of the same path, resize invariance, reversed paths, misplaced strokes, scribbles, dots, and cancelled gestures. A browser check must prove that drawing does not scroll the page, clearing works, and a cancelled gesture does not advance. Try a real iPhone Safari and Android Chrome before claiming mobile validation. - -## Build order - -1. Scaffold Void with React and pnpm, retaining its generated version pins. Read installed Void and Effect instructions. Install and prove the rat-stack compiler, lint, and hook fence before feature code. -2. Implement the tracing screen with one reviewed letter. Completion means a real phone can draw, clear, reveal, and retry without stuck gestures. -3. Add the pure matcher with meaningful geometry tests. Completion means recorded good and bad attempts have useful outcomes. -4. Expand to a handful of reviewed letters. Completion means each template has an agreed teaching sequence. -5. Add saved progress only when needed. Define its authoritative store and writer first, then follow rat-stack's store and capability patterns. - -The Void [quickstart](https://void.cloud/guide/quickstart) documents `pnpm add -D void` followed by `pnpm void init`. Choose React and skip deployment setup initially. Exact package pins must be established from the actual scaffold and rat-stack reference at implementation time. - -## How rat-stack changes this design - -React owns rendering and temporary interaction state. Keep browser drawing APIs in the browser. Pure geometry stays ordinary deterministic TypeScript. Effect owns external operations, resource lifetimes, schema parsing, and typed failures as those needs arise. Avoid creating an Effect runtime per pointer movement. - -Before adding a meaningful lifecycle with asynchronous work, follow [add-a-lifecycle-machine](https://ratstack.sh/skills/add-a-lifecycle-machine). Before adding server actions, follow [add-a-capability](https://ratstack.sh/skills/add-a-capability): shared Schema contracts and one handler, with thin framework adapters. A retry from the learner is a product outcome; a missing lesson or storage failure is a distinct failure. - -Keep the [fence](https://ratstack.sh/systems/fence): strict type checking with Effect diagnostics, import boundaries, lint, pre-commit checks, and CI. Run the scaffold's equivalent of `pnpm turbo run check test build`; the script does not exist in this folder yet. Use [keep-or-cut](https://ratstack.sh/skills/keep-or-cut) to omit unrelated auth, analytics, MCP, code-mode, and database features. - -### Deployment ownership - -The owner selected Void instead of Alchemy. Void owns resource provisioning and deployment. Follow its [deployment guide](https://void.cloud/guide/deployment) and keep one owner for each cloud resource. - -Start with local development and no cloud data resources. Keep rat-stack's checks and boundary patterns, adapted to the Void project. No deployment has been performed. - -## Intentionally deferred - -Accounts, email, cloud persistence, spaced repetition, whole-word recognition, OCR, model training, and polished stroke morphing. No Skritter code or character dataset is part of the proposed app. diff --git a/SPEC.md b/SPEC.md deleted file mode 100644 index c9fd159..0000000 --- a/SPEC.md +++ /dev/null @@ -1,137 +0,0 @@ -# Strokes: basic Hindi handwriting practice in a mobile browser - -Status: approved specification published as [GitHub issue #1](https://github.com/joshpitzalis/TimTim/issues/1) with `ready-for-agent`. The owner confirmed the four-ticket breakdown and test seams on 2026-10-22: complete-exercise browser checks, pure matcher geometry properties, and lesson-content boundary checks. Implementation and device verification remain outstanding. - -## Problem Statement - -The learner wants to practice writing Hindi characters using a phone browser. Skritter demonstrates the desired interaction: write a character directly on screen, receive guidance, and try again. - -The learner is a TypeScript developer who wants a small, understandable starting point. The first version must make handwriting practice useful without requiring native apps, trained handwriting models, accounts, or a complete language-learning platform. - -## Solution - -Build Strokes as a mobile-friendly web app using Void, React, and TypeScript. The learner opens a lesson, sees a Hindi letter and pronunciation label, and draws in a square writing area with a finger, stylus, or mouse. - -Provide a faint tracing guide, a way to reveal the expected writing, and simple controls to clear and retry. Add forgiving checks of one expected stroke at a time. An accepted stroke advances the exercise; an unsuccessful stroke leaves the learner on the same step with a useful hint. Completing every expected stroke produces a clear completion state and an option to practice again. - -Start with one original, reviewed letter template. Prove drawing and retrying before introducing automatic checking. The full scope of this specification includes that first letter's checked exercise. Further letters can use the same lesson format after content review. - -## User Stories - -1. As a Hindi learner, I want to open practice in my mobile browser, so that I can practice without installing a native app. -2. As a Hindi learner, I want to begin without creating an account, so that I can try the exercise immediately. -3. As a Hindi learner, I want to see which letter I am practicing, so that I understand the exercise. -4. As a Hindi learner, I want a pronunciation label beside the letter, so that I can connect its written form with its sound. -5. As a beginning learner, I want a faint reference in the writing area, so that I know where to place my strokes. -6. As a beginning learner, I want to see the expected stroke sequence, so that I can learn how to construct the letter. -7. As a learner, I want a hint for the next stroke, so that I can continue when I am unsure. -8. As a phone user, I want to draw with my finger, so that I can practice using the device I already have. -9. As a stylus user, I want to draw with my pen, so that I can practice with more precise input. -10. As a desktop user, I want to draw with a mouse, so that I can use the same exercise on my computer. -11. As a learner, I want ink to appear as I move, so that drawing feels responsive. -12. As a learner, I want each pen lift to finish one stroke, so that separate gestures remain separate. -13. As a phone user, I want drawing inside the pad to avoid scrolling the page, so that I can finish a stroke reliably. -14. As a phone user, I want to scroll outside the pad, so that the rest of the page remains usable. -15. As a phone user, I want an accidental second finger to leave my current stroke intact, so that it does not corrupt my attempt. -16. As a learner, I want interrupted input to stop cleanly, so that returning to the exercise does not leave it drawing indefinitely. -17. As a learner, I want the letter and my input to remain aligned when the pad changes size, so that resizing does not change the exercise. -18. As a learner, I want a Clear control, so that I can discard my current attempt and start from the first stroke. -19. As a learner, I want to reveal the expected letter and sequence, so that I can compare it with my attempt. -20. As a learner, I want revealing the answer to remain distinct from completing the exercise, so that I receive honest feedback. -21. As a learner, I want each completed gesture checked against the next expected stroke, so that feedback relates to the step I am practicing. -22. As a learner, I want reasonable variation in finger drawing to be accepted, so that small inaccuracies do not prevent practice. -23. As a learner, I want stroke direction and placement to matter, so that a reversed or misplaced gesture does not teach me the wrong movement. -24. As a learner, I want unsuccessful strokes to leave the exercise on the same step, so that I can retry without restarting the whole letter. -25. As a learner, I want a useful next-stroke hint after an unsuccessful attempt, so that I know what to try next. -26. As a learner, I want accepted strokes to remain visible, so that I can see the letter taking shape. -27. As a learner, I want a clear completion message after the final accepted stroke, so that I know when I have finished. -28. As a learner, I want Retry to reset the exercise, so that I can repeat the same letter. -29. As a learner, I want readable feedback and labeled controls, so that color or unfamiliar icons are not my only instructions. -30. As a keyboard user, I want to operate the surrounding controls with visible focus, so that navigation and retry remain accessible. -31. As a learner, I want a clear message if lesson content cannot be used, so that an app failure is not presented as a mistake in my writing. -32. As a learner, I want my handwriting to stay in the current browser session, so that this basic exercise does not upload or retain my attempts. -33. As a lesson author, I want to define original reference strokes in their teaching order, so that guidance and checking use the same reviewed content. -34. As a lesson author, I want invalid lesson geometry detected before it becomes a usable exercise, so that learners do not encounter broken templates. -35. As the developer, I want geometry separated from rendering and external work, so that I can understand and test the matcher independently. -36. As the developer, I want compiler, lint, and automated checks to enforce the project boundaries, so that future changes follow the chosen architecture. - -## Implementation Decisions - -- **Framework and deployment:** Use Void with React and TypeScript. Void owns deployment and any eventual cloud provisioning. Alchemy and Foldkit are excluded by explicit owner decisions. Follow Void's pnpm quickstart and begin locally without configuring a production deployment. -- **Engineering reference:** Retain rat-stack's source-first workflow, exact dependency pins, strict types, explicit ownership, typed boundaries, and enforceable fence. Adapt these patterns to Void and React rather than copying unrelated reference-app features. -- **Delivery sequence:** Establish the scaffold and fence, prove the single-letter tracing interaction, then add stroke checking. These are implementation milestones within this specification. A tracing-only milestone must not be reported as a completed checked exercise. -- **Lesson content:** A lesson has an identity, glyph label, pronunciation label, fixed coordinate bounds, and a nonempty ordered sequence of reference strokes. Each stroke contains a directed centerline represented by finite ordered points. -- **Content authority:** Checked-in original lesson data is the authoritative teaching sequence. The content author is its writer. The same reference points produce guides, hints, and matcher inputs. Ordinary font outlines are not a substitute for stroke-order data. -- **Content readiness:** Begin with one Hindi letter whose shape, pen lifts, direction, headline treatment, and teaching sequence have been reviewed by a competent Hindi writer or teacher. The specific letter and accepted sequence are not selected in this conversation. Content review is a dependency for claiming pedagogical validity; an unreviewed development fixture must be identified as such. -- **Drawing surface:** Use SVG with one shared coordinate system for reference and learner strokes. Convert browser input coordinates into that system. Keep the visible pad responsive and square. -- **Input ownership:** Track one active pointer. Capture it during a gesture, collect its points, and finish on release. Ignore other pointers during that gesture. Cancellation or lost capture discards the unfinished stroke without advancing or generating a handwriting failure. Releasing outside the pad must terminate the gesture safely; a path leaving the exercise bounds must not be accepted through coordinate clamping. -- **Mobile interaction:** Disable browser touch gestures only inside the drawing surface. Preserve normal page behavior elsewhere. Clear active input safely if layout changes during a stroke, while retaining completed strokes in lesson coordinates. -- **Transient state:** React owns the active pointer, unfinished stroke, accepted-stroke position, visible feedback, reveal state, and completion state. Rendered geometry is derived from that state. Clearing or retrying resets the attempt, feedback, and completion state to the first expected stroke. -- **Presentation:** Show the letter, pronunciation label, faint guide, writing pad, and labeled Clear, Reveal, and Retry controls. Revealing exposes the reference and ordered stroke hints without accepting strokes or marking completion. Hint presentation can be static; animation is not required. -- **Matching boundary:** A pure local geometry operation receives the completed input stroke and the next reference stroke. It returns an explicit accepted-or-retry outcome. It has no dependency on React, Void, storage, or network access. -- **Matching method:** Resample both paths by distance along each path. Compare corresponding positions, start and end locations, and path length. Preserve placement and scale relative to the whole letter. Do not independently recenter or resize individual strokes. Reject invalid and degenerate input before scoring; the first lesson must not require dot-like strokes. -- **Calibration:** Establish tolerances using recorded finger and stylus attempts. Thresholds are hypotheses until calibrated. Do not reuse Skritter's numeric thresholds or invent a recognition-accuracy claim. Preserve representative good and bad attempts as original fixtures. -- **Feedback:** Acceptance commits the reference stroke and advances exactly once. A retry does not advance or erase previously accepted strokes; remove the rejected transient ink and show a next-stroke hint. A clear textual completion state appears only after all reference strokes have been accepted. -- **Failure semantics:** An ordinary mismatch is a learner retry outcome. An unusable lesson or application failure is a separate failure state and cannot count as either successful writing or a handwriting mistake. -- **Effect boundary:** Keep interactive input and deterministic geometry local. Use Effect for external work, schema parsing at trust boundaries, typed failures, and resource lifetimes where required. Avoid a runtime invocation for each movement event. Follow the installed Effect version's instructions before implementation. -- **Server boundaries:** The first exercise needs no server mutation, database, or grading endpoint. If external lesson loading becomes necessary, keep transport in an adapter and expose failures explicitly. Any future server action follows rat-stack's shared-contract and handler pattern rather than duplicating domain behavior in Void routes. -- **Persistence and privacy:** Attempts exist only in memory for the current exercise. Refreshing or leaving may discard them. There is no attempt upload, durable progress, analytics stream, or hidden second store. -- **Accessibility:** Use semantic buttons, visible keyboard focus, readable labels, and text feedback in addition to color. The drawing interaction requires a pointing device in this version; keyboard handwriting input is outside scope. Label the pad and provide adjacent instructions for assistive technology. -- **Fence:** Establish exact pins, strict TypeScript checks, Effect diagnostics where applicable, import-boundary lint, meaningful tests, commit hooks, and CI before feature work. Enforce browser/server separation and keep provider adapters out of domain geometry. Preserve failing checks and report architectural violations explicitly. - -## Testing Decisions - -- **Seam confirmation:** Owner confirmed browser tests at the complete exercise boundary, focused geometry properties at the pure matcher boundary, and lesson-content boundary checks on 2026-10-22. No existing application test seam exists in this documentation-only project. -- **Primary seam:** Exercise the visible React page through browser pointer input and controls. Assert rendered strokes, feedback, progression, and completion. Avoid assertions about component internals, hook calls, event-handler names, or SVG implementation structure. -- **End-to-end behavior:** Prove a valid stroke advances once, a retry preserves the current step, revealing does not complete the exercise, and the final accepted stroke produces completion. Clear and Retry must return the exercise to its initial usable state. -- **Input robustness:** Cover interrupted input, capture loss, release outside the pad, a second pointer, resize during drawing, and repeated controls. After cancellation, the next valid gesture must work without refreshing. -- **Geometry seam:** Test matcher behavior using generated paths and original recorded examples. Check sampling-density invariance, consistent coordinate mapping across display sizes, wrong direction, misplaced strokes, excessive path length, degenerate input, and non-finite coordinates. Do not assert the implementation's chosen intermediate values or duplicate its scoring formula in tests. -- **Content boundary:** Check that unusable lesson content is rejected before interaction and that the learner sees a content failure rather than handwriting feedback. Validate the actual initial lesson, including its stroke order and coordinate bounds. -- **Meaningful properties:** Use rat-stack's preference for property and model tests where they fit. Demonstrate that a relevant intentional defect makes a new property fail before trusting it. Add example tests at browser and content seams where a concrete case communicates the behavior better. -- **Prior art:** There are no existing Strokes application tests to extend. Rat-stack's fence, Effect test conventions, and behavior-testing guidance are references, not evidence that Strokes already conforms. Select exact tooling from the scaffold and retain its version compatibility. -- **Physical devices:** Verify finger drawing, scroll behavior, pad sizing, control usability, and interruption recovery on an actual iPhone with Safari and Android phone with Chrome. Browser emulation supports development but does not establish real-device success. -- **Completion evidence:** Report automated checks, device checks, and any deployed smoke test separately. A successful build does not prove calibrated matching or sound Hindi instruction. Production publication is not required to finish the local exercise implementation. - -## Out of Scope - -- Accounts, authentication, email, subscriptions, and payment. -- Saved progress, cross-device sync, databases, and attempt history. -- Spaced repetition, due-item scheduling, streaks, leaderboards, and analytics. -- Full Hindi alphabet coverage, a curriculum, and a required fixed-size lesson catalog. -- Whole words, conjuncts, vowel-sign composition exercises, and arbitrary handwriting recognition. -- OCR, machine-learning training, AI grading, and penmanship scores. -- Multiple acceptable stroke sequences for each letter in the initial lesson. -- Dot-like reference strokes in the first lesson; these need a separate matching policy. -- Audio playback, speech synthesis, pronunciation assessment, and example sentences. -- Native mobile apps, offline installation, and service-worker caching. -- Polished stroke morphing, elaborate animations, and reproduction of Skritter's branding or layout. -- Reusing Skritter's implementation or proprietary character data in the product. -- Foldkit, Alchemy, MCP interfaces, code-mode interfaces, and unrelated rat-stack sample features. -- Creating a production cloud account or deploying the app as part of this specification-writing task. - -## Further Notes - -The Skritter investigation supports the known-character, next-stroke approach. REA statically parsed its public legacy web bundle, and focused manual inspection identified point collection, candidate checks, and reference-shape rendering. The live authenticated recognition path was not exercised. These observations inform this design; they do not validate the proposed matcher. - -The original investigation's REA evidence ID is `ev_ac4cc745a3f06625034762dfdcc80b0b49437d7c2367df61f361a94018d9353b`. The project retains its source hashes and coverage limitations in the research summary. - -Implementation references: - -- [Void quickstart](https://void.cloud/guide/quickstart) -- [Void deployment](https://void.cloud/guide/deployment) -- [Rat-stack agent guide](https://ratstack.sh/llms.txt) -- [Rat-stack fence](https://ratstack.sh/systems/fence) -- [Rat-stack keep-or-cut guidance](https://ratstack.sh/skills/keep-or-cut) -- [Pointer Events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) -- [Devanagari layout requirements](https://www.w3.org/International/ilreq/devanagari/) - -Delivery is the approved sequence: local practice shell and fence; tracing one reviewed letter; checked strokes and completion; calibration and real-phone verification. Apply `ready-for-agent` to the specification and implementation tickets. Work only on tickets whose blockers are complete. Lesson review, calibration, and device verification remain explicit completion gates. - -## Published implementation tickets - -All four tickets carry `ready-for-agent` and are sub-issues of the specification. Native GitHub blocking relationships enforce this sequence: - -1. [#2: Run the local practice shell with enforced boundaries](https://github.com/joshpitzalis/TimTim/issues/2) — no blockers; can start immediately. -2. [#3: Trace, reveal, and retry one reviewed Hindi letter](https://github.com/joshpitzalis/TimTim/issues/3) — blocked by #2. -3. [#4: Check each stroke and complete the letter](https://github.com/joshpitzalis/TimTim/issues/4) — blocked by #3. -4. [#5: Calibrate checking and verify practice on real phones](https://github.com/joshpitzalis/TimTim/issues/5) — blocked by #4. diff --git a/docs/adr/0001-assess-the-finished-letter.md b/docs/adr/0001-assess-the-finished-letter.md new file mode 100644 index 0000000..93a8c04 --- /dev/null +++ b/docs/adr/0001-assess-the-finished-letter.md @@ -0,0 +1,17 @@ +# Assess the finished letter + +Assess the learner's finished letter attempt against a reference trace rather than compliance with a prescribed stroke sequence. Each reference trace defines a target letter form; accepting a different letter form requires authoring its own trace. The goal is a forgiving recreation of that example, with stroke order, drawing direction, and number of pen lifts excluded from assessment. + +The learner writes over a faint guide and explicitly presses Check to submit the whole letter attempt. Allow small deviations while retaining the guide's approximate size and position; do not automatically recenter or resize the attempt. Repeated passes over the same correct line must not count against the learner merely because the ink becomes darker or thicker. + +Require both coverage of the whole reference trace and sufficiently little ink away from it. Substantial missing sections and substantial extra lines fail; tiny gaps and accidental tails may pass. Measuring stray distance alone is insufficient because an incomplete drawing could lie entirely on the guide. The comparison method remains open; distance from the trace is the owner's suggested starting point. + +After a failed check, retain the drawing so the learner can add missing parts and check again. Clear discards the attempt and starts again; removing unwanted lines initially requires restarting. + +Give a simple textual reason for failure, distinguishing missing parts from drawing too far from the guide. Highlighting individual regions is outside the initial scope. On success, show a success message and retain the drawing until the learner chooses Retry; do not reset or advance automatically. The first version uses one reference trace. + +Any new drawing clears the previous assessment result. The learner must press Check again to assess the changed attempt. + +Lean forgiving on borderline attempts: allow wobble, small gaps, and minor tails while rejecting substantial missing parts and extra lines. Calibration against judged handwriting examples is explicitly deferred. Initial numerical tolerances remain provisional implementation choices, not validated handwriting standards. + +This replaces the next-stroke assessment direction in the existing specification. The owner confirmed the resulting specification and its proposed test seams on 2026-10-24. The specification is published as [issue #7](https://github.com/joshpitzalis/TimTim/issues/7) with the ready-for-agent label. diff --git a/docs/ci-debugging.md b/docs/ci-debugging.md new file mode 100644 index 0000000..3bd4d50 --- /dev/null +++ b/docs/ci-debugging.md @@ -0,0 +1,29 @@ +# Debugging CI-only failures + +## Reproduce before pushing + +1. Identify the failing run, commit, test, and assertion. Inspect its log and retained Playwright trace from the `browser-results` artifact. +2. Reproduce the failing test in Linux with the command below. Observe the same failure before changing code. If it does not reproduce, compare the browser, architecture, dependencies, and test inputs with CI. +3. Change one suspected cause. Require the original assertion to pass in the reproducing environment; preserve coverage and keep retries disabled. +4. Run the affected suite, then the normal commit checks. Push the verified fix and watch the workflow for that exact commit. + +## Linux browser tests + +Start Docker and install local dependencies with `pnpm install --frozen-lockfile`, then run: + +```sh +pnpm test:browser:linux mobile.spec.ts +pnpm test:browser:linux +``` + +The command forwards Playwright arguments, copies tracked files from the working tree (including uncommitted edits), and installs Linux dependencies in a disposable container. Stage new files with `git add` so they are included. Local dependencies and Git metadata stay outside the container. Test artifacts are saved in `test-results/linux/`; package downloads are cached in the Docker volume `timtim-linux-test-cache`. The first run downloads the image and Linux dependencies; later runs reuse those downloads. + +The [official Playwright image](https://playwright.dev/docs/docker) matches the installed Playwright version. It uses the host's CPU architecture and the image's Node version, so it reproduces Linux browser behavior without promising an identical GitHub runner. For architecture-specific failures on Apple Silicon, compare against CI's x86 runner explicitly. + +For mobile input failures, test the **first tap after drawing**. Rapid raw CDP touch injection previously caused Linux Chromium to consume that tap. The native gesture in `src/features/writing/__tests__/browser/mobile.spec.ts` covers drawing followed by one ordinary Retry tap. Check the gesture producer before changing application event handlers. + +## Watch one run + +Find the run for the pushed SHA with `gh run list --commit `, then use `gh run watch --interval 30 --exit-status`. Report status transitions or new evidence; repeated pending results need no extra inspection. Confirm the run's SHA and conclusion before reporting CI success. Deployment and real-device verification are separate outcomes. + +CI runs for pull requests and pushes to `main`. New commits cancel superseded runs for the same PR or branch. diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..5618258 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,6 @@ +pre-commit: + commands: + check: + run: pnpm check + test: + run: pnpm test diff --git a/package.json b/package.json new file mode 100644 index 0000000..2c703ad --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "strokes", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "postinstall": "void prepare", + "check": "pnpm typecheck && pnpm lint", + "typecheck": "void prepare && tsc --noEmit", + "ts": "void prepare && tsc --noEmit --watch", + "lint": "oxlint --deny-warnings", + "format": "oxfmt", + "format:check": "oxfmt --check", + "test": "pnpm test:tooling && pnpm test:fence && pnpm test:content && pnpm test:browser", + "test:tooling": "node --test scripts/__tests__/*.test.mjs", + "test:content": "vitest run", + "test:browser": "playwright test", + "test:browser:linux": "bash scripts/test-browser-linux.sh", + "prepare": "effect-tsgo patch && lefthook install", + "test:fence": "node --test src/features/writing/__tests__/fence.test.mjs" + }, + "dependencies": { + "effect": "4.0.5", + "hono": "4.13.13", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@effect/tsgo": "0.56.0", + "@playwright/test": "1.64.1", + "@tailwindcss/vite": "4.3.3", + "@types/node": "26.6.5", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@void/react": "1.2.0", + "fast-check": "4.10.2", + "lefthook": "2.2.2", + "oxc-transform-react": "0.152.0", + "oxfmt": "0.74.0", + "oxlint": "1.89.0", + "oxlint-tailwindcss": "1.16.0", + "tailwindcss": "4.3.3", + "typescript": "7.0.2", + "vite": "8.3.4", + "vitest": "5.0.5", + "void": "1.2.0" + }, + "packageManager": "pnpm@12.5.1", + "engines": { + "node": ">=24.21.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b7fa7f8 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "src/features", + testMatch: "**/__tests__/browser/**/*.spec.ts", + fullyParallel: true, + forbidOnly: Boolean(process.env.CI), + retries: 0, + use: { baseURL: "http://127.0.0.1:4173", trace: "retain-on-failure" }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + webServer: { + command: "pnpm build && pnpm preview --host 127.0.0.1 --port 4173 --strictPort", + url: "http://127.0.0.1:4173", + reuseExistingServer: false, + timeout: 60_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..577ff94 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5106 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.5.1 + version: 12.5.1 + +packages: + + '@pnpm/exe.android-arm64@12.5.1': + resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.5.1': + resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.5.1': + resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.5.1': + resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.5.1': + resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.5.1': + resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.5.1': + resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.5.1': + resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.5.1': + resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.5.1': + resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.5.1': + resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.5.1': + resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.5.1': + resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.5.1': + resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==} + cpu: [x64] + os: [win32] + + pnpm@12.5.1: + resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.5.1': + optional: true + + '@pnpm/exe.android-x64@12.5.1': + optional: true + + '@pnpm/exe.darwin-arm64@12.5.1': + optional: true + + '@pnpm/exe.darwin-x64@12.5.1': + optional: true + + '@pnpm/exe.freebsd-x64@12.5.1': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.5.1': + optional: true + + '@pnpm/exe.linux-arm64@12.5.1': + optional: true + + '@pnpm/exe.linux-ppc64@12.5.1': + optional: true + + '@pnpm/exe.linux-riscv64@12.5.1': + optional: true + + '@pnpm/exe.linux-s390x@12.5.1': + optional: true + + '@pnpm/exe.linux-x64-musl@12.5.1': + optional: true + + '@pnpm/exe.linux-x64@12.5.1': + optional: true + + '@pnpm/exe.win32-arm64@12.5.1': + optional: true + + '@pnpm/exe.win32-x64@12.5.1': + optional: true + + pnpm@12.5.1: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.5.1 + '@pnpm/exe.android-x64': 12.5.1 + '@pnpm/exe.darwin-arm64': 12.5.1 + '@pnpm/exe.darwin-x64': 12.5.1 + '@pnpm/exe.freebsd-x64': 12.5.1 + '@pnpm/exe.linux-arm64': 12.5.1 + '@pnpm/exe.linux-arm64-musl': 12.5.1 + '@pnpm/exe.linux-ppc64': 12.5.1 + '@pnpm/exe.linux-riscv64': 12.5.1 + '@pnpm/exe.linux-s390x': 12.5.1 + '@pnpm/exe.linux-x64': 12.5.1 + '@pnpm/exe.linux-x64-musl': 12.5.1 + '@pnpm/exe.win32-arm64': 12.5.1 + '@pnpm/exe.win32-x64': 12.5.1 + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + effect: + specifier: 4.0.5 + version: 4.0.5 + hono: + specifier: 4.13.13 + version: 4.13.13 + react: + specifier: 19.3.1 + version: 19.3.1 + react-dom: + specifier: 19.3.1 + version: 19.3.1(react@19.3.1) + devDependencies: + '@effect/tsgo': + specifier: 0.56.0 + version: 0.56.0 + '@playwright/test': + specifier: 1.64.1 + version: 1.64.1 + '@tailwindcss/vite': + specifier: 4.3.3 + version: 4.3.3(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + '@types/node': + specifier: 26.6.5 + version: 26.6.5 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) + '@void/react': + specifier: 1.2.0 + version: 1.2.0(oxc-transform-react@0.152.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))(void@1.2.0(@types/node@26.6.5)(arktype@2.2.9)(kysely@0.29.7)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(valibot@1.5.0(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(wrangler@4.151.3(@cloudflare/workers-types@5.20261021.1)(@types/node@26.6.5))(zod@4.6.7)) + fast-check: + specifier: 4.10.2 + version: 4.10.2 + lefthook: + specifier: 2.2.2 + version: 2.2.2 + oxc-transform-react: + specifier: 0.152.0 + version: 0.152.0 + oxfmt: + specifier: 0.74.0 + version: 0.74.0 + oxlint: + specifier: 1.89.0 + version: 1.89.0 + oxlint-tailwindcss: + specifier: 1.16.0 + version: 1.16.0 + tailwindcss: + specifier: 4.3.3 + version: 4.3.3 + typescript: + specifier: 7.0.2 + version: 7.0.2 + vite: + specifier: 8.3.4 + version: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + vitest: + specifier: 5.0.5 + version: 5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + void: + specifier: 1.2.0 + version: 1.2.0(@types/node@26.6.5)(arktype@2.2.9)(kysely@0.29.7)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(valibot@1.5.0(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(wrangler@4.151.3(@cloudflare/workers-types@5.20261021.1)(@types/node@26.6.5))(zod@4.6.7) + +packages: + + '@ark/schema@0.56.8': + resolution: {integrity: sha512-R05TOe85oQBGFFw2zKMuZpqkXibLKuC/aJt6VwFn5Cjc4x3NkWfhvGRpRDycNkj3ddcHnOPtIsmjVowJO9Yxdw==} + + '@ark/util@0.56.8': + resolution: {integrity: sha512-XLiQyGfpJcws1zCtp2Cva8s1oheCdFn1Rt6JSyC4NnkDrpwCJnlaiawuuOc1Z97vMH+jEzW5vyyf0xOju5wUQA==} + + '@better-auth/core@1.7.12': + resolution: {integrity: sha512-SUStBGHJ13KnSO6+7U/1DSlfuOtln4jtOrFZqzQtxZVHZIrAoMjwHNGitlF+s7hvKbuC3Z14xjnqfLLqIqGfxw==} + peerDependencies: + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@better-fetch/fetch': 1.3.2 + '@opentelemetry/api': ^1.9.0 + better-call: 1.4.4 + jose: ^6.1.0 + kysely: ^0.28.5 || ^0.29.0 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@better-auth/drizzle-adapter@1.7.12': + resolution: {integrity: sha512-Qzoyuf05aK01Pc0k3EXv8Xj3XCT2m2dCWc5HoB7x+xX6N3Pm2eQsca+UIArsJcbuNEWWP3L51oIebnBoHJ5DTw==} + peerDependencies: + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.7.12': + resolution: {integrity: sha512-1PaFXktLv4n3DmrGbPaCj5SsvcT40NnWw7NLua5/J1+8/A051N5ZzqOwJifXEIQX5k6NdwvmymwDQxKytduEAQ==} + peerDependencies: + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + kysely: ^0.28.17 || ^0.29.0 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.7.12': + resolution: {integrity: sha512-glaDwXzDptnddHqRuu7fj47FAssUcOzLNgdlTGJoEPrspkX69x2vnXwgAzNuApjZI21WLtPH/rQ6CN+DQGybPA==} + peerDependencies: + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + + '@better-auth/mongo-adapter@1.7.12': + resolution: {integrity: sha512-HKV4sgYSw/Gdt9AuHlESMkiKL72SWdGweDy1Wsyxoh06BpG7aNu4aSJ7marNpMdKZteyRbxWWqQkA9ediA8qFw==} + peerDependencies: + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + + '@better-auth/prisma-adapter@1.7.12': + resolution: {integrity: sha512-FZRX6ORbwhDmeYE1Tl0IPtUn8v3BlVFgbJxDnm/DCNHxuFRf+0t4HfddUgYeYehHFR8aSDsdYX44fEFWM9877w==} + peerDependencies: + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@prisma/client': + optional: true + prisma: + optional: true + + '@better-auth/telemetry@1.7.12': + resolution: {integrity: sha512-DaDkKNnI99vKGGbWq9KJg2uYeaOjsK9nIBw9T2m2KG/RkxpvQRjYkZ00++up2bZQkR5pB1nukGbplza3UJG/Xg==} + peerDependencies: + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@better-fetch/fetch': 1.3.2 + + '@better-auth/utils@0.5.0': + resolution: {integrity: sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==} + + '@better-fetch/fetch@1.3.2': + resolution: {integrity: sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==} + + '@cloudflare/build-output-utils@0.9.3': + resolution: {integrity: sha512-0MgvIq6UxbRWLwB2QJU7qjLqpPkfygOebN3UiENhdp+z6zpRKrNnwkms2ynadmxCOh3sL2v+SI0/pNXxgXdteA==} + + '@cloudflare/codemods@0.5.1': + resolution: {integrity: sha512-8pAR4f6XhcA+194Oov82bDirSC/k8g0IXoIl+1ej3d7PUS3CPvlM2E4uQSzusiUbSS+4vTLP2RZA16vWuKIPAQ==} + hasBin: true + + '@cloudflare/config@0.27.0': + resolution: {integrity: sha512-wNnL0z4lj5Gm2fze5qXTOvRPbUOi4jlMmwPk7ArCUakI5oVxgjfl3Rhnfd/Mmzo3g86x/ofH75yPIhsa5msqdQ==} + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/runtime-types@0.1.12': + resolution: {integrity: sha512-eYF5teHQ+Jss4sbSVINM5tNPHxo/kJ2gKcMs7omyy7gO9PEkgUbGPZCYl06SxbBWTcJFF2vrX4epB775QTE4BQ==} + + '@cloudflare/sandbox@1.0.2': + resolution: {integrity: sha512-JPPASKBQ5PE1XLy18zzljh3oIyDleu8lK3jW/HaSGq+eh9aJMl5zwAgobuP7I2FLuRMlqA18te5MIPEBF56jlg==} + + '@cloudflare/unenv-preset@2.16.2': + resolution: {integrity: sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/vite-plugin@1.64.0': + resolution: {integrity: sha512-h7Tunx9Ou5fUoao8O9jYTH0ParfrtNZzZVPA0IS19O7AoWA1SpYjwzDA6pZC78qzuV8sQSEOpYCN/yTShmd+gg==} + hasBin: true + peerDependencies: + vite: ^6.1.0 || ^7.0.0 || ^8.0.0 + wrangler: ^4.151.0 + + '@cloudflare/workerd-darwin-64@1.20261012.1': + resolution: {integrity: sha512-dkTL9aY3V4D40b+jBpGPCistWUPFQ/M61wlWi6i1o8MvHFFgEw7EQkcaYEv6Yd3bkPDD/3TFT4hRsMb2xVZSwQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-64@1.20261019.1': + resolution: {integrity: sha512-5ZILbAta/fBQvhmAYeCE7dKMMYIr7N9wJEX50fEGLKSIwGtvT0HzSfHNIK/O7cBQ4gHQ2WfIiPClvwcye6E0fA==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20261012.1': + resolution: {integrity: sha512-2/nVGcKXjNNI+DhxKSepTKvFng9H7aN2WXv6Zu67iqgI6JrxypNt5kTPv4ZHosxrcfyKMci/2RfN7GIrN0BitQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20261019.1': + resolution: {integrity: sha512-cww2Ab6lVMxpTiEEjKmPI58p5EMo0vYTZSH+3WkqXJdQgc3ES66GuL247+U2B2YWajhhu2tqj7jySrXneU749g==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20261012.1': + resolution: {integrity: sha512-uMe7ONHnLxStD1Ye0YFHBO36uj4cStv5P53rW+5GVWXFT8BRjqVixlH0IqCAHe1Pmcwyd1+7fVoGde89MZSCJg==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-64@1.20261019.1': + resolution: {integrity: sha512-4++VRi9vkQROYqrpZYOixIJuVDmujkwrT4kszsxNFlvcmsX6Mr2GumvxIN3gIixS3vJUh3Umdtkthqj5X0S3Xw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20261012.1': + resolution: {integrity: sha512-I0VMdcXk7ygG5vYDce0ePTBFdnBXC0i9WiBY5mWiv0iTinbfea6/rHqSQ7B1UMpjgrYVs9E0m4bZ9ufsk5GCFA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20261019.1': + resolution: {integrity: sha512-75PVtJOwfjScQ53sbLtQXI7D8yP1c21t+WEUPaYbqkzaz4ZqfR5wAd+zuWs8G5Q7kOj2SQIvEfY5Sn8Y+UVrnA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20261012.1': + resolution: {integrity: sha512-c5hQvNx574UEoknaCFvMa/NdLrrYiALNbe6sx1okW8uOiRYWxrFuA8WpXFnBJ5XJypm0TEAHYaUTGv3rRLpY7Q==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workerd-windows-64@1.20261019.1': + resolution: {integrity: sha512-z49fvKTETMCnG6wnMcQwxx9CpryFceiKz04FRjq26EkhCyzRNJ6c3UXhyZz3r5f1CXnnBZc5DcU8ZouPkUJCqw==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-auth@0.14.1': + resolution: {integrity: sha512-JghUxus3O6IJW8UcghCUHBvVSVEiGVQ2ESQ2GJw/EZKX8K/rP6vwzq2VT59bgWY8w3epdzYwlm3INrzFJQCACQ==} + peerDependencies: + msw: 2.14.6 + vitest: ^4.1.0 || ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vitest: + optional: true + + '@cloudflare/workers-types@5.20261021.1': + resolution: {integrity: sha512-HLu/pEx2488dO13gQoLaLmMMTcah/NqEn/c/6c/sJV9nyBY/CVQAI9Zj+YugKacrxENDmIVPU0HKIK+mU7WKzA==} + + '@cloudflare/workers-utils@0.47.3': + resolution: {integrity: sha512-Eez8j60Iglt7Yyca+vsGy5HEX0+DynkW4mNG7YjLePezh+J9OOG18XZFeQsZh6R9R27/fL6jcNEzW5SZGEiHCQ==} + peerDependencies: + vitest: ^4.1.0 || ^5.0.0 + peerDependenciesMeta: + vitest: + optional: true + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@effect/tsgo-darwin-arm64@0.56.0': + resolution: {integrity: sha512-08yYRU3OOw/jKzy582EF741xKvLJ2HK6GO+L2zuSR97b61ES7fER2fxg1xp4mv8u2Fc3bBRvk7026GzdKEGFgw==} + cpu: [arm64] + os: [darwin] + + '@effect/tsgo-darwin-x64@0.56.0': + resolution: {integrity: sha512-UQvYvVaam+EVMYm9gfHBx1WpKwLlW+ptjb2UD7qGhZdg+GzxMbl26AhuuqV2t/dN9shZ6EcFE2+++ThISMp/9g==} + cpu: [x64] + os: [darwin] + + '@effect/tsgo-linux-arm64@0.56.0': + resolution: {integrity: sha512-zzzo/w7lLqiCMuJOI+jbjIuhHob3ccmth1bD9m1E98ukMuHhIqQMOXKU9EreoQkCGeoVrxGqHbXkl6E/+68/Sg==} + cpu: [arm64] + os: [linux] + + '@effect/tsgo-linux-arm@0.56.0': + resolution: {integrity: sha512-3WuPwP8vQPqKVpfGmKlnguuAZb7NGUyAhIHQjZnaINU6Aim7WczOy2WOmKjDPSKRlonU2WyYT+Q5T3B7f0VTLw==} + cpu: [arm] + os: [linux] + + '@effect/tsgo-linux-x64@0.56.0': + resolution: {integrity: sha512-R0d3dGf8qKPtvjff+J9HL0UjIBIES6xZiU16rMMezI4K52b+DGQkaWi1OX3tSgGivjlV+NSN7UhPJ2pYKufglw==} + cpu: [x64] + os: [linux] + + '@effect/tsgo-win32-arm64@0.56.0': + resolution: {integrity: sha512-uVaLdXULUZx9U30YxlVu5PBiu+1a+9/UT2Ex7jQpIasnJy1CHWlhMbfCD/+7Ez1vqG5fLOvUESoQu5XSjs3Lnw==} + cpu: [arm64] + os: [win32] + + '@effect/tsgo-win32-x64@0.56.0': + resolution: {integrity: sha512-Cf6SDixFTkp7Aoeg2eRwJps/2BG9lvx2m+iVRjUzxdqkTbCCNKmmOAIyDslNZm2sMwaDUw/iN/BPGqxx7xMxww==} + cpu: [x64] + os: [win32] + + '@effect/tsgo@0.56.0': + resolution: {integrity: sha512-/S5G8RJjtaVjE/RhwFNTvWQwFAQyDGc+3Ig4Oynvlo3gJu8le4MUgM/DBjoiwkwBsQ+BhufGDCBpwojplerGlw==} + hasBin: true + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@napi-rs/keyring-darwin-arm64@2.1.2': + resolution: {integrity: sha512-vsInUWUzWS6/WzR/TFNwmdtmwKWjnnR8TOpvZ/pjjxDAkJB/waz37PWzp2b8AFcw1aCcq2aQ7k2vvbLQy8pTrw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/keyring-darwin-x64@2.1.2': + resolution: {integrity: sha512-wQihYroaQ3r++R6u5c9Gj0TCw1l1F53LCb5aLOXfVvT4DsnDnpe7p7VGYo4/33bLe0iqVevtAj+ET3yKCtOlJQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/keyring-freebsd-x64@2.1.2': + resolution: {integrity: sha512-Ve5ha7MbJsq+4TYyXYiTy4IeZOnYxkBB05uH2+nIrEKBkHmyfHttF814EfWAfoJa63KqBhYRXdYJo0o1WVL2sg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/keyring-linux-arm-gnueabihf@2.1.2': + resolution: {integrity: sha512-zxijTnVnTApEyJMjgnLdVQL1g1DAudku/5brrWNDRWj4J0q0nFhUZEtzUA+nY2FvgDjl4s+2YCZYXSTH4TFMUg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/keyring-linux-arm64-gnu@2.1.2': + resolution: {integrity: sha512-PM+SAcl/yGzgGbeeCsKrnlG4T5DBIDjmDqtKm5an1bJz0o0gwSzR3BMiXf04JenkcqascZpdcIReoEvLj0cvCw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/keyring-linux-arm64-musl@2.1.2': + resolution: {integrity: sha512-+ERS9oyInPCALN957cViBDbYJc724/Q+rZ8ZAz4rOHj4B4v0VR4OAWThdYZgZqD54sZVV0MdyX24qva97hYoAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/keyring-linux-riscv64-gnu@2.1.2': + resolution: {integrity: sha512-pGADtqeT3U1SaFZGjkVsmZVjeNOQx0fr77S+NWszXjZboyaUOLlGKgqG6ssAWzcwxv8uPhKc9Ij17ujNZhSqLw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/keyring-linux-x64-gnu@2.1.2': + resolution: {integrity: sha512-Fj5TmTnl2oriuCT5DDEYlhgxYes20uGfgOg10LK9PyPVsI8/PQ1m8I4rq/4A5hkrMtY7wvFb70YmX/yPHDYWvQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/keyring-linux-x64-musl@2.1.2': + resolution: {integrity: sha512-C+KhAeU6x2A43FV3dOmMvUraJb8zsC9VZfsunjZ1cgtuAb208TALaUuAY0jThOphXqhyhBJOs2xTCQhOrRkJIw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/keyring-win32-arm64-msvc@2.1.2': + resolution: {integrity: sha512-kBq80ZZfjXoa5wHG9kgAVhYYjaPOLgHh5ngt7r1ftwFXPXDPXKH7oM2r2j2Q64cs0pVtmodVtn5Q9e6xCwzQUQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/keyring-win32-ia32-msvc@2.1.2': + resolution: {integrity: sha512-bhgKvVbWlZ6iR/tmvb2ht6/o0OzarUX9W7hjg+G+eVdGjR1prwtQ9lYo5ZESdn/+zyF1MIFvHkOCGja9YqDcww==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/keyring-win32-x64-msvc@2.1.2': + resolution: {integrity: sha512-yQtFdfuwqM0ezKlYO6r2n44in2LGzd4MC679Ddhvojip3t1hn+q/4I+gJU+cIJQVd5FAZSKNrK4tGfew3UTpAQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/keyring@2.1.2': + resolution: {integrity: sha512-9i1SR6tYeQ3o1beLeVMxhN6Fq513m9TZlTs+46+4JgvSNkaFoW9S4WrpV5MjzE+0CzVoC+dZ4RfZDIgW6+afnQ==} + engines: {node: '>= 10'} + + '@noble/ciphers@2.4.0': + resolution: {integrity: sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} + engines: {node: '>= 20.19.0'} + + '@oxc-project/types@0.154.0': + resolution: {integrity: sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==} + + '@oxc-transform-react/binding-android-arm-eabi@0.152.0': + resolution: {integrity: sha512-HrGZlhhyJUxz5tDWnsBliNrlhSVxM9lLF468EKab1uBBUMbMzfwHdO4aOHbI8x4EbI/gIzpMXqJbtWRwBLdwZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-transform-react/binding-android-arm64@0.152.0': + resolution: {integrity: sha512-h+LtkA/nSQsfZdCe6c3F28236gIZhpl3Bu9MjwEI7X0J+CGpEEShmZuXw5b7lwqXQnhr5l1ZIIZ4NA5tU5FW+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-transform-react/binding-darwin-arm64@0.152.0': + resolution: {integrity: sha512-EycvN1vJ5gjer1+AD4Czi/Z0gpD4ud/oRbiwDV3BDOiuN5PVrR5Dz0oqkoKtE+/eKK6QqygEtZrZzc8ARJkLLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-transform-react/binding-darwin-x64@0.152.0': + resolution: {integrity: sha512-5YUXuvxXi+7GmZPho95qteMlG8WgIhCbpYOug8MhyuTZzYtrr8CDBsdMzheBgiRNj9lPkFvduhQ4MK+QVQ06CQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-transform-react/binding-freebsd-x64@0.152.0': + resolution: {integrity: sha512-HZrLNspvHfOo1umeCkeskU7UBEXWjuQSoBGidHwjUQVOhsbDNp9k3oihXenEBFgShPycLEC11wAZSD3DtiIkXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-transform-react/binding-linux-arm-gnueabihf@0.152.0': + resolution: {integrity: sha512-qfPsGd0sbaUeLtQDojSf+reHYMBHWX+4mdq1lsARkRzGjoHCvstnTr+nKb7sTtGsdPYhbSgbPINI+h1V+KSF5A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-transform-react/binding-linux-arm-musleabihf@0.152.0': + resolution: {integrity: sha512-lYpuYevetblWvNsylYb3BxKbXd2t+BhUqwIoa3/98zmWeLbB+VBfxV8APdZV9gbw2W04HknXKU386pt/5duvfw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-transform-react/binding-linux-arm64-gnu@0.152.0': + resolution: {integrity: sha512-8eIPBuC+1WpE3Z7AiieAVhbkt6PLEbT4Rnm4SeVOMvIUKTml+s2PSzMu2qAUtFdIB4SFVshTj9SliturT3GHCg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-arm64-musl@0.152.0': + resolution: {integrity: sha512-rv4dvLnqhgen1tqb7eoB/fNfD5fq+Bf3RTH9YuJ+8SNdqOk2TjAXFUdve0XtjD9Wost0fQ605bVXVrStVY/IEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-transform-react/binding-linux-ppc64-gnu@0.152.0': + resolution: {integrity: sha512-zP9xCGiJFyHkP6B7UtVQ99nfYXe8e7eqLqZv0C+Zr2jlhNuLMBsB77QuRczlhtRQqncRWzBI8BkQzy/Lo7GyaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-riscv64-gnu@0.152.0': + resolution: {integrity: sha512-INC57B7OIgMEoUe4NquHvg77tlYL4E6aMglhKs3FY5k5oB9/+hd2y0Xg1DVco5uDtkgilnSQRM5P0Ptz7CxXPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-riscv64-musl@0.152.0': + resolution: {integrity: sha512-4oPU+QTufHexk7GOGe/JapxAfYlU5EXUsN5J0IsBC9s3SsGUTTjpmXQYiDe4dkX5/BGerE+9mYDG94jNwuk99A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-transform-react/binding-linux-s390x-gnu@0.152.0': + resolution: {integrity: sha512-xt4qy3y+9s32F/pz7ymaf1VfDRI+cKwDnW2poFID2r51dC/gln3QGGDaqv2XLaZGQT5o4uGwIry+9CQY5AHNeA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-x64-gnu@0.152.0': + resolution: {integrity: sha512-6va4CIMFzejx2i8KhLAYuunai7b85vRpZSLvuBR4bK2ywuTb6V0kJDFJwiKjWHQRNzEvRjbl887+jqa1rKl8QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-x64-musl@0.152.0': + resolution: {integrity: sha512-l1QfEbsREILsAWIcmpEsEu9PKjmN6BKmE1Oe97QJDUjeJ3z5lBn5g2feC3nof3JJc5SsG8BiY4F+cm1yHRhmeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-transform-react/binding-openharmony-arm64@0.152.0': + resolution: {integrity: sha512-UmdDypmnG1WSZxzQXd8GfhbZQq//XsvMu5/mm+I1uCW3v+9UwpD1J7H+jg91W4xtRY1c0qAhUoMsZi+6vB7Qug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-transform-react/binding-win32-arm64-msvc@0.152.0': + resolution: {integrity: sha512-JgETFwNiqqB0BfTKMamnVF1zBMnburMoeqnyBgWwKj+JdEqgFqO/0G/O+eWUfNW4xPryjh6HGuq9eNHZLRYrjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-transform-react/binding-win32-ia32-msvc@0.152.0': + resolution: {integrity: sha512-j2oFkpEG89D38wVY43OtU1s9QUmq0C1+Dpp62PB+SDDTAMsUwU2HofnhK3h9iYTAs7wH/6YqzWOM4XjQ6g/Nmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-transform-react/binding-win32-x64-msvc@0.152.0': + resolution: {integrity: sha512-iFxYqUqkN/8D0migw/RVZoEvVcJhZ1NzJuRBal0Fgt7V1yJed0QQIHd719kL4OrVmX5f5u2tNZxJB1UJiQ0D/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxfmt/binding-android-arm-eabi@0.74.0': + resolution: {integrity: sha512-xxQwrf5EPI5zxvuCTW8r6tzCTjPQ+h6+HqJ0iRKRy2zS1yBllt/xLVjoIyUc+FnR7g0a5g1SZv2yyDYhTItLbA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.74.0': + resolution: {integrity: sha512-FJW3sATVvrWbV64al8xofCPZj4500Z/1W0ad7Tg1rcCUyh0EMbuSrsf3jsZS7CEbwQ5fQ0mRhmkc129GFgofSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.74.0': + resolution: {integrity: sha512-TfmNKBD30dGyMu9GCLiaw2rt/z7fms9LS+5cKcNOyWst0QgAs8oZs6wPxMbVaaDVE0/PCdCosgWM1vbu6f5NUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.74.0': + resolution: {integrity: sha512-5Usf8+DfC4Q3o/MD8FkN47GrI6y0KbhQ4KhZ2HGzMZcXjcW1AeldwbUn51aKih+1TVuJFF9qkNm6X1nzm6NYng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.74.0': + resolution: {integrity: sha512-eL+1O3gykw1XihbZh2MRAciQvU2988LByrnjhyo/dmNG74X4oZ7XzigRoWPRFLfJCp8hzRkp/LbEhwe7uuT2eA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.74.0': + resolution: {integrity: sha512-Xt0QtO2hJ4stnul8IL4LJGVLyp0xnc6ccN9fZ30yuubv8FWSRYxWwd1Za62KsEqAfmPSazoS/cLSUUJpja6npA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.74.0': + resolution: {integrity: sha512-/4wYQ0gZl4vUR0QoBTGaay73FmQ1GAjUa9z9101TR2xbGNrg4a+grazlR0yzEOe3/E+PJr5BGfj66MMzm/54vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.74.0': + resolution: {integrity: sha512-846bKzF+HJd+JkOFh3rxzmYvSq4Cm+GMT+p+mW7S13ZYo6GAAGv8bL8cAOi/XcdeMqxEJDCIMTVQLVBXHMCdig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.74.0': + resolution: {integrity: sha512-nI5FJwZ4Flo6CBdscpwxymZXxO3m3IErQJX55ZS6h+ObfGXu8FostL85L/RWPFaCLnIVE4/WfLw+2q8XvVEeUA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.74.0': + resolution: {integrity: sha512-BoQYKuqUdsKAFAkK6DLBvkdv4+t0hppSm79sB0wIcE2o/eOwNj+6Z3OlKY4q3Eta/tlssP65FaS9vHn8TeF7vQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.74.0': + resolution: {integrity: sha512-FSroRrM12+jCdSQklF9aOxsKpI6wFs4ZCo/1cSYG80RWZS6z4LV+l9tXRMT8lDGu+yohIZBwi9M76FzGPFDE2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.74.0': + resolution: {integrity: sha512-0VLQD0dhGBLY9Ik/w+otMvD81D0YXi4VDwql65macV+O9UzmSWmUr6CKf+dMtysDeC0PnG3y6kBkRO08BJx52g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.74.0': + resolution: {integrity: sha512-6X7BbZNz34+OHM6Oqjf6TRcUyRn6aRcB4VtIDZAF/uQRyeVhg+G2hv6MlMvmnRekVSyTJ/8rG/u8W2dRFW6glg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.74.0': + resolution: {integrity: sha512-4kYIQK53yJXrxwxSzx8H2bojlf6/KezLGYLRXzV9QCCkoiCsvBTCid9Bg83dUPTLPa3g9K0730f6MKDbck1+OA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.74.0': + resolution: {integrity: sha512-MfWz4wDREjp/JQDmuxtKieix4FHGa1z5rBvB3betdcPwZvf5fqeQrlCm1mmoyuUVQOh9Pt3M61HwN94kJCxw7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.74.0': + resolution: {integrity: sha512-6rmAApkq5s1kbDjvNI6Nl5A+JqQkLORb2sR941pf5HWsrd01WGKyagK/B4vX9+kh1FfvRkNgEu59FHvVk4H8cQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.74.0': + resolution: {integrity: sha512-+5SpRUnZNGeXTyumHnpHa791tQtt04qZs076WT8eTcGF2OJ631VLnf7S0kQSPuDG+PtJM0phadEB2WAr5n1w9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.74.0': + resolution: {integrity: sha512-1Fg+wYBO3Dsti1QX+/6EYHjeQzLC84tLOAqPu/r7ah3UJlUs9C/DB703Ay9Vcg+8VMzqZA2P5VGhzJraVUD4OQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.74.0': + resolution: {integrity: sha512-y9jXUgPBm2TZIWbQhMYRRYAQM57+r4fbykgc+j24ICIIZl+pcuoNhnq1CRGm3oFiHnEUnZO00qOtSPGQdJ2ziQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.89.0': + resolution: {integrity: sha512-Pflpba5F3gRAQ12sWglRxvWTHEdAusNDvmi4Y5N1T5SY1AGkM7AixOnET8qkrnLJ/F32qAlNokO6eCknT8tmnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.89.0': + resolution: {integrity: sha512-dYmyD1x7LaPlb79vZWUcpTShk8dI9z1I+IubtqrBxWNJaDhQdPHwcsvg7q5StTU4xWyd9AwPfLaTYFZY8NZgtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.89.0': + resolution: {integrity: sha512-Z/oxOPiIqiS7fItRdxaaOEC+6GD3OpGi0IxoR0dMefzQHolsKLzScw2DNcu/5gUPok3tv+G9mslvYB5bo3i0qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.89.0': + resolution: {integrity: sha512-eAjTkHSjAxrdCPJxO6y5sPQL5pYLKb2kvxZz9UU7tXnOZk9I/HLtkvx9S7GHY883k+AmL/eC7mqFt+w7WCwk0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.89.0': + resolution: {integrity: sha512-0lNCA9Tl1n1FKpqy2/9ckp0kIuSrtJ/l9bMRgaEhOtM7TsOHshFuwOcvW8oA9teXLMOJT61LjEKWiaHDFGxF/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': + resolution: {integrity: sha512-OTFEvi9W+JczaZD1Caj17SGCoW8H7TE+fvx0D7ATEWa1Lohn91UMxFQ6r145SiGy/yCxeIwptMys/RAM7gSVSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.89.0': + resolution: {integrity: sha512-fRxgNHvBLswj6MrhW2uBq5UDhPRQlCbHeq/Nf8yuG9zOxhDsXqX/mi2PX1axgsQxMm88el7HLiJAzglAnflYxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.89.0': + resolution: {integrity: sha512-v9ENzHvwrQoYSWTtviH1pOYc7R9UovDxVofnck50cNmPihRRTcrg5X0UlRbLOZ7IVtxPOlh2hb7kDO+FpE8SfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.89.0': + resolution: {integrity: sha512-vfffe8mQ484AzVadlUOFpgL+JDwhvs9hrzxTgQP1FVfi4ZEUUYeffXlzC9dFV7Dgnu5s/KrNKvfmG6HxBd/ISQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.89.0': + resolution: {integrity: sha512-Buonlp4hVS7RGNP5M7bPpGNQrenyzA2C64JcO/oz9C1c6dGOXJ7VEAnaiAMY7voou0JYyCJdvDVlFQc5DZX3DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.89.0': + resolution: {integrity: sha512-1eVwJJrJ3lYIPT+cm7BcBBZZXY6IQZP/oyonaruTor/MrVyWOpvnwt1oD5CLdsHg5aVBIxXodQSvt+1gK6ck5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.89.0': + resolution: {integrity: sha512-fRQQsKUGShdEL5Fl9o9uhj3grkZbeXKLv0icQ5tspUCNYSDy8wzzEcP1WC2bKdTQ0rSszbKlO3iiHORxV22DLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.89.0': + resolution: {integrity: sha512-dEtYjIy6+fHcjKklsmtmMkZRhwHr2UbzjmpvuPtQYSeN9awQ2hx5JLms5Wvri+8P/NZtD8v5hfgHALUql+/Ycg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.89.0': + resolution: {integrity: sha512-hqjwIj5KOYM8Wjef/p7IruNi1iByLhSzbMBk62y22P/E8q7Jj0Pp+DeUTKTRYyZYCDtU1wEGlnWOnttHbyNIww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.89.0': + resolution: {integrity: sha512-iAQN3e6o9UgUCcdGwqRx/jYcKbG3HK7ur3rH6lTqVcQeNGZ85AeWIg6cgn/edwFQ0ofEZZItMwkzyCVTahnvEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.89.0': + resolution: {integrity: sha512-MNxjEByz3N/7Q4qFQuPWvc14TpN9LoD3pECEDsJTZbP/wyB7U+hduUXEl7QAbbgVezFwsb9sWtNUO6XVBndOhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.89.0': + resolution: {integrity: sha512-xxe+i9wfuFso/VnoL4mfj0UlzE+cfCFiUB06vkXCMyQdgVVmItF5FfAF4tATAktSU6uHruWR+FjqvAT9cHZZzg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.89.0': + resolution: {integrity: sha512-saFy07cO94E664glwgLmKLI40HgTL1PY86y4T6+ud4niX4H0Kdqjcmqmjps7mQNdBytV5guGbXDdyQtRfeLbLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.89.0': + resolution: {integrity: sha512-hLFJ/o0f/dTJh/EW0vmcbREYSQO9tYgfiAAlsOkiLFiJuRRWvYtFELUXN5o2ijpSo/tlO1n+YlWN58tZwPG18A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@playwright/test@1.64.1': + resolution: {integrity: sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==} + engines: {node: '>=20'} + hasBin: true + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@rolldown/binding-android-arm-eabi@1.2.14': + resolution: {integrity: sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.14': + resolution: {integrity: sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.14': + resolution: {integrity: sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.14': + resolution: {integrity: sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.14': + resolution: {integrity: sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.14': + resolution: {integrity: sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.14': + resolution: {integrity: sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.14': + resolution: {integrity: sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.14': + resolution: {integrity: sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.14': + resolution: {integrity: sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.14': + resolution: {integrity: sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.14': + resolution: {integrity: sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.14': + resolution: {integrity: sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.14': + resolution: {integrity: sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.14': + resolution: {integrity: sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.24': + resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@26.6.5': + resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-react@6.1.2': + resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.152.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + + '@vitest/mocker@5.0.5': + resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==} + peerDependencies: + msw: ^2.4.9 || ^3.0.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.5': + resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==} + + '@void/deploy-cloudflare@1.2.0': + resolution: {integrity: sha512-itHcCLtlVWi+B1ZLt2X9uxesTJhcN9GE4Nor2nrUBv4njlz9UPaxfR9Uedkd1BlDCo71y/MU5vsLWmPd+hLs9w==} + + '@void/deploy-core@1.2.0': + resolution: {integrity: sha512-Xx7qKBXGhWgz/TUxDW+DozSboNTcoNHY5X3A3bys+7drBFDUbFiI+3GhQV95FzPNDivYm6R8W7yTKaHs0KWUiw==} + + '@void/edge@1.2.0': + resolution: {integrity: sha512-YFc8bnWi5xZw1YLibz6OnqQw9lavuKcioSNFjZpPc5OCoo07mnrcqcxqxyuBEDihvgB9HiLFfaPZDWqm0I+Rhw==} + + '@void/isr@1.2.0': + resolution: {integrity: sha512-7bC7RQ3Z4wggwPnmk/YYok0sh882xLYS+2lurqrG0IZnillIPBIpeNsdqKCU8/A3qL33D8rCGjtJkBgT1s7Pvg==} + + '@void/platform@1.2.0': + resolution: {integrity: sha512-BTM6Cm5+2nCu6DEKh8fEwYl4J0KrrbLwcpIhKuK6iEGGN14MgidbNzC9TNVG5eCIUarE6AcFo1DZPKqDcjHm3A==} + + '@void/react@1.2.0': + resolution: {integrity: sha512-VrPE96gNHRQDE479vawlxsrdW3d7H/c+gWPcnwnx7azEcCfDLJL6Y5wUSctoBHYXJHDtrYjzSQIZkagBTbogOg==} + peerDependencies: + react: ^19.0.0 + react-dom: ^19.0.0 + vite: ^8.0.0 + void: 1.2.0 + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + arkregex@0.0.14: + resolution: {integrity: sha512-G3I/uCtWE0IKQiPh0qqmmAFDdtjZKZQ5mNPR6gJBeAMZ7VqYCpoaDeXnLju2Zo6ywExtmxZJELeVFb0MPDjPyA==} + + arktype@2.2.9: + resolution: {integrity: sha512-Cja0IMEn19rB35on9da1i1z6QMasuS7ULffXbetCry09jvCGYT8Bi1KgYiqri5FymF4nQAXkM90yksYkgSK2EA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + better-auth@1.7.12: + resolution: {integrity: sha512-f1fEG0QdTslzbmFA+vOGFFW/XXCWkG4kBxLeH9n1vp1otTH9+ZzV6EJ6797GStWeiYYgS5Vnt9xqE12KwSyAAw==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 || ^3.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.4.4: + resolution: {integrity: sha512-Gq4XdGFy4dmUxFEAX2nj0ycFcFbD9ZWh8YROouZoyXeF1M11TpYbplk0pDByTFF8Wz+MYa6W9052c9m7dD+mfw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + better-sqlite3@13.0.3: + resolution: {integrity: sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==} + engines: {node: '>=22'} + + blake3-jit@1.1.0: + resolution: {integrity: sha512-5lNvUdIw2roA6LSV39E1mDXN8WVK8kC0dGZtaZoErkVktSgUjgRueTv4WVqHIUMHGerkfhxBuqFtMNuo/GdLrA==} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + cf@1.0.0-beta.18: + resolution: {integrity: sha512-SG9NqB6xEZf/WZm6+E2v0OIY5JA0b9DUgl1VOOlF0D5Y+Nk5PyhHonJOd6K2/g6f261h+Z0FwTEK69c9bfIq4g==} + engines: {node: '>=22'} + hasBin: true + + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + + drizzle-arktype@0.1.3: + resolution: {integrity: sha512-X66GB2pz7Nb+NmCZefDXpdoglxjGYnB2yRU5umAK2stVkl4rvV6i6XbMg1+w1HiY/ydC8gJVq4jKAARYazpb3g==} + peerDependencies: + arktype: '>=2.0.0' + drizzle-orm: '>=0.36.0' + + drizzle-kit@0.31.11: + resolution: {integrity: sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg==} + hasBin: true + + drizzle-orm@0.45.4: + resolution: {integrity: sha512-NiM1YZpJ2UxyeE67DpDDZ+Tsw4YLX3trSlkNT7Tm9eKoUNb1LvaAaQzXgYQsWs+AQ9TUmmwMmBOCXuqqxUWKKQ==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@netlify/db': '>=0.4.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@netlify/db': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + drizzle-valibot@0.4.2: + resolution: {integrity: sha512-tzjT7g0Di/HX7426marIy8IDtWODjPgrwvgscdevLQRUe5rzYzRhx6bDsYLdDFF9VI/eaYgnjNeF/fznWJoUjg==} + peerDependencies: + drizzle-orm: '>=0.36.0' + valibot: '>=1.0.0-beta.7' + + drizzle-zod@0.8.3: + resolution: {integrity: sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww==} + peerDependencies: + drizzle-orm: '>=0.36.0' + zod: ^3.25.0 || ^4.0.0 + + effect@4.0.5: + resolution: {integrity: sha512-BY4/28NlpOd99yzmV741xbVGmp+1U7tks4oB7F/Png64bTa5tm5eCA6W0RiXsnuLDDoJpjfVSo4Dnq4BZb/89Q==} + + email-validator@2.0.4: + resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==} + engines: {node: '>4.0'} + + enhanced-resolve@5.26.0: + resolution: {integrity: sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==} + engines: {node: '>=10.13.0'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-check@4.10.2: + resolution: {integrity: sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==} + engines: {node: '>=12.17.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@5.0.1: + resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} + engines: {node: '>=12'} + + hono@4.13.13: + resolution: {integrity: sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==} + engines: {node: '>=16.9.0'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ignore@7.0.13: + resolution: {integrity: sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==} + engines: {node: '>= 4'} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jose@6.2.13: + resolution: {integrity: sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + kysely@0.29.7: + resolution: {integrity: sha512-fmIq+7kv4WbFm2tcfznRzikqB4cuNpgZ40PSU2fAJ2/OO5XhojqnTPrmgrCjYSRJVwBln03XcRXJGnw9H099IQ==} + engines: {node: '>=22.0.0'} + + lefthook-android-arm64@2.2.2: + resolution: {integrity: sha512-wb9rQQFLX7u1dJLxvMrq9VUfMYi5kxM7tJQ6Y4mWEOwaU+SmIm2wjoT/nromKq27Y8fh2Qd7FgCD3G1wfW8CnQ==} + cpu: [arm64] + os: [android] + + lefthook-darwin-arm64@2.2.2: + resolution: {integrity: sha512-6V8D3bLnD/60Xhqn0o2EQD6H3Fe/U9LGkt4WF1O/3eBHbux+5ZJqoH0hfrC+Bi9Dy1Ve1AWxiAY7U2asYl8qVA==} + cpu: [arm64] + os: [darwin] + + lefthook-darwin-x64@2.2.2: + resolution: {integrity: sha512-fH/kArZ9ZZ65wCFtvBMgGcq25qwQpkQ5jp93ba+8wpjgxxzjczYGluwHvM/NQDJWLOFXuKXW0yFs76SramQkyQ==} + cpu: [x64] + os: [darwin] + + lefthook-freebsd-arm64@2.2.2: + resolution: {integrity: sha512-MCRsU01ju989QQffOfS3gCeFfyVDXs5B/FzR65fmrNvqcxb98fldvfzzoAbnK2q6SBJlRT3QT1tNEMCxddCwlA==} + cpu: [arm64] + os: [freebsd] + + lefthook-freebsd-x64@2.2.2: + resolution: {integrity: sha512-LkKuJU0S2oLimeoHwPTy1TLWB6GcN0t5lLxiXNlsBKUkr6R47QRcvPMZMoXmu3IetQtd+WliXMLE8qTwi2fqBg==} + cpu: [x64] + os: [freebsd] + + lefthook-linux-arm64@2.2.2: + resolution: {integrity: sha512-d+HFtS2DEKaOnD7vRU0tUz8pNgHwsUYmQ22+1dyXpOrXGyzNXR0AqX5KsIRF36JGhVvO5X5Anw5QYgR3mxVSuA==} + cpu: [arm64] + os: [linux] + + lefthook-linux-x64@2.2.2: + resolution: {integrity: sha512-N/OlQyAhcU6RAXjrwFxLsh70sn96rBoi+XkuLJ8CVBkk9Ci3VDXQayDZTNz5B1xML8RwoMcV8Wblniy1xFOmSA==} + cpu: [x64] + os: [linux] + + lefthook-openbsd-arm64@2.2.2: + resolution: {integrity: sha512-6G/gKXcpIwC+XcODg7ELZYP/hIrVIHQxhy2GoRdVyecmpGMFj+BZo3iqcUmeWusQMuL+UdNexbkrUaeJalIkNw==} + cpu: [arm64] + os: [openbsd] + + lefthook-openbsd-x64@2.2.2: + resolution: {integrity: sha512-eMJGi3/tqKEqu7XCZlqwQWRtK5JlL/9diaqqgRlEdlRD7GxwzHZN21z6awbITum9emea3lALvXFe6V8odQLGOA==} + cpu: [x64] + os: [openbsd] + + lefthook-windows-arm64@2.2.2: + resolution: {integrity: sha512-4P1EaQb5xG1zhYD1RLJ6TEGdFkmHkB+Slqx6yRDPKuBDNxMiD0+kTAAdUY2E8xPuRR35xHpVPR+b3/HtzPrjQQ==} + cpu: [arm64] + os: [win32] + + lefthook-windows-x64@2.2.2: + resolution: {integrity: sha512-9Ck4zancQilBfXT3ZeRipm7Lcq4RQfxyMpy8SAVINw0t/BrbjsT25V46ZuziERcwdajnE4myfMzC33GoCBwr7g==} + cpu: [x64] + os: [win32] + + lefthook@2.2.2: + resolution: {integrity: sha512-37fYHAhL12r1twaEV0PA5TwoIT491eTGGYjhhXxlYNMq3DSkiGZBQ++Jfk5maXj0vq52NRauLtMikBKtbNJs3Q==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru.min@1.1.5: + resolution: {integrity: sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + + mime@4.1.1: + resolution: {integrity: sha512-YAApRsTpM2+gnUFHhRNGfkX8NK0V5xj1nyQLufGlwEI+eX4xRsTcel2IXDuMEGSGjBvzykNW4VuoEACgwYe7BQ==} + engines: {node: '>=16'} + hasBin: true + + miniflare@5.20261012.0-beta: + resolution: {integrity: sha512-Cjrs+vHHQ7krLO4HYELYpYBJBuMhcnNZD/Z5z/UcQSnFqWSQY3DhtTCFalXgI1/yO1iK+YrRKK8EYkumrEopxg==} + engines: {node: '>=22.0.0'} + + miniflare@5.20261012.1-beta: + resolution: {integrity: sha512-8Canhy6g9+vAhqMc0o80YphqYIwpiA3aAp8aX+uDXU7J/qbUeoZzcII9c2V1P4uT/eHD4SB93qet1o1bRY3BuQ==} + engines: {node: '>=22.0.0'} + + miniflare@5.20261019.1-beta: + resolution: {integrity: sha512-1n7MN2UqZnPniyYAqNuV1YEAaja66ozKGVCc5ZTmYQhTubnoAwGLccwb+ZWYLOajuw3pOaJ70u//bZMuURlRew==} + engines: {node: '>=22.0.0'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + mysql2@3.24.6: + resolution: {integrity: sha512-enu73KhbAua2L3dIxjc6qg6ZJ7H3pt1BImZASWNWY5xSsmJ1ZuUPULTDGXgmJbI61vb+nJAG19IZclIq19UidQ==} + engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanostores@1.5.5: + resolution: {integrity: sha512-FixtE239Gl6Gz0ZGCK0WzvNwObNCIwVT+L7zQcf1QgcA6AzBxblzqe9OT88qiucNkhsjSOqiogSaF5jVbJiqSA==} + engines: {node: ^20.0.0 || >=22.0.0} + + node-addon-api@8.9.2: + resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==} + engines: {node: ^18 || ^20 || >= 21} + + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + oxc-transform-react@0.152.0: + resolution: {integrity: sha512-CAx+SKDU4uSOmQnyn7daMj/v/h8tF3fnaUMi4yImKAARPnXV0BieHe/qw1LTcK4KlnIuAhpfUJfSfNKIBh8HqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxfmt@0.74.0: + resolution: {integrity: sha512-G4OP7FgwGYZR5ttRpCOOfJdyRsSyvtn34vSXnjDq6Oqed5Dufg5mB44mank+51nAdHOKuTJ0lj0mksY7L+N61A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@astrojs/compiler-rs': ^0.4.0 || ^0.5.0 + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + '@astrojs/compiler-rs': + optional: true + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tailwindcss@1.16.0: + resolution: {integrity: sha512-qwkw3gahMiLOVkZAInULbvnFaFceanEnk+GxpMyNZU4bq2nRmZd3xD4En0EhIakEBiIt15RiGwQ7AuhrllNoNw==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxlint@1.89.0: + resolution: {integrity: sha512-a25hkfo61okGPvJz432fRIPoxlKtNnOVy9w4rzrmGgNm4iVTPwpSQlmdqNWnp6p9QyyPVfAZRgnf5XvSBZFk/A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2003' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathslash@0.1.0: + resolution: {integrity: sha512-mJSfhMv8Ra0gN4xGqzRQ7IpUSc4v6rV+r3X5FY9HrxC9Te2ZvtZe6RPS6UDmgOVUI5DKGDHuPXRuNzhffawG2Q==} + engines: {node: '>=22'} + + pg-cloudflare@1.4.1: + resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==} + + pg-connection-string@2.14.1: + resolution: {integrity: sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.1: + resolution: {integrity: sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.1: + resolution: {integrity: sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.64.1: + resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.64.1: + resolution: {integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==} + engines: {node: '>=20'} + hasBin: true + + postal-mime@4.0.11: + resolution: {integrity: sha512-T0zAqYnuSB8s+zL9WdiGj/2DeWeObEjqrcw2LWq+hgwpCSbLt7xujsFXn6Kgr1wsLl8DJh+v+lG9GQSQfMcUyA==} + engines: {node: '>=20'} + + postcss@8.5.31: + resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + rolldown@1.2.14: + resolution: {integrity: sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rou3@0.9.2: + resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@3.1.4: + resolution: {integrity: sha512-iEmdEew+Tn1BwMGMP8LzGBEPC7Mf1sySjglCeEPb/Vchm9bnGezePvacYcId+fGprQzIng+TBkzqfJ/qS7AsBg==} + + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sql-escaper@1.5.2: + resolution: {integrity: sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-hyperlinks@4.6.0: + resolution: {integrity: sha512-FL/q/YePS4tFFTNiBOR+zPk2ZrKheqNtmTJ/9OUZ4e1wKHJofDbGQSmSsxS0XNnabSkqexryCaXGvEKz1vf9UQ==} + engines: {node: '>=20'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terminal-link@5.0.0: + resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==} + engines: {node: '>=20'} + + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.2.0: + resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} + engines: {node: ^20.0.0 || >=22.0.0} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.15: + resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + undici@7.29.1: + resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + valibot@1.5.0: + resolution: {integrity: sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + vite@8.3.4: + resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + + vitest@5.0.5: + resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.5 + '@vitest/browser-preview': 5.0.5 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.5 + '@vitest/coverage-v8': 5.0.5 + '@vitest/ui': 5.0.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@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 + + void@1.2.0: + resolution: {integrity: sha512-MSbF2UVk1twGb+5P9KJNBzJJyOAwe6MLgpraQ3XsG2cJkP9gCKARyyR4Q7s/Lt0hhX29xSfseolpsWr67aynjw==} + engines: {node: '>=24.21.0'} + hasBin: true + peerDependencies: + '@void/md': 1.2.0 + arktype: '>=2.0.0' + valibot: '>=1.0.0-beta.7' + vite: ^8.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@void/md': + optional: true + arktype: + optional: true + valibot: + optional: true + zod: + optional: true + + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} + hasBin: true + + workerd@1.20261012.1: + resolution: {integrity: sha512-tEHoMPsMga0seVjxfsR8Pd3f8IRqYwkq++dTUKI5Gdgj6zuSDluYwisydq+dn90SsIACwBquJUQ1nBpcOqaWTA==} + engines: {node: '>=16'} + hasBin: true + + workerd@1.20261019.1: + resolution: {integrity: sha512-UUYRUq/7DmeCl11YriNbcVbjUZVLtt8uuCXFu+nz77hGFf8d4DEVXoCYtnA+MlBatE4+wxoj47DVqzK3xGBAiw==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.151.3: + resolution: {integrity: sha512-mzPQLmkAhZUV7CdiAhE+hiIAJlS9xPL6vZe1B8oQFuWxFu7nmQnvHAVjcAcabwSGkXFXH9uspzXwHOFrfbQmqQ==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20261019.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + +snapshots: + + '@ark/schema@0.56.8': + dependencies: + '@ark/util': 0.56.8 + + '@ark/util@0.56.8': {} + + '@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5)': + dependencies: + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + '@standard-schema/spec': 1.1.0 + better-call: 1.4.4(zod@4.6.7) + jose: 6.2.13 + kysely: 0.29.7 + nanostores: 1.5.5 + zod: 4.6.7 + + '@better-auth/drizzle-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1))': + dependencies: + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + optionalDependencies: + drizzle-orm: 0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1) + + '@better-auth/kysely-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(kysely@0.29.7)': + dependencies: + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + optionalDependencies: + kysely: 0.29.7 + + '@better-auth/memory-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)': + dependencies: + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + + '@better-auth/mongo-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)': + dependencies: + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + + '@better-auth/prisma-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)': + dependencies: + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + + '@better-auth/telemetry@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)': + dependencies: + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + + '@better-auth/utils@0.5.0': + dependencies: + '@noble/hashes': 2.4.0 + + '@better-fetch/fetch@1.3.2': {} + + '@cloudflare/build-output-utils@0.9.3': + dependencies: + '@cloudflare/config': 0.27.0 + + '@cloudflare/codemods@0.5.1': {} + + '@cloudflare/config@0.27.0': + dependencies: + zod: 4.6.5 + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/runtime-types@0.1.12(@types/node@26.6.5)': + dependencies: + miniflare: 5.20261012.1-beta(@types/node@26.6.5) + workerd: 1.20261012.1 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + '@cloudflare/sandbox@1.0.2': + dependencies: + aws4fetch: 1.0.20 + zod: 4.6.7 + + '@cloudflare/unenv-preset@2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261012.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20261012.1 + + '@cloudflare/unenv-preset@2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261019.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20261019.1 + + '@cloudflare/vite-plugin@1.64.0(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))(wrangler@4.151.3(@cloudflare/workers-types@5.20261021.1)(@types/node@26.6.5))': + dependencies: + '@cloudflare/unenv-preset': 2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261012.1) + miniflare: 5.20261012.0-beta(@types/node@26.6.5) + unenv: 2.0.0-rc.24 + vite: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + workerd: 1.20261012.1 + wrangler: 4.151.3(@cloudflare/workers-types@5.20261021.1)(@types/node@26.6.5) + ws: 8.21.0 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + '@cloudflare/workerd-darwin-64@1.20261012.1': + optional: true + + '@cloudflare/workerd-darwin-64@1.20261019.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20261012.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20261019.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20261012.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20261019.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20261012.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20261019.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20261012.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20261019.1': + optional: true + + '@cloudflare/workers-auth@0.14.1(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))': + dependencies: + '@cloudflare/workers-utils': 0.47.3(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) + undici: 7.29.1 + optionalDependencies: + vitest: 5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + + '@cloudflare/workers-types@5.20261021.1': {} + + '@cloudflare/workers-utils@0.47.3(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))': + dependencies: + chalk: 5.3.0 + dotenv: 16.3.1 + undici: 7.29.1 + optionalDependencies: + vitest: 5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@drizzle-team/brocli@0.10.2': {} + + '@effect/tsgo-darwin-arm64@0.56.0': + optional: true + + '@effect/tsgo-darwin-x64@0.56.0': + optional: true + + '@effect/tsgo-linux-arm64@0.56.0': + optional: true + + '@effect/tsgo-linux-arm@0.56.0': + optional: true + + '@effect/tsgo-linux-x64@0.56.0': + optional: true + + '@effect/tsgo-win32-arm64@0.56.0': + optional: true + + '@effect/tsgo-win32-x64@0.56.0': + optional: true + + '@effect/tsgo@0.56.0': + optionalDependencies: + '@effect/tsgo-darwin-arm64': 0.56.0 + '@effect/tsgo-darwin-x64': 0.56.0 + '@effect/tsgo-linux-arm': 0.56.0 + '@effect/tsgo-linux-arm64': 0.56.0 + '@effect/tsgo-linux-x64': 0.56.0 + '@effect/tsgo-win32-arm64': 0.56.0 + '@effect/tsgo-win32-x64': 0.56.0 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.3 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/keyring-darwin-arm64@2.1.2': + optional: true + + '@napi-rs/keyring-darwin-x64@2.1.2': + optional: true + + '@napi-rs/keyring-freebsd-x64@2.1.2': + optional: true + + '@napi-rs/keyring-linux-arm-gnueabihf@2.1.2': + optional: true + + '@napi-rs/keyring-linux-arm64-gnu@2.1.2': + optional: true + + '@napi-rs/keyring-linux-arm64-musl@2.1.2': + optional: true + + '@napi-rs/keyring-linux-riscv64-gnu@2.1.2': + optional: true + + '@napi-rs/keyring-linux-x64-gnu@2.1.2': + optional: true + + '@napi-rs/keyring-linux-x64-musl@2.1.2': + optional: true + + '@napi-rs/keyring-win32-arm64-msvc@2.1.2': + optional: true + + '@napi-rs/keyring-win32-ia32-msvc@2.1.2': + optional: true + + '@napi-rs/keyring-win32-x64-msvc@2.1.2': + optional: true + + '@napi-rs/keyring@2.1.2': + optionalDependencies: + '@napi-rs/keyring-darwin-arm64': 2.1.2 + '@napi-rs/keyring-darwin-x64': 2.1.2 + '@napi-rs/keyring-freebsd-x64': 2.1.2 + '@napi-rs/keyring-linux-arm-gnueabihf': 2.1.2 + '@napi-rs/keyring-linux-arm64-gnu': 2.1.2 + '@napi-rs/keyring-linux-arm64-musl': 2.1.2 + '@napi-rs/keyring-linux-riscv64-gnu': 2.1.2 + '@napi-rs/keyring-linux-x64-gnu': 2.1.2 + '@napi-rs/keyring-linux-x64-musl': 2.1.2 + '@napi-rs/keyring-win32-arm64-msvc': 2.1.2 + '@napi-rs/keyring-win32-ia32-msvc': 2.1.2 + '@napi-rs/keyring-win32-x64-msvc': 2.1.2 + + '@noble/ciphers@2.4.0': {} + + '@noble/hashes@2.4.0': {} + + '@oxc-project/types@0.154.0': {} + + '@oxc-transform-react/binding-android-arm-eabi@0.152.0': + optional: true + + '@oxc-transform-react/binding-android-arm64@0.152.0': + optional: true + + '@oxc-transform-react/binding-darwin-arm64@0.152.0': + optional: true + + '@oxc-transform-react/binding-darwin-x64@0.152.0': + optional: true + + '@oxc-transform-react/binding-freebsd-x64@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-arm-gnueabihf@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-arm-musleabihf@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-arm64-gnu@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-arm64-musl@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-ppc64-gnu@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-riscv64-gnu@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-riscv64-musl@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-s390x-gnu@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-x64-gnu@0.152.0': + optional: true + + '@oxc-transform-react/binding-linux-x64-musl@0.152.0': + optional: true + + '@oxc-transform-react/binding-openharmony-arm64@0.152.0': + optional: true + + '@oxc-transform-react/binding-win32-arm64-msvc@0.152.0': + optional: true + + '@oxc-transform-react/binding-win32-ia32-msvc@0.152.0': + optional: true + + '@oxc-transform-react/binding-win32-x64-msvc@0.152.0': + optional: true + + '@oxfmt/binding-android-arm-eabi@0.74.0': + optional: true + + '@oxfmt/binding-android-arm64@0.74.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.74.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.74.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.74.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.74.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.74.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.74.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.74.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.74.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.74.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.74.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.74.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.74.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.89.0': + optional: true + + '@oxlint/binding-android-arm64@1.89.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.89.0': + optional: true + + '@oxlint/binding-darwin-x64@1.89.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.89.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.89.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.89.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.89.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.89.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.89.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.89.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.89.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.89.0': + optional: true + + '@playwright/test@1.64.1': + dependencies: + playwright: 1.64.1 + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@rolldown/binding-android-arm-eabi@1.2.14': + optional: true + + '@rolldown/binding-android-arm64@1.2.14': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.14': + optional: true + + '@rolldown/binding-darwin-x64@1.2.14': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.14': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.14': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.14': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.14': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.14': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.14': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.14': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.24': {} + + '@standard-schema/spec@1.1.0': {} + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.26.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.2 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@26.6.5': + dependencies: + undici-types: 8.9.0 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@vitejs/plugin-react@6.1.2(oxc-transform-react@0.152.0)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + optionalDependencies: + oxc-transform-react: 0.152.0 + + '@vitest/mocker@5.0.5(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.5 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + vite: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + + '@vitest/spy@5.0.5': {} + + '@void/deploy-cloudflare@1.2.0': + dependencies: + '@void/deploy-core': 1.2.0 + jsonc-parser: 3.3.1 + + '@void/deploy-core@1.2.0': {} + + '@void/edge@1.2.0': + dependencies: + '@void/deploy-core': 1.2.0 + + '@void/isr@1.2.0': + dependencies: + '@cloudflare/workers-types': 5.20261021.1 + '@void/edge': 1.2.0 + + '@void/platform@1.2.0': + dependencies: + pathslash: 0.1.0 + + '@void/react@1.2.0(oxc-transform-react@0.152.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))(void@1.2.0(@types/node@26.6.5)(arktype@2.2.9)(kysely@0.29.7)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(valibot@1.5.0(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(wrangler@4.151.3(@cloudflare/workers-types@5.20261021.1)(@types/node@26.6.5))(zod@4.6.7))': + dependencies: + '@vitejs/plugin-react': 6.1.2(oxc-transform-react@0.152.0)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + vite: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + void: 1.2.0(@types/node@26.6.5)(arktype@2.2.9)(kysely@0.29.7)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(valibot@1.5.0(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(wrangler@4.151.3(@cloudflare/workers-types@5.20261021.1)(@types/node@26.6.5))(zod@4.6.7) + transitivePeerDependencies: + - '@rolldown/plugin-babel' + - babel-plugin-react-compiler + - oxc-transform-react + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + arkregex@0.0.14: + dependencies: + '@ark/util': 0.56.8 + + arktype@2.2.9: + dependencies: + '@ark/schema': 0.56.8 + '@ark/util': 0.56.8 + arkregex: 0.0.14 + + assertion-error@2.0.1: {} + + aws-ssl-profiles@1.1.2: {} + + aws4fetch@1.0.20: {} + + balanced-match@4.0.4: {} + + better-auth@1.7.12(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1))(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))): + dependencies: + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/drizzle-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1)) + '@better-auth/kysely-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(kysely@0.29.7) + '@better-auth/memory-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0) + '@better-auth/mongo-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0) + '@better-auth/prisma-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0) + '@better-auth/telemetry': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2) + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + '@noble/ciphers': 2.4.0 + '@noble/hashes': 2.4.0 + better-call: 1.4.4(zod@4.6.7) + defu: 6.1.7 + jose: 6.2.13 + kysely: 0.29.7 + nanostores: 1.5.5 + zod: 4.6.7 + optionalDependencies: + drizzle-kit: 0.31.11 + drizzle-orm: 0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1) + mysql2: 3.24.6(@types/node@26.6.5) + pg: 8.23.1 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + vitest: 5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + transitivePeerDependencies: + - '@opentelemetry/api' + + better-call@1.4.4(zod@4.6.7): + dependencies: + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + rou3: 0.9.2 + set-cookie-parser: 3.1.4 + optionalDependencies: + zod: 4.6.7 + + better-sqlite3@13.0.3: + dependencies: + node-addon-api: 8.9.2 + + blake3-jit@1.1.0: {} + + blake3-wasm@2.1.5: {} + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + buffer-from@1.1.2: {} + + cf@1.0.0-beta.18(@types/node@26.6.5): + dependencies: + '@cloudflare/build-output-utils': 0.9.3 + '@cloudflare/config': 0.27.0 + '@cloudflare/runtime-types': 0.1.12(@types/node@26.6.5) + blake3-wasm: 2.1.5 + miniflare: 5.20261012.1-beta(@types/node@26.6.5) + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + chai@6.3.0: {} + + chalk@5.3.0: {} + + cookie@1.1.1: {} + + csstype@3.2.3: {} + + defu@6.1.7: {} + + detect-libc@2.1.2: {} + + dotenv@16.3.1: {} + + drizzle-arktype@0.1.3(arktype@2.2.9)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1)): + dependencies: + arktype: 2.2.9 + drizzle-orm: 0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1) + + drizzle-kit@0.31.11: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.23.15 + + drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1): + optionalDependencies: + '@cloudflare/workers-types': 5.20261021.1 + better-sqlite3: 13.0.3 + kysely: 0.29.7 + mysql2: 3.24.6(@types/node@26.6.5) + pg: 8.23.1 + + drizzle-valibot@0.4.2(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1))(valibot@1.5.0(typescript@7.0.2)): + dependencies: + drizzle-orm: 0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1) + valibot: 1.5.0(typescript@7.0.2) + + drizzle-zod@0.8.3(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1))(zod@4.6.7): + dependencies: + drizzle-orm: 0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1) + zod: 4.6.7 + + effect@4.0.5: {} + + email-validator@2.0.4: {} + + enhanced-resolve@5.26.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + environment@1.1.0: {} + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@2.3.2: {} + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.28.2: + 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 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-check@4.10.2: + dependencies: + pure-rand: 8.4.2 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + + graceful-fs@4.2.11: {} + + has-flag@5.0.1: {} + + hono@4.13.13: {} + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@7.0.13: {} + + is-property@1.0.2: {} + + jiti@2.7.0: {} + + jose@6.2.13: {} + + jsonc-parser@3.3.1: {} + + kleur@4.1.5: {} + + kysely@0.29.7: {} + + lefthook-android-arm64@2.2.2: + optional: true + + lefthook-darwin-arm64@2.2.2: + optional: true + + lefthook-darwin-x64@2.2.2: + optional: true + + lefthook-freebsd-arm64@2.2.2: + optional: true + + lefthook-freebsd-x64@2.2.2: + optional: true + + lefthook-linux-arm64@2.2.2: + optional: true + + lefthook-linux-x64@2.2.2: + optional: true + + lefthook-openbsd-arm64@2.2.2: + optional: true + + lefthook-openbsd-x64@2.2.2: + optional: true + + lefthook-windows-arm64@2.2.2: + optional: true + + lefthook-windows-x64@2.2.2: + optional: true + + lefthook@2.2.2: + optionalDependencies: + lefthook-android-arm64: 2.2.2 + lefthook-darwin-arm64: 2.2.2 + lefthook-darwin-x64: 2.2.2 + lefthook-freebsd-arm64: 2.2.2 + lefthook-freebsd-x64: 2.2.2 + lefthook-linux-arm64: 2.2.2 + lefthook-linux-x64: 2.2.2 + lefthook-openbsd-arm64: 2.2.2 + lefthook-openbsd-x64: 2.2.2 + lefthook-windows-arm64: 2.2.2 + lefthook-windows-x64: 2.2.2 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + 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 + + long@5.3.2: {} + + lru.min@1.1.5: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magic-string@1.4.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mime@4.1.1: {} + + miniflare@5.20261012.0-beta(@types/node@26.6.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.5(@types/node@26.6.5) + undici: 7.29.1 + workerd: 1.20261012.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + miniflare@5.20261012.1-beta(@types/node@26.6.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.5(@types/node@26.6.5) + undici: 7.29.1 + workerd: 1.20261012.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + miniflare@5.20261019.1-beta(@types/node@26.6.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.5(@types/node@26.6.5) + undici: 7.29.1 + workerd: 1.20261019.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + mysql2@3.24.6(@types/node@26.6.5): + dependencies: + '@types/node': 26.6.5 + aws-ssl-profiles: 1.1.2 + generate-function: 2.3.1 + iconv-lite: 0.7.3 + long: 5.3.2 + lru.min: 1.1.5 + named-placeholders: 1.1.6 + sql-escaper: 1.5.2 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.5 + + nanoid@3.3.20: {} + + nanostores@1.5.5: {} + + node-addon-api@8.9.2: {} + + obug@3.0.0: {} + + oxc-transform-react@0.152.0: + optionalDependencies: + '@oxc-transform-react/binding-android-arm-eabi': 0.152.0 + '@oxc-transform-react/binding-android-arm64': 0.152.0 + '@oxc-transform-react/binding-darwin-arm64': 0.152.0 + '@oxc-transform-react/binding-darwin-x64': 0.152.0 + '@oxc-transform-react/binding-freebsd-x64': 0.152.0 + '@oxc-transform-react/binding-linux-arm-gnueabihf': 0.152.0 + '@oxc-transform-react/binding-linux-arm-musleabihf': 0.152.0 + '@oxc-transform-react/binding-linux-arm64-gnu': 0.152.0 + '@oxc-transform-react/binding-linux-arm64-musl': 0.152.0 + '@oxc-transform-react/binding-linux-ppc64-gnu': 0.152.0 + '@oxc-transform-react/binding-linux-riscv64-gnu': 0.152.0 + '@oxc-transform-react/binding-linux-riscv64-musl': 0.152.0 + '@oxc-transform-react/binding-linux-s390x-gnu': 0.152.0 + '@oxc-transform-react/binding-linux-x64-gnu': 0.152.0 + '@oxc-transform-react/binding-linux-x64-musl': 0.152.0 + '@oxc-transform-react/binding-openharmony-arm64': 0.152.0 + '@oxc-transform-react/binding-win32-arm64-msvc': 0.152.0 + '@oxc-transform-react/binding-win32-ia32-msvc': 0.152.0 + '@oxc-transform-react/binding-win32-x64-msvc': 0.152.0 + + oxfmt@0.74.0: + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.74.0 + '@oxfmt/binding-android-arm64': 0.74.0 + '@oxfmt/binding-darwin-arm64': 0.74.0 + '@oxfmt/binding-darwin-x64': 0.74.0 + '@oxfmt/binding-freebsd-x64': 0.74.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.74.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.74.0 + '@oxfmt/binding-linux-arm64-gnu': 0.74.0 + '@oxfmt/binding-linux-arm64-musl': 0.74.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.74.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.74.0 + '@oxfmt/binding-linux-riscv64-musl': 0.74.0 + '@oxfmt/binding-linux-s390x-gnu': 0.74.0 + '@oxfmt/binding-linux-x64-gnu': 0.74.0 + '@oxfmt/binding-linux-x64-musl': 0.74.0 + '@oxfmt/binding-openharmony-arm64': 0.74.0 + '@oxfmt/binding-win32-arm64-msvc': 0.74.0 + '@oxfmt/binding-win32-ia32-msvc': 0.74.0 + '@oxfmt/binding-win32-x64-msvc': 0.74.0 + + oxlint-tailwindcss@1.16.0: + dependencies: + '@tailwindcss/node': 4.3.3 + tailwindcss: 4.3.3 + + oxlint@1.89.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.89.0 + '@oxlint/binding-android-arm64': 1.89.0 + '@oxlint/binding-darwin-arm64': 1.89.0 + '@oxlint/binding-darwin-x64': 1.89.0 + '@oxlint/binding-freebsd-x64': 1.89.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.89.0 + '@oxlint/binding-linux-arm-musleabihf': 1.89.0 + '@oxlint/binding-linux-arm64-gnu': 1.89.0 + '@oxlint/binding-linux-arm64-musl': 1.89.0 + '@oxlint/binding-linux-ppc64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-musl': 1.89.0 + '@oxlint/binding-linux-s390x-gnu': 1.89.0 + '@oxlint/binding-linux-x64-gnu': 1.89.0 + '@oxlint/binding-linux-x64-musl': 1.89.0 + '@oxlint/binding-openharmony-arm64': 1.89.0 + '@oxlint/binding-win32-arm64-msvc': 1.89.0 + '@oxlint/binding-win32-ia32-msvc': 1.89.0 + '@oxlint/binding-win32-x64-msvc': 1.89.0 + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + pathslash@0.1.0: {} + + pg-cloudflare@1.4.1: + optional: true + + pg-connection-string@2.14.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.1): + dependencies: + pg: 8.23.1 + + pg-protocol@1.16.1: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.1: + dependencies: + pg-connection-string: 2.14.1 + pg-pool: 3.14.0(pg@8.23.1) + pg-protocol: 1.16.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.64.1: {} + + playwright@1.64.1: + dependencies: + playwright-core: 1.64.1 + + postal-mime@4.0.11: {} + + postcss@8.5.31: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + pure-rand@8.4.2: {} + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react@19.3.1: {} + + resolve-pkg-maps@1.0.0: {} + + retry@0.12.0: {} + + rolldown@1.2.14: + dependencies: + '@oxc-project/types': 0.154.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.14 + '@rolldown/binding-android-arm64': 1.2.14 + '@rolldown/binding-darwin-arm64': 1.2.14 + '@rolldown/binding-darwin-x64': 1.2.14 + '@rolldown/binding-freebsd-x64': 1.2.14 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.14 + '@rolldown/binding-linux-arm64-gnu': 1.2.14 + '@rolldown/binding-linux-arm64-musl': 1.2.14 + '@rolldown/binding-linux-ppc64-gnu': 1.2.14 + '@rolldown/binding-linux-s390x-gnu': 1.2.14 + '@rolldown/binding-linux-x64-gnu': 1.2.14 + '@rolldown/binding-linux-x64-musl': 1.2.14 + '@rolldown/binding-openharmony-arm64': 1.2.14 + '@rolldown/binding-win32-arm64-msvc': 1.2.14 + '@rolldown/binding-win32-x64-msvc': 1.2.14 + + rou3@0.9.2: {} + + safer-buffer@2.1.2: {} + + scheduler@0.28.0: {} + + semver@7.8.5: {} + + set-cookie-parser@3.1.4: {} + + sharp@0.35.5(@types/node@26.6.5): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 26.6.5 + + signal-exit@3.0.7: {} + + source-map-js@1.2.2: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + sql-escaper@1.5.2: {} + + std-env@4.3.0: {} + + supports-color@10.2.2: {} + + supports-hyperlinks@4.6.0: + dependencies: + has-flag: 5.0.1 + supports-color: 10.2.2 + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + terminal-link@5.0.0: + dependencies: + ansi-escapes: 7.3.0 + supports-hyperlinks: 4.6.0 + + tinybench@6.2.2: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@2.2.0: {} + + tslib@2.8.1: + optional: true + + tsx@4.23.15: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + undici-types@8.9.0: {} + + undici@7.29.1: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + valibot@1.5.0(typescript@7.0.2): + optionalDependencies: + typescript: 7.0.2 + + vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.31 + rolldown: 1.2.14 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.6.5 + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.23.15 + yaml: 2.9.1 + + vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.5(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 3.0.0 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + why-is-node-running: 3.2.1 + optionalDependencies: + '@types/node': 26.6.5 + transitivePeerDependencies: + - msw + + void@1.2.0(@types/node@26.6.5)(arktype@2.2.9)(kysely@0.29.7)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(valibot@1.5.0(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(wrangler@4.151.3(@cloudflare/workers-types@5.20261021.1)(@types/node@26.6.5))(zod@4.6.7): + dependencies: + '@cloudflare/build-output-utils': 0.9.3 + '@cloudflare/codemods': 0.5.1 + '@cloudflare/config': 0.27.0 + '@cloudflare/sandbox': 1.0.2 + '@cloudflare/vite-plugin': 1.64.0(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))(wrangler@4.151.3(@cloudflare/workers-types@5.20261021.1)(@types/node@26.6.5)) + '@cloudflare/workers-auth': 0.14.1(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) + '@cloudflare/workers-types': 5.20261021.1 + '@napi-rs/keyring': 2.1.2 + '@void/deploy-cloudflare': 1.2.0 + '@void/deploy-core': 1.2.0 + '@void/edge': 1.2.0 + '@void/isr': 1.2.0 + '@void/platform': 1.2.0 + better-auth: 1.7.12(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1))(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) + better-sqlite3: 13.0.3 + blake3-jit: 1.1.0 + cf: 1.0.0-beta.18(@types/node@26.6.5) + drizzle-arktype: 0.1.3(arktype@2.2.9)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1)) + drizzle-kit: 0.31.11 + drizzle-orm: 0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1) + drizzle-valibot: 0.4.2(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1))(valibot@1.5.0(typescript@7.0.2)) + drizzle-zod: 0.8.3(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261021.1)(better-sqlite3@13.0.3)(kysely@0.29.7)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1))(zod@4.6.7) + email-validator: 2.0.4 + hono: 4.13.13 + ignore: 7.0.13 + jiti: 2.7.0 + jsonc-parser: 3.3.1 + mime: 4.1.1 + miniflare: 5.20261012.1-beta(@types/node@26.6.5) + minimatch: 10.2.6 + mysql2: 3.24.6(@types/node@26.6.5) + pg: 8.23.1 + postal-mime: 4.0.11 + proper-lockfile: 4.1.2 + terminal-link: 5.0.0 + vite: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + yaml: 2.9.1 + optionalDependencies: + arktype: 2.2.9 + valibot: 1.5.0(typescript@7.0.2) + zod: 4.6.7 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@lynx-js/react' + - '@neondatabase/serverless' + - '@netlify/db' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@sveltejs/kit' + - '@tanstack/react-start' + - '@tanstack/solid-start' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/node' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - bufferutil + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mongodb + - msw + - next + - pg-native + - postgres + - prisma + - react + - react-dom + - solid-js + - sql.js + - sqlite3 + - svelte + - utf-8-validate + - vitest + - vue + - wrangler + + why-is-node-running@3.2.1: {} + + workerd@1.20261012.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20261012.1 + '@cloudflare/workerd-darwin-arm64': 1.20261012.1 + '@cloudflare/workerd-linux-64': 1.20261012.1 + '@cloudflare/workerd-linux-arm64': 1.20261012.1 + '@cloudflare/workerd-windows-64': 1.20261012.1 + + workerd@1.20261019.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20261019.1 + '@cloudflare/workerd-darwin-arm64': 1.20261019.1 + '@cloudflare/workerd-linux-64': 1.20261019.1 + '@cloudflare/workerd-linux-arm64': 1.20261019.1 + '@cloudflare/workerd-windows-64': 1.20261019.1 + + wrangler@4.151.3(@cloudflare/workers-types@5.20261021.1)(@types/node@26.6.5): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261019.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.2 + miniflare: 5.20261019.1-beta(@types/node@26.6.5) + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20261019.1 + optionalDependencies: + '@cloudflare/workers-types': 5.20261021.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + ws@8.21.0: {} + + xtend@4.0.2: {} + + yaml@2.9.1: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.24 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod@4.6.5: {} + + zod@4.6.7: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..be1ccde --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,24 @@ +minimumReleaseAgeExclude: + - '@void/deploy-cloudflare@1.2.0' + - '@void/deploy-core@1.2.0' + - '@void/edge@1.2.0' + - '@void/isr@1.2.0' + - '@void/platform@1.2.0' + - void@1.2.0 + - '@void/react@1.2.0' + - '@effect/tsgo-darwin-arm64@0.56.0' + - '@effect/tsgo-darwin-x64@0.56.0' + - '@effect/tsgo-linux-arm64@0.56.0' + - '@effect/tsgo-linux-arm@0.56.0' + - '@effect/tsgo-linux-x64@0.56.0' + - '@effect/tsgo-win32-arm64@0.56.0' + - '@effect/tsgo-win32-x64@0.56.0' + - '@effect/tsgo@0.56.0' + +allowBuilds: + better-sqlite3: false + esbuild: true + workerd: true + sharp: true + lefthook: true + unrs-resolver: true diff --git a/research/rea-summary.json b/research/rea-summary.json deleted file mode 100644 index c1aae83..0000000 --- a/research/rea-summary.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "evidence_id": "ev_ac4cc745a3f06625034762dfdcc80b0b49437d7c2367df61f361a94018d9353b", - "operation": "analyze_javascript_application", - "provider": { - "id": "rea-javascript-application", - "name": "REA JavaScript application analyzer", - "version": "1" - }, - "parameters": { - "format": "auto", - "integrity_policy": "fail", - "source_paths": [ - "application.js" - ], - "analysis_timeout_ms": 90000 - }, - "subject": { - "name": "skritter-inspection", - "digest": { - "sha256": "b8f588d69659347aefcf9072d4d02f1df9da27534bcea957a57f1a84a95e246b" - }, - "local_path": "/private/tmp/skritter-inspection", - "format": "directory", - "architecture": null - }, - "statistics": { - "relevant_files": 3, - "nested_asar_containers": 0, - "text_bytes_read": 1920522, - "invalid_utf8_files": 0, - "parsed_javascript_files": 1, - "skipped_javascript_files": 0, - "visited_ast_nodes": 401723, - "findings": 1271, - "modules": 0, - "parse_failures": 0 - }, - "limitations": [ - "Caller-selected source analysis leaves 2 inventoried files outside analysis scope. Inventory identity covers the complete artifact; relationships through unselected sources remain unknown.", - "CommonJS and ESM binding relationships were recovered from inert syntax and resolved only within the inventoried artifact container.", - "Graph coverage is incomplete; unavailable facts remain explicit and must not be read as absence.", - "JavaScript and HTML were parsed as inert text; bundle bootstrap code was never executed.", - "Static imports, entrypoints, workers, endpoints, and storage relationships do not prove runtime execution.", - "Static paths and relationships may remain unresolved when expressions are dynamic or obfuscated.", - "Webpack/Rspack factories were recovered from AST literals; REA did not invoke push handlers or bundle bootstrap code." - ], - "source_urls": { - "application.js": "https://skritter.com/js/application.js", - "libraries.js": "https://skritter.com/js/libraries.js", - "startup.js": "https://skritter.com/js/startup.js" - }, - "downloaded_sha256": { - "libraries.js": "19b262cf3be1a7f3a1be199532262eab3580377f4834cd4a5c47e391525aaf55", - "startup.js": "c982d435012dda8c6a8e990cd85aac6a74b388f1e37c06c578c3f4bfb61f08f5", - "application.js": "8933badb04686e9d3aef97b1ef8b22bf030021868617403241b08aeda2c730e5" - }, - "full_evidence_path": "/private/tmp/skritter-rea-evidence.json" -} diff --git a/scripts/__tests__/test-browser-linux.test.mjs b/scripts/__tests__/test-browser-linux.test.mjs new file mode 100644 index 0000000..1e15cee --- /dev/null +++ b/scripts/__tests__/test-browser-linux.test.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; + +test("Linux runner preserves filters, omits Mac metadata, and propagates Docker failure", (t) => { + const directory = mkdtempSync(join(tmpdir(), "timtim-linux-runner-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(join(directory, "docker"), `#!/bin/sh +if [ "$1" = info ]; then exit 0; fi +cat > "$LINUX_RUNNER_TEST_DIR/source.tar" +printf '%s\\n' "$@" > "$LINUX_RUNNER_TEST_DIR/arguments" +exit 17 +`, { mode: 0o755 }); + + const result = spawnSync("bash", ["scripts/test-browser-linux.sh", "mobile.spec.ts", "--grep", "first tap"], { + env: { ...process.env, PATH: `${directory}:${process.env.PATH}`, LINUX_RUNNER_TEST_DIR: directory }, + encoding: "utf8", + }); + assert.equal(result.status, 17, result.stderr); + const args = readFileSync(join(directory, "arguments"), "utf8").trimEnd().split("\n"); + assert.deepEqual(args.slice(-3), ["mobile.spec.ts", "--grep", "first tap"]); + const archive = spawnSync("tar", ["-tf", join(directory, "source.tar")], { encoding: "utf8" }); + assert.equal(archive.status, 0, archive.stderr); + assert.match(archive.stdout, /^package\.json$/m); + assert.doesNotMatch(archive.stdout, /(^|\/)\._/m); +}); diff --git a/scripts/oxlint-boundaries.mjs b/scripts/oxlint-boundaries.mjs new file mode 100644 index 0000000..1fcbc63 --- /dev/null +++ b/scripts/oxlint-boundaries.mjs @@ -0,0 +1,65 @@ +import { isBuiltin } from "node:module"; +import { dirname, resolve } from "node:path"; + +const browserVoidImports = new Set([ + "void/client", "void/client/react", "void/client/vue", "void/client/svelte", "void/client/solid", + "void/sse/client", "void/live/client", "void/pages-client", "void/pages-prefetch", + "@void/react", "@void/react/pages-client", +]); + +function isBrowserModule(filename) { + return ["/browser/", "/components/", "/use-case/"].some((folder) => filename.includes(folder)); +} + +export default { + meta: { name: "strokes-boundaries" }, + rules: { + "no-platform-in-domain": { + meta: { type: "problem", schema: [], messages: { forbidden: "Keep domain code pure and browser/server dependencies in their owning modules." } }, + create(context) { + const filename = context.filename; + const domain = filename.includes("/domain/"); + const browser = isBrowserModule(filename); + const page = filename.includes("/pages/") && !filename.endsWith(".server.ts"); + function isTypeOnly(node) { + if (node.type === "ImportDeclaration") { + return node.importKind === "type" || ( + node.specifiers?.length > 0 && + node.specifiers.every((specifier) => specifier.importKind === "type") + ); + } + if (node.type === "ExportAllDeclaration") return node.exportKind === "type"; + if (node.type === "ExportNamedDeclaration") { + const specifiers = node.specifiers ?? []; + return node.exportKind === "type" || ( + node.declaration == null && + specifiers.length > 0 && + specifiers.every((specifier) => specifier.exportKind === "type") + ); + } + return false; + } + function check(node) { + if (isTypeOnly(node)) return; + const source = node.source; + if (!source) return; + const name = source.value; + if (typeof name !== "string") { + if (domain || browser || page) context.report({ node, messageId: "forbidden" }); + return; + } + const target = name.startsWith(".") ? resolve(dirname(filename), name) : name; + const provider = target.includes("/providers/") || target.includes("/routes/") || target.endsWith(".server") || target.endsWith(".server.ts"); + const platform = /^(?:node:|react(?:-dom)?(?:\/|$)|void(?:\/|$)|@void\/)/.test(name); + const forbidden = domain + ? provider || isBrowserModule(target) || target.includes("/pages/") || platform || (!name.startsWith(".") && name !== "effect" && !name.startsWith("effect/")) + : (browser || page) + ? provider || isBuiltin(name) || name.startsWith("node:") || ((name === "void" || name.startsWith("void/") || name.startsWith("@void/")) && !browserVoidImports.has(name)) + : (filename.includes("/routes/") || filename.includes("/providers/")) && isBrowserModule(target); + if (forbidden) context.report({ node, messageId: "forbidden" }); + } + return { ImportDeclaration: check, ExportNamedDeclaration: check, ExportAllDeclaration: check, ImportExpression: check }; + }, + }, + }, +}; diff --git a/scripts/test-browser-linux.sh b/scripts/test-browser-linux.sh new file mode 100644 index 0000000..20eb99c --- /dev/null +++ b/scripts/test-browser-linux.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" +playwright_version=$(node -p 'require("@playwright/test/package.json").version') +package_manager=$(node -p 'require("./package.json").packageManager') +docker info >/dev/null +mkdir -p test-results/linux + +# Copy tracked working-tree files, including staged additions, without host dependencies or untracked files. +git ls-files -z | COPYFILE_DISABLE=1 tar --no-xattrs --null -T - -cf - | docker run --rm -i --init --ipc=host \ + --mount "type=bind,src=$PWD/test-results/linux,dst=/results" \ + --mount type=volume,src=timtim-linux-test-cache,dst=/cache \ + --env npm_config_cache=/cache/npm \ + "mcr.microsoft.com/playwright:v${playwright_version}-noble" \ + bash -euc ' + mkdir /app + tar -xf - -C /app + cd /app + git init -q + npm install --global "$1" + shift + pnpm install --frozen-lockfile --store-dir /cache/pnpm + pnpm test:browser "$@" --output=/results + ' -- "$package_manager" "$@" diff --git a/src/features/writing/__tests__/browser/checking.spec.ts b/src/features/writing/__tests__/browser/checking.spec.ts new file mode 100644 index 0000000..06940ef --- /dev/null +++ b/src/features/writing/__tests__/browser/checking.spec.ts @@ -0,0 +1,140 @@ +import type { Page } from "@playwright/test"; +import { expect, test } from "./fixtures"; + +type Stroke = readonly (readonly [number, number])[]; + +// Independent learner attempts in the pad's 0–100 coordinate space, with small deviations. +const curve: Stroke = [ + [39, 72], + [31, 65], + [26, 56], + [26, 49], + [30, 42], + [35, 39], + [40, 42], + [44, 49], + [48, 56], + [52, 50], + [59, 45], + [66, 45], +]; +const stem: Stroke = [ + [68, 26], + [68, 50], + [66, 75], +]; +const headline: Stroke = [ + [23, 26], + [49, 24], + [76, 26], +]; +const letter = [curve, stem, headline]; +const extraInk: Stroke = [ + [8, 8], + [90, 90], +]; +const accepted = "That’s it! You matched the letter."; +const rejected = "Not quite yet. Follow the whole shape and try again."; + +async function draw(page: Page, strokes: readonly Stroke[]): Promise { + const pad = page.getByRole("img", { name: "Hindi writing practice area" }); + await pad.scrollIntoViewIfNeeded(); + const bounds = await pad.boundingBox(); + if (!bounds) throw new Error("Writing pad has no bounds"); + for (const stroke of strokes) { + for (const [index, [x, y]] of stroke.entries()) { + await page.mouse.move( + bounds.x + (bounds.width * x) / 100, + bounds.y + (bounds.height * y) / 100, + { steps: index === 0 ? 1 : 3 }, + ); + if (index === 0) await page.mouse.down(); + } + await page.mouse.up(); + } +} + +const attempts: readonly { + readonly name: string; + readonly strokes: readonly Stroke[]; + readonly result: string; +}[] = [ + { + name: "accepts a complete character with small drawing deviations", + strokes: letter, + result: accepted, + }, + { + name: "accepts reversed direction and a different stroke order", + strokes: [...letter].reverse().map((stroke) => [...stroke].reverse()), + result: accepted, + }, + { name: "rejects a missing headline", strokes: [curve, stem], result: rejected }, + { name: "rejects a single dot", strokes: [[[68, 26]]], result: rejected }, + { + name: "rejects a misplaced character", + strokes: letter.map((stroke) => stroke.map(([x, y]) => [x + 18, y])), + result: rejected, + }, + { + name: "rejects extra ink even with a complete character", + strokes: [...letter, extraInk], + result: rejected, + }, +]; + +for (const viewport of [ + { name: "desktop", width: 1200, height: 1000 }, + { name: "phone", width: 390, height: 844 }, +]) { + test.describe(viewport.name, () => { + test.use({ viewport: { width: viewport.width, height: viewport.height } }); + + for (const attempt of attempts) { + test(attempt.name, async ({ page }) => { + await page.goto("/"); + const status = page.getByRole("status"); + const check = page.getByRole("button", { name: "Check", exact: true }); + await expect(status).toContainText("Ready to trace"); + await expect(check).toBeDisabled(); + await draw(page, attempt.strokes); + await expect(check).toBeEnabled(); + await check.click(); + await expect(status).toContainText(attempt.result); + await check.click(); + await expect(status).toContainText(attempt.result); + await page.getByRole("button", { name: "Retry", exact: true }).click(); + await expect(status).toContainText("Ready to trace"); + await expect(check).toBeDisabled(); + }); + } + + test("a rejected character can be completed without resetting", async ({ page }) => { + await page.goto("/"); + const status = page.getByRole("status"); + const check = page.getByRole("button", { name: "Check", exact: true }); + await expect(status).toContainText("Ready to trace"); + await draw(page, [curve, stem]); + await check.click(); + await expect(status).toContainText(rejected); + await draw(page, [headline]); + await expect(status).toContainText("3 strokes drawn"); + await check.click(); + await expect(status).toContainText(accepted); + }); + + test("new ink clears success and is included in the next check", async ({ page }) => { + await page.goto("/"); + const status = page.getByRole("status"); + const check = page.getByRole("button", { name: "Check", exact: true }); + await expect(status).toContainText("Ready to trace"); + await draw(page, letter); + await check.click(); + await expect(status).toContainText(accepted); + await draw(page, [extraInk]); + await expect(status).toContainText("4 strokes drawn"); + await check.click(); + await expect(status).toContainText(rejected); + }); + }); +} diff --git a/src/features/writing/__tests__/browser/fixtures.ts b/src/features/writing/__tests__/browser/fixtures.ts new file mode 100644 index 0000000..e7e0d07 --- /dev/null +++ b/src/features/writing/__tests__/browser/fixtures.ts @@ -0,0 +1 @@ +export { test, expect } from "@playwright/test"; diff --git a/src/features/writing/__tests__/browser/mobile.spec.ts b/src/features/writing/__tests__/browser/mobile.spec.ts new file mode 100644 index 0000000..e43a131 --- /dev/null +++ b/src/features/writing/__tests__/browser/mobile.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from "./fixtures"; + +for (const viewport of [ + { width: 320, height: 568 }, + { width: 390, height: 844 }, + { width: 430, height: 932 }, +]) { + test.describe(`mobile browser ${viewport.width}px`, () => { + test.use({ viewport, isMobile: true, hasTouch: true, deviceScaleFactor: 3 }); + + test("uses device width and stacks practice above guidance", async ({ page }, testInfo) => { + await page.goto("/"); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + await expect.poll(() => page.evaluate(() => window.innerWidth)).toBe(viewport.width); + const pad = page.getByRole("img", { name: "Hindi writing practice area" }); + const retry = page.getByRole("button", { name: "Retry", exact: true }); + const guidance = page.getByRole("complementary", { name: "Practice guidance" }); + const bounds = await pad.boundingBox(); + const retryBounds = await retry.boundingBox(); + const guidanceBounds = await guidance.boundingBox(); + if (!bounds || !retryBounds || !guidanceBounds) throw new Error("Missing practice layout"); + expect(bounds.width).toBeGreaterThanOrEqual(viewport.width - 48); + expect(Math.abs(bounds.width - bounds.height)).toBeLessThan(1); + expect(retryBounds.height).toBeGreaterThanOrEqual(48); + expect(guidanceBounds.y).toBeGreaterThanOrEqual(retryBounds.y + retryBounds.height); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(viewport.width); + const touch = await page.context().newCDPSession(page); + // Native gestures avoid Linux Chromium swallowing the next tap after raw touch injection. + await touch.send("Input.synthesizeScrollGesture", { + x: bounds.x + bounds.width * 0.3, + y: bounds.y + bounds.height * 0.4, + xDistance: -bounds.width * 0.25, + yDistance: -bounds.height * 0.15, + gestureSourceType: "touch", + preventFling: true, + }); + await expect(page.getByRole("status")).toContainText("1 stroke drawn"); + expect(await page.evaluate(() => window.scrollY)).toBe(0); + await retry.tap(); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + await page.screenshot({ path: testInfo.outputPath("mobile-practice.png"), fullPage: true }); + }); + }); +} diff --git a/src/features/writing/__tests__/browser/practice.spec.ts b/src/features/writing/__tests__/browser/practice.spec.ts new file mode 100644 index 0000000..ea99216 --- /dev/null +++ b/src/features/writing/__tests__/browser/practice.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from "./fixtures"; + +test("opens a readable practice page with a square pad on phone and desktop", async ({ page }) => { + for (const viewport of [{ width: 390, height: 844 }, { width: 1280, height: 800 }]) { + await page.setViewportSize(viewport); + await page.goto("/"); + await expect(page.getByRole("heading", { name: "Strokes", exact: true })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Focus on the whole letter" })).toBeVisible(); + await expect(page.getByText(/Take your time with the curves and lines/)).toHaveCount(1); + await expect(page.getByText(/Your ink stays on this page/)).toHaveCount(0); + const pad = page.getByRole("img", { name: "Hindi writing practice area" }); + await expect(pad).toBeVisible(); + const bounds = await pad.boundingBox(); + expect(bounds).not.toBeNull(); + if (bounds === null) throw new Error("Practice pad has no visible bounds"); + expect(Math.abs(bounds.width - bounds.height)).toBeLessThan(1); + expect(bounds.width).toBeLessThanOrEqual(viewport.width); + } +}); diff --git a/src/features/writing/__tests__/browser/tracing.spec.ts b/src/features/writing/__tests__/browser/tracing.spec.ts new file mode 100644 index 0000000..fc699c6 --- /dev/null +++ b/src/features/writing/__tests__/browser/tracing.spec.ts @@ -0,0 +1,245 @@ +import { test, expect } from "./fixtures"; +import { DEVELOPMENT_LESSON } from "../../use-case/tracing/constants/development-lesson"; +import type { Locator } from "@playwright/test"; + +async function inkScreenshot(pad: Locator): Promise<{ readonly count: number; readonly bounds: { readonly left: number; readonly top: number; readonly right: number; readonly bottom: number } }> { + const png = (await pad.screenshot()).toString("base64"); + return pad.evaluate(async (_element, encoded) => { + const response = await fetch(`data:image/png;base64,${encoded}`); + const bitmap = await createImageBitmap(await response.blob()); + const canvas = document.createElement("canvas"); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + const context = canvas.getContext("2d"); + if (!context) throw new Error("No screenshot decoder"); + context.drawImage(bitmap, 0, 0); + bitmap.close(); + const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data; + let count = 0; + let left = canvas.width, top = canvas.height, right = 0, bottom = 0; + for (let index = 0; index < pixels.length; index += 4) { + const red = pixels[index] ?? 255; + const green = pixels[index + 1] ?? 255; + const blue = pixels[index + 2] ?? 0; + if (blue > red * 1.5 && blue > green * 1.5) { + count++; + const x = index / 4 % canvas.width, y = Math.floor(index / 4 / canvas.width); + left = Math.min(left, x); top = Math.min(top, y); + right = Math.max(right, x); bottom = Math.max(bottom, y); + } + } + return { count, bounds: { left: left / canvas.width, top: top / canvas.height, right: right / canvas.width, bottom: bottom / canvas.height } }; + }, png); +} + +async function inkPixels(pad: Locator): Promise { + return (await inkScreenshot(pad)).count; +} + +test.use({ viewport: { width: 1200, height: 1200 } }); + +test("shows live local ink, separate gestures, and a clear tracing-only reset", async ({ page }) => { + await page.goto("/"); + await expect(page.getByText("Development lesson — review pending", { exact: true })).toBeVisible(); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + const pad = page.getByRole("img", { name: "Hindi writing practice area" }); + const bounds = await pad.boundingBox(); + if (!bounds) throw new Error("No writing pad"); + expect(await inkPixels(pad)).toBe(0); + await page.mouse.move(bounds.x + bounds.width * 0.25, bounds.y + bounds.height * 0.4); + await page.mouse.down(); + await page.mouse.move(bounds.x + bounds.width * 0.5, bounds.y + bounds.height * 0.65, { steps: 8 }); + await expect(page.getByRole("status")).toContainText("Drawing…"); + expect(await inkPixels(pad)).toBeGreaterThan(100); + await page.mouse.up(); + await expect(page.getByRole("status")).toContainText("1 stroke drawn"); + await page.mouse.move(bounds.x + bounds.width * 0.65, bounds.y + bounds.height * 0.3); + await page.mouse.down(); + await page.mouse.move(bounds.x + bounds.width * 0.65, bounds.y + bounds.height * 0.7, { steps: 6 }); + await page.mouse.up(); + await expect(page.getByRole("status")).toContainText("2 strokes drawn"); + await page.getByRole("button", { name: "Retry", exact: true }).click(); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + expect(await inkPixels(pad)).toBe(0); +}); + +test("whole-letter guidance has no reveal or ordered hints, and Retry resets ink", async ({ page }) => { + await page.goto("/"); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + await expect(page.getByRole("button", { name: "Reveal", exact: true })).toHaveCount(0); + await expect(page.getByRole("heading", { name: "Focus on the whole letter" })).toBeVisible(); + await expect(page.getByText(/in any stroke order/)).toBeVisible(); + await expect(page.getByRole("listitem")).toHaveCount(0); + const pad = page.getByRole("img", { name: "Hindi writing practice area" }); + const bounds = await pad.boundingBox(); + if (!bounds) throw new Error("No writing pad"); + await page.mouse.move(bounds.x + 40, bounds.y + 40); + await page.mouse.down(); + await page.mouse.move(bounds.x + 100, bounds.y + 100, { steps: 5 }); + await page.mouse.up(); + await expect(page.getByRole("status")).toContainText("1 stroke drawn"); + for (let repeat = 0; repeat < 2; repeat++) { + await page.getByRole("button", { name: "Retry", exact: true }).click(); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + expect(await inkPixels(pad)).toBe(0); + } +}); + +for (const interruption of ["cancel", "capture loss", "blur", "resize", "layout shift", "Retry"] as const) { + test(`${interruption} discards unfinished ink and permits the next gesture`, async ({ page }) => { + await page.goto("/"); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + const pad = page.getByRole("img", { name: "Hindi writing practice area" }); + const bounds = await pad.boundingBox(); + if (!bounds) throw new Error("No pad"); + const start = { x: bounds.x + bounds.width * .2, y: bounds.y + bounds.height * .4 }; + await page.mouse.move(start.x, start.y); + await page.mouse.down(); + await page.mouse.move(start.x + 70, start.y + 50, { steps: 5 }); + await page.mouse.up(); + await expect(page.getByRole("status")).toContainText("1 stroke drawn"); + const finishedInk = await inkScreenshot(pad); + const finished = finishedInk.count; + expect(finished).toBeGreaterThan(100); + await pad.evaluate((element) => element.addEventListener("pointerdown", (event) => { + if (event instanceof PointerEvent) element.setAttribute("data-test-pointer", String(event.pointerId)); + }, { once: true })); + await page.mouse.move(start.x + 120, start.y); + await page.mouse.down(); + await page.mouse.move(start.x + 170, start.y + 60, { steps: 4 }); + await expect(page.getByRole("status")).toContainText("Drawing…"); + const id = Number(await pad.getAttribute("data-test-pointer")); + if (interruption === "cancel") await pad.dispatchEvent("pointercancel", { pointerId: id }); + if (interruption === "capture loss") { + await pad.evaluate((element, pointerId) => element.releasePointerCapture(pointerId), id); + await page.mouse.move(start.x + 180, start.y + 60); + } + if (interruption === "blur") await page.evaluate(() => window.dispatchEvent(new Event("blur"))); + if (interruption === "resize") await page.setViewportSize({ width: 390, height: 844 }); + if (interruption === "layout shift") { + await pad.evaluate((element) => { element.style.transform = "translateY(20px)"; }); + await page.mouse.move(start.x + 180, start.y + 60); + } + if (interruption === "Retry") { + await page.getByRole("button", { name: interruption, exact: true }).focus(); + await page.keyboard.press("Enter"); + } + await expect(page.getByRole("status")).toContainText(interruption === "Retry" ? "Ready to trace" : "Stroke interrupted"); + await page.mouse.up(); + if (interruption === "cancel" || interruption === "capture loss" || interruption === "blur") { + // Pixel coverage tolerates subpixel rasterization while detecting leftover unfinished ink. + expect(Math.abs(await inkPixels(pad) - finished)).toBeLessThan(finished * .05); + } + const nextBounds = await pad.boundingBox(); + if (!nextBounds) throw new Error("No resized pad"); + if (interruption === "resize") { + expect(nextBounds.width).toBeLessThan(bounds.width * .8); + const resizedInk = await inkScreenshot(pad); + expect(resizedInk.count).toBeGreaterThan(50); + for (const edge of ["left", "top", "right", "bottom"] as const) { + expect(Math.abs(resizedInk.bounds[edge] - finishedInk.bounds[edge])).toBeLessThan(.01); + } + } + await page.mouse.move(nextBounds.x + 30, nextBounds.y + 30); + await page.mouse.down(); + await page.mouse.move(nextBounds.x + 80, nextBounds.y + 80, { steps: 4 }); + await page.mouse.up(); + await expect(page.getByRole("status")).toContainText(interruption === "Retry" ? "1 stroke drawn" : "2 strokes drawn"); + }); +} + +test("other pointers do not interrupt capture, and releasing outside does not leave ghost ink", async ({ page }) => { + await page.goto("/"); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + const pad = page.getByRole("img", { name: "Hindi writing practice area" }); + const bounds = await pad.boundingBox(); + if (!bounds) throw new Error("No pad"); + await page.mouse.move(bounds.x + 80, bounds.y + 80); + await page.mouse.down(); + for (const type of ["pointerdown", "pointermove", "pointerup", "pointercancel"]) await pad.dispatchEvent(type, { pointerId: 999, pointerType: "touch", button: 0, buttons: 1, clientX: bounds.x + 200, clientY: bounds.y + 200 }); + await expect(page.getByRole("status")).toContainText("Drawing…"); + await page.mouse.move(bounds.x + bounds.width + 30, bounds.y + 100, { steps: 10 }); + await page.mouse.up(); + await expect(page.getByRole("status")).toContainText("1 stroke drawn"); + const finished = await inkPixels(pad); + await page.mouse.move(bounds.x + 90, bounds.y + 90, { steps: 5 }); + expect(Math.abs(await inkPixels(pad) - finished)).toBeLessThan(finished * .05); + await page.mouse.down(); + await page.mouse.move(bounds.x + 140, bounds.y + 140, { steps: 5 }); + await page.mouse.up(); + await expect(page.getByRole("status")).toContainText("2 strokes drawn"); +}); + + +test("phone layout supports drawing, reset, and visible keyboard focus", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto("/"); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + const pad = page.getByRole("img", { name: "Hindi writing practice area" }); + const bounds = await pad.boundingBox(); + if (!bounds) throw new Error("No phone pad"); + expect(bounds.width).toBeLessThan(390); + await page.mouse.move(bounds.x + bounds.width * .2, bounds.y + bounds.height * .3); + await page.mouse.down(); + await page.mouse.move(bounds.x + bounds.width * .6, bounds.y + bounds.height * .7, { steps: 8 }); + await page.mouse.up(); + await expect(page.getByRole("status")).toContainText("1 stroke drawn"); + expect(await inkPixels(pad)).toBeGreaterThan(100); + await page.keyboard.press("Tab"); + const clear = page.getByRole("button", { name: "Check", exact: true }); + await expect(clear).toBeFocused(); + expect(await clear.evaluate((element) => getComputedStyle(element).outlineStyle)).toBe("solid"); + await page.keyboard.press("Tab"); + const retry = page.getByRole("button", { name: "Retry", exact: true }); + await expect(retry).toBeFocused(); + await page.keyboard.press("Enter"); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + expect(await inkPixels(pad)).toBe(0); +}); + +for (const reducedMotion of ["no-preference", "reduce"] as const) { + test(`Check gives whole-character feedback and Retry resets it (${reducedMotion})`, async ({ page }) => { + await page.emulateMedia({ reducedMotion }); + if (reducedMotion === "reduce") await page.setViewportSize({ width: 390, height: 844 }); + await page.goto("/"); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + const check = page.getByRole("button", { name: "Check", exact: true }); + const retry = page.getByRole("button", { name: "Retry", exact: true }); + await expect(check).toBeDisabled(); + await expect(page.getByRole("button", { name: "Clear", exact: true })).toHaveCount(0); + const pad = page.getByRole("img", { name: "Hindi writing practice area" }); + const bounds = await pad.boundingBox(); + if (!bounds) throw new Error("No pad"); + await page.mouse.move(bounds.x + 20, bounds.y + 20); + await page.mouse.down(); + await expect(check).toBeDisabled(); + await page.mouse.move(bounds.x + 80, bounds.y + 20, { steps: 6 }); + await page.mouse.up(); + await check.click(); + await expect(page.getByRole("status")).toContainText("Not quite yet"); + await expect.poll(() => pad.evaluate((element) => getComputedStyle(element.parentElement ?? element).backgroundColor)).toBe("rgb(252, 234, 234)"); + if (reducedMotion === "reduce") expect(await pad.evaluate((element) => element.parentElement?.getAnimations({ subtree: true }).length)).toBe(0); + await retry.click(); + for (const stroke of [...DEVELOPMENT_LESSON.strokes].reverse()) { + const points = [...stroke.points].reverse(); + for (const [index, point] of points.entries()) { + await page.mouse.move(bounds.x + point.x / 100 * bounds.width, bounds.y + point.y / 100 * bounds.height); + if (index === 0) await page.mouse.down(); + } + await page.mouse.up(); + } + await check.click(); + await expect(page.getByRole("status")).toContainText("That’s it! You matched the letter."); + await expect.poll(() => pad.evaluate((element) => getComputedStyle(element.parentElement ?? element).backgroundColor)).toBe("rgb(229, 245, 233)"); + expect(await inkPixels(pad)).toBeGreaterThan(100); + if (reducedMotion === "reduce") { + expect(await pad.evaluate((element) => element.parentElement?.getAnimations({ subtree: true }).length)).toBe(0); + expect(await page.getByRole("status").evaluate((element) => element.getAnimations({ subtree: true }).length)).toBe(0); + } + await retry.click(); + await expect.poll(() => pad.evaluate((element) => getComputedStyle(element.parentElement ?? element).backgroundColor)).toBe("rgb(255, 255, 255)"); + await expect(page.getByRole("status")).toContainText("Ready to trace"); + await expect(check).toBeDisabled(); + expect(await inkPixels(pad)).toBe(0); + }); +} diff --git a/src/features/writing/__tests__/content/check-character.test.ts b/src/features/writing/__tests__/content/check-character.test.ts new file mode 100644 index 0000000..89b67fb --- /dev/null +++ b/src/features/writing/__tests__/content/check-character.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from "vitest"; +import { checkCharacter } from "../../use-case/tracing"; +import { DEVELOPMENT_LESSON } from "../../use-case/tracing/constants/development-lesson"; +import { parseLesson } from "../../use-case/tracing/helpers"; + +const content = parseLesson(DEVELOPMENT_LESSON); +if (content.kind !== "ready") throw new Error("Invalid development lesson"); +const lesson = content.lesson; +const reference = lesson.strokes.map((stroke) => stroke.points); + +test("accepts the whole shape with any stroke order, direction, or segmentation", () => { + expect(checkCharacter(lesson, reference)).toBe(true); + expect( + checkCharacter( + lesson, + [...reference].reverse().map((stroke) => [...stroke].reverse()), + ), + ).toBe(true); + expect( + checkCharacter( + lesson, + reference.flatMap((stroke) => + stroke.slice(1).map((end, index) => [stroke[index] ?? end, end]), + ), + ), + ).toBe(true); + expect( + checkCharacter( + lesson, + reference.map((stroke) => stroke.map((point) => ({ x: point.x + 2, y: point.y - 2 }))), + ), + ).toBe(true); +}); + +test("rejects empty, incomplete, misplaced, and extra ink", () => { + expect(checkCharacter(lesson, [])).toBe(false); + expect(checkCharacter(lesson, [[{ x: 67, y: 25 }]])).toBe(false); + expect(checkCharacter(lesson, reference.slice(1))).toBe(false); + expect( + checkCharacter( + lesson, + reference.map((stroke) => stroke.map((point) => ({ x: point.x + 18, y: point.y }))), + ), + ).toBe(false); + expect( + checkCharacter(lesson, [ + ...reference, + [ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + ], + ]), + ).toBe(false); +}); diff --git a/src/features/writing/__tests__/content/lesson.test.ts b/src/features/writing/__tests__/content/lesson.test.ts new file mode 100644 index 0000000..194815b --- /dev/null +++ b/src/features/writing/__tests__/content/lesson.test.ts @@ -0,0 +1,98 @@ +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { expect, test } from "vitest"; +import { ErrorBox } from "../../../../pages/index"; +import { DEVELOPMENT_LESSON } from "../../use-case/tracing/constants/development-lesson"; +import { parseLesson } from "../../use-case/tracing/helpers"; + +test("the original development lesson parses with honest review status and reference geometry", () => { + const result = parseLesson(DEVELOPMENT_LESSON); + expect(result.kind).toBe("ready"); + if (result.kind !== "ready") throw new Error("Development lesson did not parse"); + expect(result.lesson.glyph).toBe("ल"); + expect(result.lesson.pronunciation).toBe("la"); + expect(result.lesson.review).toBe("pending"); + expect(result.lesson.strokes).toHaveLength(3); +}); + +test("unusable content fails before interaction instead of becoming handwriting feedback", () => { + const first = DEVELOPMENT_LESSON.strokes[0]; + if (!first) throw new Error("Fixture needs a stroke"); + const invalid = [ + null, + { ...DEVELOPMENT_LESSON, id: " " }, + { ...DEVELOPMENT_LESSON, pronunciation: "" }, + { ...DEVELOPMENT_LESSON, glyph: "" }, + { ...DEVELOPMENT_LESSON, strokes: [] }, + ...[0, -1, NaN, Infinity].map((width) => ({ + ...DEVELOPMENT_LESSON, + bounds: { ...DEVELOPMENT_LESSON.bounds, width }, + })), + ...[ + [], + [{ x: 20, y: 30 }], + [ + { x: 20, y: 30 }, + { x: 20, y: 30 }, + ], + [ + { x: NaN, y: 30 }, + { x: 30, y: 40 }, + ], + [ + { x: -1, y: 20 }, + { x: 40, y: 20 }, + ], + [ + { x: 20, y: 20 }, + { x: 101, y: 20 }, + ], + ].map((points) => ({ + ...DEVELOPMENT_LESSON, + strokes: [{ ...first, points }], + })), + ]; + for (const input of invalid) { + expect(parseLesson(input).kind).toBe("unusable"); + } +}); + +test("the content boundary preserves the reference geometry", () => { + const result = parseLesson({ + ...DEVELOPMENT_LESSON, + strokes: [ + { + points: [ + { x: 70, y: 20 }, + { x: 40, y: 20 }, + ], + }, + { + points: [ + { x: 10, y: 30 }, + { x: 10, y: 70 }, + ], + }, + ], + }); + expect(result.kind).toBe("ready"); + if (result.kind !== "ready") throw new Error("Reference geometry did not parse"); + expect(result.lesson.strokes.map((stroke) => stroke.points[0])).toEqual([ + { x: 70, y: 20 }, + { x: 10, y: 30 }, + ]); +}); + +test("invalid content renders a distinct failure without learner controls or completion", () => { + const content = parseLesson({ ...DEVELOPMENT_LESSON, strokes: [] }); + expect(content.kind).toBe("unusable"); + if (content.kind !== "unusable") throw new Error("Invalid lesson did not fail"); + const markup = renderToStaticMarkup(createElement(ErrorBox, { message: content.message })); + expect(markup).toContain('role="alert"'); + expect(markup).toContain("Lesson unavailable"); + expect(markup).toContain("Its content needs repair."); + expect(markup).not.toContain("Hindi writing practice area"); + expect(markup).not.toContain(" { + const committed = apply([ + { type: "start", pointerId: 1, point: a }, + { type: "extend", point: b }, + { type: "commit", point: c }, + ]); + + expect(committed.attempt).toEqual({ + strokes: [[a, b, c]], + active: null, + feedback: "ready", + }); +}); + +test("extend and commit leave a session unchanged when no stroke is active", () => { + expect(reduceSession(INITIAL_SESSION, { type: "extend", point: a })).toBe(INITIAL_SESSION); + expect(reduceSession(INITIAL_SESSION, { type: "commit", point: a })).toBe(INITIAL_SESSION); +}); + +test("reset clears the attempt", () => { + const session = apply([ + { type: "start", pointerId: 1, point: a }, + { type: "commit", point: b }, + { type: "reset" }, + ]); + + expect(session).toBe(INITIAL_SESSION); + expect(session.attempt).toBe(EMPTY); +}); + +test("feedback follows readiness, then the stroke, then how much is drawn", () => { + const idle = INITIAL_SESSION; + const drawn = apply([ + { type: "start", pointerId: 1, point: a }, + { type: "commit", point: b }, + ]); + const drawing = reduceSession(drawn, { type: "start", pointerId: 3, point: c }); + const interrupted = reduceSession(drawing, { type: "interrupt" }); + + expect(feedbackMessage({ ...idle, ready: false })).toBe("Preparing the writing area…"); + expect(feedbackMessage({ ...idle, ready: true })).toBe("Ready to trace. Follow the faint guide."); + expect(feedbackMessage({ ...drawn, ready: true })).toBe( + "1 stroke drawn. Keep tracing or start again.", + ); + expect(feedbackMessage({ ...drawing, ready: true })).toBe( + "Drawing… Lift your finger or pen to finish this stroke.", + ); + expect(feedbackMessage({ ...interrupted, ready: true })).toBe( + "Stroke interrupted. Your finished strokes are safe. Try again.", + ); + expect(feedbackMessage({ ...interrupted, ready: false })).toBe("Preparing the writing area…"); +}); + +test("checking ignores empty and active attempts, and drawing or retry clears the result", () => { + const content = parseLesson(DEVELOPMENT_LESSON); + if (content.kind !== "ready") throw new Error("Invalid lesson"); + const action: SessionAction = { type: "check", lesson: content.lesson }; + expect(reduceSession(INITIAL_SESSION, action)).toBe(INITIAL_SESSION); + const drawing = reduceSession(INITIAL_SESSION, { type: "start", pointerId: 1, point: a }); + expect(reduceSession(drawing, action)).toBe(drawing); + const committed = reduceSession(drawing, { type: "commit", point: b }); + const checked = reduceSession(committed, action); + expect(checked.attempt.feedback).toBe("incorrect"); + expect(reduceSession(checked, { type: "start", pointerId: 2, point: c }).attempt.feedback).toBe( + "drawing", + ); + expect(reduceSession(checked, { type: "reset" })).toBe(INITIAL_SESSION); +}); diff --git a/src/features/writing/__tests__/fence.test.mjs b/src/features/writing/__tests__/fence.test.mjs new file mode 100644 index 0000000..024a458 --- /dev/null +++ b/src/features/writing/__tests__/fence.test.mjs @@ -0,0 +1,111 @@ +import { strict as assert } from "node:assert"; +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { test } from "node:test"; + +test("Tailwind lint rejects unknown, conflicting and dynamic classes but accepts theme utilities", () => { + const directory = mkdtempSync(join(process.cwd(), ".fence-fixtures-")); + const probe = join(directory, "probe.tsx"); + try { + for (const [expression, rule] of [ + ['"timtim-invalid-class"', "no-unknown-classes"], + ['"flex block"', "no-conflicting-classes"], + ['"flex flex"', "no-duplicate-classes"], + ['`text-${size}`', "no-dynamic-classes"], + ['"flex gap-4 text-accent"', null], + ]) { + writeFileSync(probe, `export const Probe = ({ size }: { size: string }) =>
{size}
;`); + const result = spawnSync("./node_modules/.bin/oxlint", ["--deny-warnings", probe], { encoding: "utf8" }); + const output = result.stdout + result.stderr; + if (rule) { + assert.notEqual(result.status, 0, `${expression} must fail`); + assert.ok(output.includes(rule), output); + } else { + assert.equal(result.status, 0, output); + } + } + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test("the fence rejects invalid types, floating Effects and cross-boundary imports", () => { + const directory = mkdtempSync(join(process.cwd(), ".fence-fixtures-")); + const probe = join(directory, "domain", "probe.ts"); + mkdirSync(join(directory, "domain")); + mkdirSync(join(directory, "browser")); + writeFileSync(join(directory, "browser", "adapter.ts"), "export const adapter = 1;\n"); + writeFileSync(join(directory, "tsconfig.json"), JSON.stringify({ extends: "../tsconfig.json", include: ["domain/probe.ts"], exclude: [] })); + const compile = () => spawnSync("./node_modules/.bin/tsc", ["--noEmit", "--project", join(directory, "tsconfig.json")], { encoding: "utf8" }); + const lint = () => spawnSync("./node_modules/.bin/oxlint", ["--deny-warnings", probe], { encoding: "utf8" }); + try { + writeFileSync(probe, 'export const points: number = "broken";\n'); + let result = compile(); + assert.notEqual(result.status, 0, "ordinary type violations must fail"); + assert.match(result.stdout + result.stderr, /not assignable/); + writeFileSync(probe, 'import { Effect } from "effect";\nEffect.succeed(1);\n'); + result = compile(); + assert.notEqual(result.status, 0, "Effect diagnostics must fail the compiler"); + assert.match(result.stdout + result.stderr, /floatingEffect/); + for (const source of [ + 'import { adapter } from "../browser/adapter"; export const value = adapter;', + 'export { adapter } from "../browser/adapter";', + 'export const adapter = import("../browser/adapter");', + 'import { adapter } from "../components/adapter"; export const value = adapter;', + 'export { adapter } from "../use-case/adapter";', + 'export const adapter = import("../components/adapter");', + 'import { readFile } from "node:fs"; export const adapter = readFile;', + 'import { createElement } from "react"; export const adapter = createElement;', + ]) { + writeFileSync(probe, source); + result = lint(); + assert.notEqual(result.status, 0, "domain platform imports must fail"); + assert.match(result.stdout + result.stderr, /no-platform-in-domain/); + } + for (const source of ["export const platform = window;", "export const platform = document;"]) { + writeFileSync(probe, source); + result = lint(); + assert.notEqual(result.status, 0, "domain browser globals must fail"); + assert.match(result.stdout + result.stderr, /no-restricted-globals/); + } + mkdirSync(join(directory, "pages")); + for (const folder of ["browser", "components", "use-case", "pages"]) { + mkdirSync(join(directory, folder), { recursive: true }); + const browserProbe = join(directory, folder, "probe.ts"); + for (const name of ["void", "fs", "fs/promises", "path", "child_process", "node:fs", "void/handler", "void/kv", "void/storage", "@void/react/pages-server", "@void/react/plugin"]) { + writeFileSync(browserProbe, `import * as dependency from "${name}"; export const value = dependency;`); + result = spawnSync("./node_modules/.bin/oxlint", ["--deny-warnings", browserProbe], { encoding: "utf8" }); + assert.notEqual(result.status, 0, `${folder} server import ${name} must fail`); + assert.match(result.stdout + result.stderr, /no-platform-in-domain/); + } + for (const name of ["react", "@void/react", "@void/react/pages-client", "void/client", "void/client/react", "void/sse/client", "void/live/client", "void/pages-client", "void/pages-prefetch"]) { + writeFileSync(browserProbe, `import * as dependency from "${name}"; export const value = dependency;`); + result = spawnSync("./node_modules/.bin/oxlint", ["--deny-warnings", browserProbe], { encoding: "utf8" }); + assert.equal(result.status, 0, `${folder} browser import ${name} must pass: ${result.stdout}${result.stderr}`); + } + } + for (const folder of ["routes", "providers"]) { + mkdirSync(join(directory, folder)); + for (const target of ["components", "use-case"]) { + const serverProbe = join(directory, folder, "probe.ts"); + writeFileSync(serverProbe, `export { adapter } from "../${target}/adapter";`); + result = spawnSync("./node_modules/.bin/oxlint", ["--deny-warnings", serverProbe], { encoding: "utf8" }); + assert.notEqual(result.status, 0, `${folder} importing ${target} must fail`); + assert.match(result.stdout + result.stderr, /no-platform-in-domain/); + } + } + writeFileSync(probe, 'import type { PointerEvent } from "react"; export type Handler = (event: PointerEvent) => void;\n'); + result = lint(); + assert.equal(result.status, 0, result.stdout + result.stderr); + writeFileSync(probe, 'import { Effect } from "effect"; export const value = Effect.succeed(1);'); + result = lint(); + assert.equal(result.status, 0, result.stdout + result.stderr); + writeFileSync(probe, "export const points: number = 3;\n"); + execFileSync("./node_modules/.bin/tsc", ["--noEmit", "--project", join(directory, "tsconfig.json")]); + result = lint(); + assert.equal(result.status, 0, result.stdout + result.stderr); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/src/features/writing/domain/lessons.ts b/src/features/writing/domain/lessons.ts new file mode 100644 index 0000000..211c00c --- /dev/null +++ b/src/features/writing/domain/lessons.ts @@ -0,0 +1,26 @@ +import { Schema } from "effect"; +import { FINITE, LABEL } from "./utils/constraints"; +import { STROKES_WITHIN_BOUNDS } from "./utils/strokes-within-bounds"; +import { REFERENCE_STROKE } from "./strokes"; + +export const LESSON = Schema.Struct({ + id: LABEL, + glyph: LABEL, + pronunciation: LABEL, + review: Schema.Literal("pending"), + bounds: Schema.Struct({ + x: FINITE, + y: FINITE, + width: FINITE, + height: FINITE, + }), + strokes: Schema.NonEmptyArray(REFERENCE_STROKE), +}) + .check(STROKES_WITHIN_BOUNDS) + .pipe(Schema.brand("Lesson")); + +export interface Lesson extends Schema.Schema.Type {} + +export type LessonContent = + | { readonly kind: "ready"; readonly lesson: Lesson } + | { readonly kind: "unusable"; readonly message: string }; diff --git a/src/features/writing/domain/strokes.ts b/src/features/writing/domain/strokes.ts new file mode 100644 index 0000000..a64e45e --- /dev/null +++ b/src/features/writing/domain/strokes.ts @@ -0,0 +1,65 @@ +import { Schema } from "effect"; +import type { PointerEvent, RefObject } from "react"; +import { FINITE } from "./utils/constraints"; + +export const Point = Schema.Struct({ x: FINITE, y: FINITE }); +export interface Point extends Schema.Schema.Type {} + +export const REFERENCE_STROKE = Schema.Struct({ + points: Schema.NonEmptyArray(Point).check( + Schema.makeFilter((points) => { + const length = points.reduce((total, point, index) => { + const previous = points[index - 1]; + return previous ? total + Math.hypot(point.x - previous.x, point.y - previous.y) : total; + }, 0); + return length > 0 && Number.isFinite(length); + }), + ), +}); + +export const ActiveStroke = Schema.Struct({ + pointerId: FINITE, + points: Schema.Array(Point), +}); +export interface ActiveStroke extends Schema.Schema.Type {} + +export const Attempt = Schema.Struct({ + strokes: Schema.Array(Schema.Array(Point)), + active: Schema.NullOr(ActiveStroke), + feedback: Schema.Literals(["ready", "drawing", "interrupted", "correct", "incorrect"]), +}); +export interface Attempt extends Schema.Schema.Type {} + +const ScreenMatrix = Schema.declare( + (value): value is DOMMatrix => typeof DOMMatrix !== "undefined" && value instanceof DOMMatrix, +); + +export const Capture = Schema.Struct({ + pointerId: FINITE, + matrix: ScreenMatrix, +}); +export interface Capture extends Schema.Schema.Type {} + +const PadRef = Schema.declare( + (value): value is RefObject => + typeof value === "object" && value !== null && "current" in value, +); +const Action = Schema.declare((value): value is () => void => typeof value === "function"); +const PointerHandler = Schema.declare( + (value): value is (event: PointerEvent) => void => typeof value === "function", +); + +export const Drawing = Schema.Struct({ + padRef: PadRef, + ready: Schema.Boolean, + attempt: Attempt, + reset: Action, + check: Action, + abort: Action, + onPointerDown: PointerHandler, + onPointerMove: PointerHandler, + onPointerUp: PointerHandler, + onPointerCancel: PointerHandler, + onLostPointerCapture: PointerHandler, +}); +export interface Drawing extends Schema.Schema.Type {} diff --git a/src/features/writing/domain/utils/constraints.ts b/src/features/writing/domain/utils/constraints.ts new file mode 100644 index 0000000..287708e --- /dev/null +++ b/src/features/writing/domain/utils/constraints.ts @@ -0,0 +1,4 @@ +import { Schema } from "effect"; + +export const FINITE = Schema.Finite; +export const LABEL = Schema.String.check(Schema.makeFilter((value) => value.trim().length > 0)); diff --git a/src/features/writing/domain/utils/strokes-within-bounds.ts b/src/features/writing/domain/utils/strokes-within-bounds.ts new file mode 100644 index 0000000..e811e41 --- /dev/null +++ b/src/features/writing/domain/utils/strokes-within-bounds.ts @@ -0,0 +1,24 @@ +import { Schema } from "effect"; + +type BoundedStrokes = { + readonly bounds: { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; + }; + readonly strokes: ReadonlyArray<{ + readonly points: ReadonlyArray<{ readonly x: number; readonly y: number }>; + }>; +}; + +export const STROKES_WITHIN_BOUNDS = Schema.makeFilter((lesson: BoundedStrokes) => { + const { x, y, width, height } = lesson.bounds; + if (width <= 0 || height <= 0 || !Number.isFinite(x + width) || !Number.isFinite(y + height)) + return false; + return lesson.strokes.every((stroke) => + stroke.points.every( + (point) => point.x >= x && point.x <= x + width && point.y >= y && point.y <= y + height, + ), + ); +}); diff --git a/src/features/writing/use-case/tracing/components/Controls.tsx b/src/features/writing/use-case/tracing/components/Controls.tsx new file mode 100644 index 0000000..e794989 --- /dev/null +++ b/src/features/writing/use-case/tracing/components/Controls.tsx @@ -0,0 +1,32 @@ +export function Controls({ + ready, + canCheck, + onCheck, + onReset, +}: { + readonly ready: boolean; + readonly canCheck: boolean; + readonly onCheck: () => void; + readonly onReset: () => void; +}) { + return ( +
+ + +
+ ); +} diff --git a/src/features/writing/use-case/tracing/components/Explainer.tsx b/src/features/writing/use-case/tracing/components/Explainer.tsx new file mode 100644 index 0000000..a77fe6b --- /dev/null +++ b/src/features/writing/use-case/tracing/components/Explainer.tsx @@ -0,0 +1,15 @@ +export function Explainer() { + return ( + + ); +} diff --git a/src/features/writing/use-case/tracing/components/Feedback.tsx b/src/features/writing/use-case/tracing/components/Feedback.tsx new file mode 100644 index 0000000..a71ea9b --- /dev/null +++ b/src/features/writing/use-case/tracing/components/Feedback.tsx @@ -0,0 +1,45 @@ +import "./feedback.css"; + +export function Feedback({ + message, + outcome, +}: { + readonly message: string; + readonly outcome: string; +}) { + return ( + <> + + + {outcome === "correct" && ( + + )} + {outcome === "incorrect" && ( + + )} + {message} + + + + ); +} diff --git a/src/features/writing/use-case/tracing/components/Header.tsx b/src/features/writing/use-case/tracing/components/Header.tsx new file mode 100644 index 0000000..9b8e172 --- /dev/null +++ b/src/features/writing/use-case/tracing/components/Header.tsx @@ -0,0 +1,22 @@ +export function Header({ + glyph, + pronunciation, +}: { + readonly glyph: string; + readonly pronunciation: string; +}) { + return ( +
+ + {glyph} + +
+ {pronunciation} +

Hindi letter practice

+
+
+ ); +} diff --git a/src/features/writing/use-case/tracing/components/TracingBox.tsx b/src/features/writing/use-case/tracing/components/TracingBox.tsx new file mode 100644 index 0000000..158c349 --- /dev/null +++ b/src/features/writing/use-case/tracing/components/TracingBox.tsx @@ -0,0 +1,105 @@ +import "./feedback.css"; +import type { PointerEvent, RefObject } from "react"; +import type { Lesson } from "../../../domain/lessons"; +import type { Attempt } from "../../../domain/strokes"; +import { pointsAttribute } from "../helpers"; + +export function TracingBox({ + lesson, + attempt, + padRef, + ready, + onPointerDown, + onPointerMove, + onPointerUp, + onPointerCancel, + onLostPointerCapture, +}: { + readonly lesson: Lesson; + readonly attempt: Attempt; + readonly padRef: RefObject; + readonly ready: boolean; + readonly onPointerDown: (event: PointerEvent) => void; + readonly onPointerMove: (event: PointerEvent) => void; + readonly onPointerUp: (event: PointerEvent) => void; + readonly onPointerCancel: (event: PointerEvent) => void; + readonly onLostPointerCapture: (event: PointerEvent) => void; +}) { + const { x, y, width, height } = lesson.bounds; + const viewBox = `${x} ${y} ${width} ${height}`; + + return ( +
+
+ ); +} diff --git a/src/features/writing/use-case/tracing/components/feedback.css b/src/features/writing/use-case/tracing/components/feedback.css new file mode 100644 index 0000000..8db3141 --- /dev/null +++ b/src/features/writing/use-case/tracing/components/feedback.css @@ -0,0 +1,104 @@ +@keyframes letter-check { + from { + stroke-dasharray: 1; + stroke-dashoffset: 1; + } + to { + stroke-dasharray: 1; + stroke-dashoffset: 0; + } +} + +@keyframes letter-retry { + from { + transform: rotate(-50deg); + } + to { + transform: rotate(0); + } +} + +[data-writing-result="correct"] { + background-color: var(--color-success-soft); + color: var(--color-success); +} + +[data-writing-result="incorrect"] { + background-color: var(--color-retry-soft); + color: var(--color-retry); +} + +[data-writing-result="correct"] > svg { + border-color: var(--color-success); +} + +[data-writing-result="incorrect"] > svg { + border-color: var(--color-retry); +} + +[data-writing-feedback="correct"] { + color: var(--color-success); + font-weight: 650; +} + +[data-writing-feedback="incorrect"] { + color: var(--color-retry); + font-weight: 650; +} + +@media (prefers-reduced-motion: no-preference) { + [data-writing-result="correct"] { + animation: canvas-celebrate 650ms cubic-bezier(0.16, 1, 0.3, 1); + } + + [data-writing-result="incorrect"] { + animation: canvas-retry 500ms ease-out; + } + + [data-writing-result="incorrect"] > div { + animation: canvas-nudge 360ms ease-out; + } +} + +@keyframes canvas-celebrate { + 0% { + background-color: #bde8c9; + } + 100% { + background-color: var(--color-success-soft); + } +} + +@keyframes canvas-retry { + from { + background-color: #f4caca; + } + to { + background-color: var(--color-retry-soft); + } +} + +@keyframes canvas-nudge { + 0%, + 100% { + transform: translateX(0); + } + 25% { + transform: translateX(-5px); + } + 55% { + transform: translateX(4px); + } + 80% { + transform: translateX(-2px); + } +} + +@keyframes result-stamp { + from { + transform: translateY(6px) scale(0.65) rotate(-12deg); + } + to { + transform: translateY(0) scale(1) rotate(0); + } +} diff --git a/src/features/writing/use-case/tracing/constants/development-lesson.ts b/src/features/writing/use-case/tracing/constants/development-lesson.ts new file mode 100644 index 0000000..8f49b97 --- /dev/null +++ b/src/features/writing/use-case/tracing/constants/development-lesson.ts @@ -0,0 +1,37 @@ +export const DEVELOPMENT_LESSON = { + id: "la-development-1", + glyph: "ल", + pronunciation: "la", + review: "pending", + bounds: { x: 0, y: 0, width: 100, height: 100 }, + strokes: [ + { + points: [ + { x: 38, y: 73 }, + { x: 30, y: 66 }, + { x: 25, y: 57 }, + { x: 25, y: 48 }, + { x: 29, y: 41 }, + { x: 35, y: 38 }, + { x: 41, y: 41 }, + { x: 45, y: 48 }, + { x: 47, y: 57 }, + { x: 51, y: 49 }, + { x: 58, y: 44 }, + { x: 67, y: 44 }, + ], + }, + { + points: [ + { x: 67, y: 25 }, + { x: 67, y: 76 }, + ], + }, + { + points: [ + { x: 22, y: 25 }, + { x: 77, y: 25 }, + ], + }, + ], +}; diff --git a/src/features/writing/use-case/tracing/helpers/check-character.ts b/src/features/writing/use-case/tracing/helpers/check-character.ts new file mode 100644 index 0000000..0e84e0d --- /dev/null +++ b/src/features/writing/use-case/tracing/helpers/check-character.ts @@ -0,0 +1,54 @@ +import type { Lesson } from "../../../domain/lessons"; +import type { Point } from "../../../domain/strokes"; + +function distanceSquared(point: Point, start: Point, end: Point): number { + const dx = end.x - start.x; + const dy = end.y - start.y; + const length = dx * dx + dy * dy; + const fraction = + length === 0 + ? 0 + : Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) / length)); + return (point.x - start.x - fraction * dx) ** 2 + (point.y - start.y - fraction * dy) ** 2; +} + +function samples(strokes: readonly (readonly Point[])[], spacing: number): Point[] { + return strokes.flatMap((stroke) => + stroke.flatMap((end, index) => { + const start = stroke[index - 1]; + if (!start) return [end]; + const steps = Math.max(1, Math.ceil(Math.hypot(end.x - start.x, end.y - start.y) / spacing)); + return Array.from({ length: steps }, (_, step) => ({ + x: start.x + ((end.x - start.x) * (step + 1)) / steps, + y: start.y + ((end.y - start.y) * (step + 1)) / steps, + })); + }), + ); +} + +function coverage( + points: readonly Point[], + target: readonly (readonly Point[])[], + tolerance: number, +): number { + if (points.length === 0) return 0; + const matched = points.filter((point) => + target.some((stroke) => + stroke.some( + (end, index) => distanceSquared(point, stroke[index - 1] ?? end, end) <= tolerance ** 2, + ), + ), + ); + return matched.length / points.length; +} + +export function checkCharacter(lesson: Lesson, strokes: readonly (readonly Point[])[]): boolean { + const size = Math.min(lesson.bounds.width, lesson.bounds.height); + const reference = lesson.strokes.map((stroke) => stroke.points); + // shortcut: provisional shape tolerances; calibrate with real finger and stylus attempts. + const tolerance = size * 0.055; + return ( + coverage(samples(reference, size / 100), strokes, tolerance) >= 0.9 && + coverage(samples(strokes, size / 100), reference, tolerance) >= 0.9 + ); +} diff --git a/src/features/writing/use-case/tracing/helpers/index.ts b/src/features/writing/use-case/tracing/helpers/index.ts new file mode 100644 index 0000000..1487bcf --- /dev/null +++ b/src/features/writing/use-case/tracing/helpers/index.ts @@ -0,0 +1,43 @@ +import { Result, Schema } from "effect"; +import { type LessonContent, LESSON } from "../../../domain/lessons"; +import type { Attempt, Point } from "../../../domain/strokes"; + +export const EMPTY: Attempt = { strokes: [], active: null, feedback: "ready" }; + +export function sameMatrix(left: DOMMatrix, right: DOMMatrix): boolean { + return ( + left.a === right.a && + left.b === right.b && + left.c === right.c && + left.d === right.d && + left.e === right.e && + left.f === right.f + ); +} + +export function toPoint( + svg: SVGSVGElement, + x: number, + y: number, +): { readonly point: Point; readonly matrix: DOMMatrix } | null { + const matrix = svg.getScreenCTM(); + if (!matrix || matrix.a * matrix.d - matrix.b * matrix.c === 0) return null; + const point = new DOMPoint(x, y).matrixTransform(matrix.inverse()); + if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return null; + return { point: { x: point.x, y: point.y }, matrix }; +} + +export function parseLesson(input: unknown): LessonContent { + const result = Schema.decodeUnknownResult(LESSON)(input); + if (Result.isFailure(result)) { + return { + kind: "unusable", + message: "This lesson cannot be opened. Its content needs repair.", + }; + } + return { kind: "ready", lesson: result.success }; +} + +export function pointsAttribute(points: readonly Point[]): string { + return points.map((point) => `${point.x},${point.y}`).join(" "); +} diff --git a/src/features/writing/use-case/tracing/helpers/session.ts b/src/features/writing/use-case/tracing/helpers/session.ts new file mode 100644 index 0000000..72e9793 --- /dev/null +++ b/src/features/writing/use-case/tracing/helpers/session.ts @@ -0,0 +1,96 @@ +import { checkCharacter } from "./check-character"; +import type { Lesson } from "../../../domain/lessons"; +import type { Attempt, Point } from "../../../domain/strokes"; +import { EMPTY } from "./index"; + +export interface Session { + readonly attempt: Attempt; +} + +export const INITIAL_SESSION: Session = { attempt: EMPTY }; + +export type SessionAction = + | { readonly type: "start"; readonly pointerId: number; readonly point: Point } + | { readonly type: "extend"; readonly point: Point } + | { readonly type: "commit"; readonly point: Point } + | { readonly type: "interrupt" } + | { readonly type: "reset" } + | { readonly type: "check"; readonly lesson: Lesson }; + +export function reduceSession(session: Session, action: SessionAction): Session { + switch (action.type) { + case "start": + return { + ...session, + attempt: { + ...session.attempt, + active: { pointerId: action.pointerId, points: [action.point] }, + feedback: "drawing", + }, + }; + case "extend": + return session.attempt.active + ? { + ...session, + attempt: { + ...session.attempt, + active: { + ...session.attempt.active, + points: [...session.attempt.active.points, action.point], + }, + }, + } + : session; + case "commit": + return session.attempt.active + ? { + ...session, + attempt: { + strokes: [ + ...session.attempt.strokes, + [...session.attempt.active.points, action.point], + ], + active: null, + feedback: "ready", + }, + } + : session; + case "interrupt": + return { + ...session, + attempt: { ...session.attempt, active: null, feedback: "interrupted" }, + }; + case "check": + if (session.attempt.active || session.attempt.strokes.length === 0) return session; + return { + ...session, + attempt: { + ...session.attempt, + feedback: checkCharacter(action.lesson, session.attempt.strokes) + ? "correct" + : "incorrect", + }, + }; + case "reset": + return INITIAL_SESSION; + } +} + +export function feedbackMessage(input: { + readonly attempt: Attempt; + readonly ready: boolean; +}): string { + const count = input.attempt.strokes.length; + let feedback = "Ready to trace. Follow the faint guide."; + if (count > 0) + feedback = `${count} ${count === 1 ? "stroke" : "strokes"} drawn. Keep tracing or start again.`; + if (input.attempt.feedback === "drawing") + feedback = "Drawing… Lift your finger or pen to finish this stroke."; + if (input.attempt.feedback === "interrupted") + feedback = "Stroke interrupted. Your finished strokes are safe. Try again."; + if (input.attempt.feedback === "correct") feedback = "That’s it! You matched the letter."; + if (input.attempt.feedback === "incorrect") + feedback = "Not quite yet. Follow the whole shape and try again."; + if (!input.ready) feedback = "Preparing the writing area…"; + return feedback; +} diff --git a/src/features/writing/use-case/tracing/hooks/use-drawing.ts b/src/features/writing/use-case/tracing/hooks/use-drawing.ts new file mode 100644 index 0000000..c22fa79 --- /dev/null +++ b/src/features/writing/use-case/tracing/hooks/use-drawing.ts @@ -0,0 +1,109 @@ +import { useCallback, useEffect, useReducer, useRef, useState } from "react"; +import type { PointerEvent } from "react"; +import type { Lesson } from "../../../domain/lessons"; +import type { Capture, Drawing } from "../../../domain/strokes"; +import { sameMatrix, toPoint } from "../helpers"; +import { INITIAL_SESSION, reduceSession } from "../helpers/session"; + +export function useDrawing(lesson: Lesson): Drawing { + const padRef = useRef(null); + const captureRef = useRef(null); + const [ready, setReady] = useState(false); + const [session, dispatch] = useReducer(reduceSession, INITIAL_SESSION); + + const release = useCallback((): void => { + const capture = captureRef.current; + captureRef.current = null; + const pad = padRef.current; + if (capture && pad?.hasPointerCapture(capture.pointerId)) + pad.releasePointerCapture(capture.pointerId); + }, []); + + const abort = useCallback((): void => { + if (!captureRef.current) return; + release(); + dispatch({ type: "interrupt" }); + }, [release]); + + useEffect(() => { + const pad = padRef.current; + if (!pad) return; + const observer = new ResizeObserver(() => { + const capture = captureRef.current; + const matrix = pad.getScreenCTM(); + if (capture && (!matrix || !sameMatrix(capture.matrix, matrix))) abort(); + }); + observer.observe(pad); + setReady(true); + const onVisibilityChange = (): void => { + if (document.hidden) abort(); + }; + window.addEventListener("blur", abort); + document.addEventListener("visibilitychange", onVisibilityChange); + window.addEventListener("resize", abort); + window.addEventListener("scroll", abort, true); + window.visualViewport?.addEventListener("resize", abort); + window.visualViewport?.addEventListener("scroll", abort); + return () => { + observer.disconnect(); + window.removeEventListener("blur", abort); + document.removeEventListener("visibilitychange", onVisibilityChange); + window.removeEventListener("resize", abort); + window.removeEventListener("scroll", abort, true); + window.visualViewport?.removeEventListener("resize", abort); + window.visualViewport?.removeEventListener("scroll", abort); + release(); + }; + }, [abort, release]); + + function onPointerDown(event: PointerEvent): void { + if (!ready || captureRef.current || event.button !== 0) return; + const mapped = toPoint(event.currentTarget, event.clientX, event.clientY); + if (!mapped) return; + event.currentTarget.setPointerCapture(event.pointerId); + captureRef.current = { pointerId: event.pointerId, matrix: mapped.matrix }; + dispatch({ type: "start", pointerId: event.pointerId, point: mapped.point }); + } + + function onPointerMove(event: PointerEvent): void { + const capture = captureRef.current; + if (!capture || event.pointerId !== capture.pointerId) return; + const mapped = toPoint(event.currentTarget, event.clientX, event.clientY); + if (!mapped || !sameMatrix(capture.matrix, mapped.matrix)) return abort(); + dispatch({ type: "extend", point: mapped.point }); + } + + function onPointerUp(event: PointerEvent): void { + const capture = captureRef.current; + if (!capture || event.pointerId !== capture.pointerId) return; + const mapped = toPoint(event.currentTarget, event.clientX, event.clientY); + if (!mapped || !sameMatrix(capture.matrix, mapped.matrix)) return abort(); + release(); + dispatch({ type: "commit", point: mapped.point }); + } + + function onInterrupted(event: PointerEvent): void { + if (event.pointerId === captureRef.current?.pointerId) abort(); + } + + function reset(): void { + release(); + dispatch({ type: "reset" }); + } + + return { + padRef, + ready, + attempt: session.attempt, + reset, + check: () => { + if (ready && !captureRef.current) dispatch({ type: "check", lesson }); + }, + abort, + onPointerDown, + onPointerMove, + onPointerUp, + onPointerCancel: onInterrupted, + onLostPointerCapture: onInterrupted, + }; +} diff --git a/src/features/writing/use-case/tracing/index.ts b/src/features/writing/use-case/tracing/index.ts new file mode 100644 index 0000000..4d94fa8 --- /dev/null +++ b/src/features/writing/use-case/tracing/index.ts @@ -0,0 +1,14 @@ +export { parseLesson } from "./helpers"; +export { checkCharacter } from "./helpers/check-character"; +export { feedbackMessage } from "./helpers/session"; +export { useDrawing } from "./hooks/use-drawing"; + +// UI Components +export { Controls } from "./components/Controls"; +export { Explainer } from "./components/Explainer"; +export { Feedback } from "./components/Feedback"; +export { Header } from "./components/Header"; +export { TracingBox } from "./components/TracingBox"; + +// Constants +export { DEVELOPMENT_LESSON } from "./constants/development-lesson"; diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..249505b --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,81 @@ +import "../styles.css"; +import type { Lesson } from "../features/writing/domain/lessons"; +import { + Controls, + DEVELOPMENT_LESSON, + Explainer, + Feedback, + Header, + TracingBox, + feedbackMessage, + parseLesson, + useDrawing, +} from "../features/writing/use-case/tracing"; + +export default function HomePage() { + const content = parseLesson(DEVELOPMENT_LESSON); + if (content.kind === "unusable") { + return ; + } + return ; +} + +export function PracticeArea({ lesson }: { readonly lesson: Lesson }) { + const drawing = useDrawing(lesson); + const feedback = feedbackMessage({ + attempt: drawing.attempt, + ready: drawing.ready, + }); + + return ( +
+
+

+ Strokes +

+

+ Development lesson — review pending +

+
+
+
+
+ + + 0 && !drawing.attempt.active} + onCheck={drawing.check} + onReset={drawing.reset} + /> +
+ +
+
+ ); +} + +export function ErrorBox({ message }: { readonly message: string }) { + return ( +
+

+ Strokes +

+
+

Lesson unavailable

+

{message}

+

Your writing has not been checked. Reload the page to try again.

+
+
+ ); +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..c5090d7 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,27 @@ +@import "tailwindcss"; + +@theme { + --font-sans: system-ui, sans-serif; + --color-page: #edf1f6; + --color-ink: #15223b; + --color-muted: #526079; + --color-line: #c9d3e3; + --color-grid: #dce3ee; + --color-guide: #c6d2f6; + --color-accent: #2348d1; + --color-accent-dark: #17349e; + --color-accent-soft: #e3e9ff; + --color-accent-pressed: #d3ddff; + --color-success: #166534; + --color-success-soft: #e5f5e9; + --color-retry: #a52626; + --color-retry-soft: #fceaea; + --breakpoint-desktop: 760.01px; +} + +@layer base { + html { + @apply bg-page font-sans text-base leading-normal text-ink; + font-synthesis: none; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff73ca5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./.void/tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["void/env", "node", "vite/client"], + "jsx": "react-jsx", + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "plugins": [ + { + "name": "@effect/language-service", + "diagnosticSeverity": { + "floatingEffect": "error", + "anyUnknownInErrorContext": "error", + "missingEffectContext": "error", + "missingEffectError": "error", + "unsafeEffectTypeAssertion": "error" + }, + "ignoreEffectWarningsInTscExitCode": false, + "ignoreEffectErrorsInTscExitCode": false + } + ], + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "*.ts", "*.mts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ac259ee --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vite"; +import { voidPlugin } from "void"; +import { voidReact } from "@void/react/plugin"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [ + tailwindcss(), + voidPlugin(), + voidReact({ react: { compiler: true } }), + ], +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0848f4e --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ test: { include: ["src/features/**/__tests__/content/**/*.test.ts"] } }); diff --git a/void.config.ts b/void.config.ts new file mode 100644 index 0000000..2caea64 --- /dev/null +++ b/void.config.ts @@ -0,0 +1,12 @@ +// Generated by void init. +import { defineConfig } from "void/config"; + +export default defineConfig({ + sourceDir: "src", + head: { + meta: [{ name: "viewport", content: "width=device-width, initial-scale=1" }], + }, + worker: { + compatibility_date: "2026-02-24", + }, +});