diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..533a6cb
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,32 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+
+ - run: pnpm install --frozen-lockfile
+
+ - run: pnpm typecheck
+
+ - run: pnpm test
+
+ - run: pnpm validate-content
+
+ - run: pnpm check-i18n
+
+ - run: pnpm build
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1637f4f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+node_modules
+dist
+dist-ssr
+*.local
+.DS_Store
+coverage
+.turbo
+*.log
+.env
+.env.*
+!.env.example
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..47174e4
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 100
+}
diff --git a/README.md b/README.md
index 58a1dbc..f5a89d7 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,55 @@
-# coderkeys
-An open-source, local-first typing tutor tailored for programmers and technical writers to master code syntax and specific vocabularies.
+# CoderKeys
+
+Open-source, local-first typing tutor tailored for **programmers** and **technical writers** to master code syntax and real-world vocabularies.
+
+## Features
+
+- **Skill-based tracks** — Programmers (code syntax, symbols) and Technical Writers (requirements, documentation)
+- **Bilingual** — Interface and lessons in `en-US` and `pt-BR`
+- **100% local** — No backend, no cloud costs; progress saved in IndexedDB
+- **Contributor-friendly** — Add lessons by creating JSON files in `content/`
+
+## Quick Start
+
+```bash
+git clone https://github.com/iagommendes/coderkeys.git
+cd coderkeys
+pnpm install
+pnpm dev
+```
+
+Open [http://localhost:5173](http://localhost:5173) in your browser.
+
+### Other commands
+
+```bash
+pnpm build # Production build
+pnpm preview # Preview production build locally
+pnpm test # Run all tests
+pnpm validate-content # Validate lesson JSON files
+pnpm check-i18n # Verify translation key parity
+```
+
+## Project Structure
+
+```
+coderkeys/
+├── apps/web/ # React SPA (Vite + Tailwind)
+├── packages/engine/ # Typing engine (WPM, accuracy) — framework-agnostic
+├── packages/schemas/ # Zod schemas for lessons and progress
+├── content/ # Lesson content (contributors edit here)
+└── docs/ # Architecture and planning docs
+```
+
+## Contributing Lessons
+
+1. Create a JSON file in `content/tracks/{track}/{locale}/{module}/`
+2. Follow the schema in `packages/schemas/src/lesson.schema.ts`
+3. Run `pnpm validate-content`
+4. Open a Pull Request
+
+See [docs/PLANEJAMENTO-ARQUITETURA.md](docs/PLANEJAMENTO-ARQUITETURA.md) for the full architecture plan.
+
+## License
+
+MIT
diff --git a/apps/web/.gitignore b/apps/web/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/apps/web/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/apps/web/.oxlintrc.json b/apps/web/.oxlintrc.json
new file mode 100644
index 0000000..6fa991d
--- /dev/null
+++ b/apps/web/.oxlintrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
diff --git a/apps/web/README.md b/apps/web/README.md
new file mode 100644
index 0000000..d6af7e3
--- /dev/null
+++ b/apps/web/README.md
@@ -0,0 +1,32 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the Oxlint configuration
+
+If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
+
+```json
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "options": {
+ "typeAware": true
+ },
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
+```
+
+See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
diff --git a/apps/web/index.html b/apps/web/index.html
new file mode 100644
index 0000000..483cd24
--- /dev/null
+++ b/apps/web/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ CoderKeys — Typing Tutor
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/package.json b/apps/web/package.json
new file mode 100644
index 0000000..eef1fc5
--- /dev/null
+++ b/apps/web/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@coderkeys/web",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview",
+ "typecheck": "tsc -b --noEmit",
+ "test": "vitest run --passWithNoTests",
+ "lint": "eslint src"
+ },
+ "dependencies": {
+ "@coderkeys/engine": "workspace:*",
+ "@coderkeys/schemas": "workspace:*",
+ "clsx": "^2.1.1",
+ "dexie": "^4.0.11",
+ "i18next": "^24.2.3",
+ "i18next-browser-languagedetector": "^8.0.4",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "react-i18next": "^15.4.1",
+ "react-router-dom": "^7.4.0",
+ "tailwind-merge": "^3.0.2",
+ "zustand": "^5.0.3"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.14",
+ "@types/node": "^22.14.0",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.2",
+ "tailwindcss": "^4.0.14",
+ "typescript": "^5.8.2",
+ "vite": "^8.1.0",
+ "vitest": "^3.0.9"
+ }
+}
diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/apps/web/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/web/public/icons.svg b/apps/web/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/apps/web/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx
new file mode 100644
index 0000000..bc19427
--- /dev/null
+++ b/apps/web/src/app/providers.tsx
@@ -0,0 +1,29 @@
+import { useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useSettingsStore } from '@/shared/stores/settings.store';
+import '@/i18n';
+
+export function AppProviders({ children }: { children: React.ReactNode }) {
+ const { i18n } = useTranslation();
+ const { load, uiLocale, loaded } = useSettingsStore();
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ useEffect(() => {
+ if (loaded) {
+ void i18n.changeLanguage(uiLocale);
+ }
+ }, [loaded, uiLocale, i18n]);
+
+ if (!loaded) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ return <>{children}>;
+}
diff --git a/apps/web/src/app/router.tsx b/apps/web/src/app/router.tsx
new file mode 100644
index 0000000..21e7d84
--- /dev/null
+++ b/apps/web/src/app/router.tsx
@@ -0,0 +1,27 @@
+import { BrowserRouter, Navigate, Route, Routes, useParams } from 'react-router-dom';
+import { Layout } from '@/shared/components/Layout';
+import { CatalogPage, TrackPage } from '@/features/catalog/pages/CatalogPage';
+import { LessonPage } from '@/features/lesson/pages/LessonPage';
+import { SettingsPage } from '@/features/settings/pages/SettingsPage';
+
+function TrackRoute() {
+ const { trackId } = useParams<{ trackId: string }>();
+ if (!trackId) return ;
+ return ;
+}
+
+export function AppRouter() {
+ return (
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ );
+}
diff --git a/apps/web/src/assets/hero.png b/apps/web/src/assets/hero.png
new file mode 100644
index 0000000..02251f4
Binary files /dev/null and b/apps/web/src/assets/hero.png differ
diff --git a/apps/web/src/assets/react.svg b/apps/web/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/apps/web/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/web/src/assets/vite.svg b/apps/web/src/assets/vite.svg
new file mode 100644
index 0000000..5101b67
--- /dev/null
+++ b/apps/web/src/assets/vite.svg
@@ -0,0 +1 @@
+
diff --git a/apps/web/src/db/database.ts b/apps/web/src/db/database.ts
new file mode 100644
index 0000000..cb63d64
--- /dev/null
+++ b/apps/web/src/db/database.ts
@@ -0,0 +1,63 @@
+import Dexie, { type EntityTable } from 'dexie';
+import type { Locale } from '@coderkeys/schemas';
+
+export interface UserSettings {
+ id: 'default';
+ uiLocale: Locale;
+ contentLocale: Locale;
+ theme: 'light' | 'dark' | 'system';
+ strictMode: boolean;
+ soundEnabled: boolean;
+}
+
+export interface LessonProgress {
+ lessonId: string;
+ track: string;
+ module: string;
+ bestWpm: number;
+ bestAccuracy: number;
+ bestAwpm: number;
+ attempts: number;
+ completed: boolean;
+ lastPlayedAt: string;
+}
+
+export interface SessionRecord {
+ id: string;
+ lessonId: string;
+ wpm: number;
+ rawWpm: number;
+ accuracy: number;
+ awpm: number;
+ durationMs: number;
+ errorPositions: number[];
+ errorChars: string[];
+ completedAt: string;
+}
+
+export class CoderKeysDatabase extends Dexie {
+ settings!: EntityTable;
+ lessonProgress!: EntityTable;
+ sessions!: EntityTable;
+
+ constructor() {
+ super('coderkeys');
+
+ this.version(1).stores({
+ settings: 'id',
+ lessonProgress: 'lessonId, track, module, lastPlayedAt',
+ sessions: 'id, lessonId, completedAt',
+ });
+ }
+}
+
+export const db = new CoderKeysDatabase();
+
+export const DEFAULT_SETTINGS: UserSettings = {
+ id: 'default',
+ uiLocale: 'en-US',
+ contentLocale: 'en-US',
+ theme: 'dark',
+ strictMode: false,
+ soundEnabled: false,
+};
diff --git a/apps/web/src/db/repositories/progress.repo.ts b/apps/web/src/db/repositories/progress.repo.ts
new file mode 100644
index 0000000..10b2d72
--- /dev/null
+++ b/apps/web/src/db/repositories/progress.repo.ts
@@ -0,0 +1,52 @@
+import type { SessionResult as EngineResult } from '@coderkeys/engine';
+import { db, type LessonProgress, type SessionRecord } from '../database';
+
+export async function getLessonProgress(lessonId: string): Promise {
+ return db.lessonProgress.get(lessonId);
+}
+
+export async function getAllLessonProgress(): Promise {
+ return db.lessonProgress.toArray();
+}
+
+export async function saveSessionResult(
+ lessonId: string,
+ track: string,
+ module: string,
+ result: EngineResult,
+): Promise {
+ const sessionId = crypto.randomUUID();
+ const completedAt = new Date().toISOString();
+
+ const session: SessionRecord = {
+ id: sessionId,
+ lessonId,
+ wpm: result.wpm,
+ rawWpm: result.rawWpm,
+ accuracy: result.accuracy,
+ awpm: result.awpm,
+ durationMs: result.durationMs,
+ errorPositions: result.errorPositions,
+ errorChars: result.errorChars,
+ completedAt,
+ };
+
+ const existing = await db.lessonProgress.get(lessonId);
+
+ const progress: LessonProgress = {
+ lessonId,
+ track,
+ module,
+ bestWpm: Math.max(existing?.bestWpm ?? 0, result.wpm),
+ bestAccuracy: Math.max(existing?.bestAccuracy ?? 0, result.accuracy),
+ bestAwpm: Math.max(existing?.bestAwpm ?? 0, result.awpm),
+ attempts: (existing?.attempts ?? 0) + 1,
+ completed: result.completed || (existing?.completed ?? false),
+ lastPlayedAt: completedAt,
+ };
+
+ await db.transaction('rw', db.sessions, db.lessonProgress, async () => {
+ await db.sessions.put(session);
+ await db.lessonProgress.put(progress);
+ });
+}
diff --git a/apps/web/src/db/repositories/settings.repo.ts b/apps/web/src/db/repositories/settings.repo.ts
new file mode 100644
index 0000000..d7a6c01
--- /dev/null
+++ b/apps/web/src/db/repositories/settings.repo.ts
@@ -0,0 +1,18 @@
+import type { Locale } from '@coderkeys/schemas';
+import { db, DEFAULT_SETTINGS, type UserSettings } from '../database';
+
+export async function getSettings(): Promise {
+ const settings = await db.settings.get('default');
+ return settings ?? DEFAULT_SETTINGS;
+}
+
+export async function saveSettings(partial: Partial>): Promise {
+ const current = await getSettings();
+ const updated: UserSettings = { ...current, ...partial };
+ await db.settings.put(updated);
+ return updated;
+}
+
+export async function setUiLocale(locale: Locale): Promise {
+ return saveSettings({ uiLocale: locale });
+}
diff --git a/apps/web/src/features/catalog/lesson-loader.ts b/apps/web/src/features/catalog/lesson-loader.ts
new file mode 100644
index 0000000..fb54d95
--- /dev/null
+++ b/apps/web/src/features/catalog/lesson-loader.ts
@@ -0,0 +1,84 @@
+import manifest from '@content/manifest.json';
+import { LessonSchema, ManifestSchema, TrackMetaSchema, type Lesson, type Locale, type Manifest } from '@coderkeys/schemas';
+
+const lessonModules = import.meta.glob<{ default: Lesson }>('@content/tracks/**/*.json', {
+ eager: true,
+});
+
+const trackMetaModules = import.meta.glob<{ default: Record & { id: string } }>(
+ '@content/tracks/*/_meta.json',
+ { eager: true },
+);
+
+export interface LessonRef {
+ lessonId: string;
+ trackId: string;
+ moduleId: string;
+ locale: Locale;
+ fileSlug: string;
+}
+
+export interface TrackMetaView {
+ id: string;
+ name: Record;
+ description: Record;
+ defaultModule: string;
+}
+
+function parseLessonPath(path: string): LessonRef | null {
+ const match = path.match(/tracks\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)\.json$/);
+ if (!match) return null;
+
+ const [, trackId, locale, moduleId, fileSlug] = match;
+ if (locale !== 'en-US' && locale !== 'pt-BR') return null;
+
+ return {
+ lessonId: `${trackId}/${moduleId}/${locale}/${fileSlug}`,
+ trackId,
+ moduleId,
+ locale,
+ fileSlug,
+ };
+}
+
+export function getManifest(): Manifest {
+ return ManifestSchema.parse(manifest);
+}
+
+export function getTrackMeta(trackId: string): TrackMetaView | null {
+ const entry = Object.entries(trackMetaModules).find(([path]) => path.includes(`/tracks/${trackId}/`));
+ if (!entry) return null;
+ return TrackMetaSchema.parse(entry[1].default) as TrackMetaView;
+}
+
+export function getAllLessons(): Lesson[] {
+ return Object.entries(lessonModules)
+ .filter(([path]) => !path.includes('_meta.json'))
+ .map(([, mod]) => LessonSchema.parse(mod.default));
+}
+
+export function getLessonById(lessonId: string): Lesson | null {
+ const entry = Object.entries(lessonModules)
+ .filter(([path]) => !path.includes('_meta.json'))
+ .find(([path]) => {
+ const ref = parseLessonPath(path);
+ return ref?.lessonId === lessonId;
+ });
+
+ if (!entry) return null;
+ return LessonSchema.parse(entry[1].default);
+}
+
+export function getLessonsForTrack(trackId: string, locale: Locale): Lesson[] {
+ return getAllLessons().filter((l) => l.track === trackId && l.locale === locale);
+}
+
+export function getLessonsForModule(trackId: string, moduleId: string, locale: Locale): Lesson[] {
+ return getAllLessons().filter(
+ (l) => l.track === trackId && l.module === moduleId && l.locale === locale,
+ );
+}
+
+export function buildLessonId(trackId: string, moduleId: string, locale: Locale, fileSlug: string): string {
+ return `${trackId}/${moduleId}/${locale}/${fileSlug}`;
+}
diff --git a/apps/web/src/features/catalog/pages/CatalogPage.tsx b/apps/web/src/features/catalog/pages/CatalogPage.tsx
new file mode 100644
index 0000000..d3d8c07
--- /dev/null
+++ b/apps/web/src/features/catalog/pages/CatalogPage.tsx
@@ -0,0 +1,118 @@
+import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { useEffect, useState } from 'react';
+import type { LessonProgress } from '@/db/database';
+import { getAllLessonProgress } from '@/db/repositories/progress.repo';
+import { getManifest, getTrackMeta } from '@/features/catalog/lesson-loader';
+import { useSettingsStore } from '@/shared/stores/settings.store';
+import { Card } from '@/shared/components/ui';
+
+export function CatalogPage() {
+ const { t } = useTranslation('catalog');
+ const { t: tc } = useTranslation('common');
+ const contentLocale = useSettingsStore((s) => s.contentLocale);
+ const manifest = getManifest();
+
+ return (
+
+
+
{t('title')}
+
{t('subtitle')}
+
+
+
+ {manifest.tracks.map((track) => {
+ const meta = getTrackMeta(track.id);
+ if (!meta) return null;
+
+ return (
+
+
+
+
{track.icon === 'code' ? '💻' : '📝'}
+
+
{meta.name[contentLocale]}
+
{meta.description[contentLocale]}
+
+ {track.modules.length} {t('modules').toLowerCase()}
+
+
+
+
+
+ );
+ })}
+
+
+
+ {tc('tagline')}
+
+
+ );
+}
+
+export function TrackPage({ trackId }: { trackId: string }) {
+ const { t } = useTranslation('catalog');
+ const contentLocale = useSettingsStore((s) => s.contentLocale);
+ const manifest = getManifest();
+ const meta = getTrackMeta(trackId);
+ const track = manifest.tracks.find((tr) => tr.id === trackId);
+ const [progress, setProgress] = useState([]);
+
+ useEffect(() => {
+ void getAllLessonProgress().then(setProgress);
+ }, []);
+
+ if (!track || !meta) {
+ return Track not found.
;
+ }
+
+ return (
+
+
+
{meta.name[contentLocale]}
+
{meta.description[contentLocale]}
+
+
+ {track.modules.map((mod) => {
+ const lessonSlugs = mod.lessons[contentLocale] ?? [];
+
+ return (
+
+ {mod.id.replace(/-/g, ' ')}
+
+ {lessonSlugs.length === 0 ? (
+ {t('noLessons')}
+ ) : (
+
+ {lessonSlugs.map((slug) => {
+ const lessonId = `${trackId}/${mod.id}/${contentLocale}/${slug}`;
+ const lp = progress.find((p) => p.lessonId === lessonId);
+
+ return (
+
+
+
+
{slug.replace(/^\d{3}-/, '').replace(/-/g, ' ')}
+
+ {lp?.completed ? t('completed') : lp ? `${t('attempts')}: ${lp.attempts}` : t('notStarted')}
+
+
+ {lp && (
+
+
{t('bestWpm')}
+
{lp.bestWpm}
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/web/src/features/lesson/components/CharDisplay.tsx b/apps/web/src/features/lesson/components/CharDisplay.tsx
new file mode 100644
index 0000000..a14d60b
--- /dev/null
+++ b/apps/web/src/features/lesson/components/CharDisplay.tsx
@@ -0,0 +1,37 @@
+import { buildCharStates } from '@coderkeys/engine';
+import { cn } from '@/shared/lib/cn';
+
+interface CharDisplayProps {
+ passage: string;
+ input: string;
+ display: 'code' | 'prose';
+}
+
+export function CharDisplay({ passage, input, display }: CharDisplayProps) {
+ const states = buildCharStates(passage, input);
+
+ return (
+
+ {states.map((state, index) => (
+
+ {state.char === '\n' ? '\n' : state.char}
+
+ ))}
+
+ );
+}
diff --git a/apps/web/src/features/lesson/components/LessonSession.tsx b/apps/web/src/features/lesson/components/LessonSession.tsx
new file mode 100644
index 0000000..0def6c6
--- /dev/null
+++ b/apps/web/src/features/lesson/components/LessonSession.tsx
@@ -0,0 +1,154 @@
+import { useEffect, useReducer, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import { TypingSession } from '@coderkeys/engine';
+import type { Lesson } from '@coderkeys/schemas';
+import { saveSessionResult } from '@/db/repositories/progress.repo';
+import { useSettingsStore } from '@/shared/stores/settings.store';
+import { CharDisplay } from './CharDisplay';
+import { Button, Card } from '@/shared/components/ui';
+import { buildLessonId } from '@/features/catalog/lesson-loader';
+
+interface LessonSessionProps {
+ lesson: Lesson;
+ onComplete: () => void;
+}
+
+function formatTime(ms: number): string {
+ const seconds = Math.floor(ms / 1000);
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
+}
+
+export function LessonSession({ lesson, onComplete }: LessonSessionProps) {
+ const { t } = useTranslation('lesson');
+ const strictMode = useSettingsStore((s) => s.strictMode);
+ const inputRef = useRef(null);
+ const sessionRef = useRef(null);
+ const savedRef = useRef(false);
+ const [, forceRender] = useReducer((x: number) => x + 1, 0);
+
+ useEffect(() => {
+ sessionRef.current = new TypingSession({
+ passage: lesson.content.text,
+ strictMode,
+ });
+ sessionRef.current.prepare();
+ inputRef.current?.focus();
+ savedRef.current = false;
+ }, [lesson, strictMode]);
+
+ const session = sessionRef.current;
+ const state = session?.getState();
+ const metrics = session?.getLiveMetrics();
+ const isComplete = state?.status === 'complete';
+
+ useEffect(() => {
+ if (!isComplete || !session || savedRef.current) return;
+
+ savedRef.current = true;
+ const result = session.getResult();
+ const lessonId = buildLessonId(lesson.track, lesson.module, lesson.locale, lesson.id);
+
+ void saveSessionResult(lessonId, lesson.track, lesson.module, result);
+ }, [isComplete, lesson, session]);
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (!session) return;
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
+ if (event.key === 'Tab') return;
+
+ event.preventDefault();
+
+ if (state?.status === 'ready') {
+ session.start();
+ }
+
+ const handled = session.handleKey(event.key);
+ if (handled) {
+ forceRender();
+ }
+ };
+
+ const goalMet =
+ lesson.goals &&
+ metrics &&
+ (!lesson.goals.minWpm || metrics.wpm >= lesson.goals.minWpm) &&
+ (!lesson.goals.minAccuracy || metrics.accuracy >= lesson.goals.minAccuracy);
+
+ if (isComplete && session) {
+ const result = session.getResult();
+ return (
+
+
+
{t('completeTitle')}
+
{t('completeSubtitle')}
+
+
+
+
+
+
+
+
+
+
+ {goalMet ? t('goalMet') : t('goalMissed')}
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {strictMode &&
{t('strictModeHint')}
}
+
+
inputRef.current?.focus()}
+ role="presentation"
+ >
+
+ {state?.status === 'ready' && (
+
{t('pressToStart')}
+ )}
+
+
+
{}}
+ />
+
+ );
+}
+
+function Stat({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
diff --git a/apps/web/src/features/lesson/pages/LessonPage.tsx b/apps/web/src/features/lesson/pages/LessonPage.tsx
new file mode 100644
index 0000000..361c354
--- /dev/null
+++ b/apps/web/src/features/lesson/pages/LessonPage.tsx
@@ -0,0 +1,36 @@
+import { Link, useNavigate, useParams } from 'react-router-dom';
+import { getLessonById } from '@/features/catalog/lesson-loader';
+import { LessonSession } from '../components/LessonSession';
+import { Button } from '@/shared/components/ui';
+
+export function LessonPage() {
+ const { lessonId: encodedId } = useParams<{ lessonId: string }>();
+ const navigate = useNavigate();
+ const lessonId = encodedId ? decodeURIComponent(encodedId) : '';
+ const lesson = getLessonById(lessonId);
+
+ if (!lesson) {
+ return (
+
+
Lesson not found.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ ← {lesson.track}
+
+
{lesson.title}
+
{lesson.description}
+
+
+
navigate(`/tracks/${lesson.track}`)} />
+
+ );
+}
diff --git a/apps/web/src/features/settings/pages/SettingsPage.tsx b/apps/web/src/features/settings/pages/SettingsPage.tsx
new file mode 100644
index 0000000..d2093e6
--- /dev/null
+++ b/apps/web/src/features/settings/pages/SettingsPage.tsx
@@ -0,0 +1,100 @@
+import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import type { Locale } from '@coderkeys/schemas';
+import { useSettingsStore } from '@/shared/stores/settings.store';
+import { Button, Card } from '@/shared/components/ui';
+
+export function SettingsPage() {
+ const { t, i18n } = useTranslation('settings');
+ const { uiLocale, contentLocale, strictMode, loaded, load, setUiLocale, setContentLocale, setStrictMode } =
+ useSettingsStore();
+ const [saved, setSaved] = useState(false);
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ useEffect(() => {
+ if (loaded) {
+ void i18n.changeLanguage(uiLocale);
+ }
+ }, [loaded, uiLocale, i18n]);
+
+ const handleUiLocale = async (locale: Locale) => {
+ await setUiLocale(locale);
+ await i18n.changeLanguage(locale);
+ setSaved(true);
+ setTimeout(() => setSaved(false), 2000);
+ };
+
+ if (!loaded) return null;
+
+ return (
+
+
{t('title')}
+
+
+
+
+
+ handleUiLocale('en-US')}>
+ English
+
+ handleUiLocale('pt-BR')}>
+ Português (BR)
+
+
+
+
+
+
+
+ void setContentLocale('en-US')}
+ >
+ English
+
+ void setContentLocale('pt-BR')}
+ >
+ Português (BR)
+
+
+
+
+
+
+
{t('strictMode')}
+
{t('strictModeDesc')}
+
+
void setStrictMode(e.target.checked)}
+ className="h-4 w-4 accent-accent"
+ />
+
+
+ {saved && {t('saved')}
}
+
+
+ );
+}
+
+function LocaleButton({
+ active,
+ onClick,
+ children,
+}: {
+ active: boolean;
+ onClick: () => void;
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/i18n/index.ts b/apps/web/src/i18n/index.ts
new file mode 100644
index 0000000..b88b8a5
--- /dev/null
+++ b/apps/web/src/i18n/index.ts
@@ -0,0 +1,45 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+
+import enCommon from './locales/en-US/common.json';
+import enCatalog from './locales/en-US/catalog.json';
+import enLesson from './locales/en-US/lesson.json';
+import enSettings from './locales/en-US/settings.json';
+
+import ptCommon from './locales/pt-BR/common.json';
+import ptCatalog from './locales/pt-BR/catalog.json';
+import ptLesson from './locales/pt-BR/lesson.json';
+import ptSettings from './locales/pt-BR/settings.json';
+
+void i18n
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ resources: {
+ 'en-US': {
+ common: enCommon,
+ catalog: enCatalog,
+ lesson: enLesson,
+ settings: enSettings,
+ },
+ 'pt-BR': {
+ common: ptCommon,
+ catalog: ptCatalog,
+ lesson: ptLesson,
+ settings: ptSettings,
+ },
+ },
+ fallbackLng: 'en-US',
+ supportedLngs: ['en-US', 'pt-BR'],
+ defaultNS: 'common',
+ ns: ['common', 'catalog', 'lesson', 'settings'],
+ interpolation: { escapeValue: false },
+ detection: {
+ order: ['localStorage', 'navigator'],
+ caches: ['localStorage'],
+ lookupLocalStorage: 'coderkeys-ui-locale',
+ },
+ });
+
+export default i18n;
diff --git a/apps/web/src/i18n/locales/en-US/catalog.json b/apps/web/src/i18n/locales/en-US/catalog.json
new file mode 100644
index 0000000..998466d
--- /dev/null
+++ b/apps/web/src/i18n/locales/en-US/catalog.json
@@ -0,0 +1,11 @@
+{
+ "title": "Lesson Catalog",
+ "subtitle": "Choose a track and module to start practicing",
+ "modules": "Modules",
+ "lessons": "Lessons",
+ "noLessons": "No lessons available for this language yet.",
+ "bestWpm": "Best WPM",
+ "attempts": "Attempts",
+ "completed": "Completed",
+ "notStarted": "Not started"
+}
diff --git a/apps/web/src/i18n/locales/en-US/common.json b/apps/web/src/i18n/locales/en-US/common.json
new file mode 100644
index 0000000..8ba6382
--- /dev/null
+++ b/apps/web/src/i18n/locales/en-US/common.json
@@ -0,0 +1,17 @@
+{
+ "appName": "CoderKeys",
+ "tagline": "Typing skills for programmers and technical writers",
+ "nav": {
+ "catalog": "Catalog",
+ "settings": "Settings",
+ "github": "GitHub"
+ },
+ "actions": {
+ "start": "Start",
+ "retry": "Try again",
+ "back": "Back",
+ "save": "Save"
+ },
+ "difficulty": "Difficulty",
+ "minutes": "min"
+}
diff --git a/apps/web/src/i18n/locales/en-US/lesson.json b/apps/web/src/i18n/locales/en-US/lesson.json
new file mode 100644
index 0000000..2a00b42
--- /dev/null
+++ b/apps/web/src/i18n/locales/en-US/lesson.json
@@ -0,0 +1,12 @@
+{
+ "liveWpm": "WPM",
+ "liveAccuracy": "Accuracy",
+ "liveAwpm": "AWPM",
+ "elapsed": "Time",
+ "completeTitle": "Lesson complete!",
+ "completeSubtitle": "Great job. Your results have been saved locally.",
+ "goalMet": "Goal met",
+ "goalMissed": "Keep practicing",
+ "pressToStart": "Click here or press any key to start",
+ "strictModeHint": "Strict mode: errors block progress"
+}
diff --git a/apps/web/src/i18n/locales/en-US/settings.json b/apps/web/src/i18n/locales/en-US/settings.json
new file mode 100644
index 0000000..ca96215
--- /dev/null
+++ b/apps/web/src/i18n/locales/en-US/settings.json
@@ -0,0 +1,9 @@
+{
+ "title": "Settings",
+ "uiLocale": "Interface language",
+ "contentLocale": "Lesson content language",
+ "strictMode": "Strict mode",
+ "strictModeDesc": "Block typing when a character is incorrect",
+ "theme": "Theme",
+ "saved": "Settings saved"
+}
diff --git a/apps/web/src/i18n/locales/pt-BR/catalog.json b/apps/web/src/i18n/locales/pt-BR/catalog.json
new file mode 100644
index 0000000..3878d03
--- /dev/null
+++ b/apps/web/src/i18n/locales/pt-BR/catalog.json
@@ -0,0 +1,11 @@
+{
+ "title": "Catálogo de Lições",
+ "subtitle": "Escolha uma trilha e módulo para começar a praticar",
+ "modules": "Módulos",
+ "lessons": "Lições",
+ "noLessons": "Nenhuma lição disponível para este idioma ainda.",
+ "bestWpm": "Melhor WPM",
+ "attempts": "Tentativas",
+ "completed": "Concluída",
+ "notStarted": "Não iniciada"
+}
diff --git a/apps/web/src/i18n/locales/pt-BR/common.json b/apps/web/src/i18n/locales/pt-BR/common.json
new file mode 100644
index 0000000..0e83180
--- /dev/null
+++ b/apps/web/src/i18n/locales/pt-BR/common.json
@@ -0,0 +1,17 @@
+{
+ "appName": "CoderKeys",
+ "tagline": "Habilidades de digitação para programadores e escritores técnicos",
+ "nav": {
+ "catalog": "Catálogo",
+ "settings": "Configurações",
+ "github": "GitHub"
+ },
+ "actions": {
+ "start": "Iniciar",
+ "retry": "Tentar novamente",
+ "back": "Voltar",
+ "save": "Salvar"
+ },
+ "difficulty": "Dificuldade",
+ "minutes": "min"
+}
diff --git a/apps/web/src/i18n/locales/pt-BR/lesson.json b/apps/web/src/i18n/locales/pt-BR/lesson.json
new file mode 100644
index 0000000..af1cd9e
--- /dev/null
+++ b/apps/web/src/i18n/locales/pt-BR/lesson.json
@@ -0,0 +1,12 @@
+{
+ "liveWpm": "PPM",
+ "liveAccuracy": "Precisão",
+ "liveAwpm": "PPM ajustado",
+ "elapsed": "Tempo",
+ "completeTitle": "Lição concluída!",
+ "completeSubtitle": "Ótimo trabalho. Seus resultados foram salvos localmente.",
+ "goalMet": "Meta atingida",
+ "goalMissed": "Continue praticando",
+ "pressToStart": "Clique aqui ou pressione qualquer tecla para começar",
+ "strictModeHint": "Modo rigoroso: erros bloqueiam o progresso"
+}
diff --git a/apps/web/src/i18n/locales/pt-BR/settings.json b/apps/web/src/i18n/locales/pt-BR/settings.json
new file mode 100644
index 0000000..e3ca90a
--- /dev/null
+++ b/apps/web/src/i18n/locales/pt-BR/settings.json
@@ -0,0 +1,9 @@
+{
+ "title": "Configurações",
+ "uiLocale": "Idioma da interface",
+ "contentLocale": "Idioma do conteúdo das lições",
+ "strictMode": "Modo rigoroso",
+ "strictModeDesc": "Bloquear digitação quando um caractere estiver incorreto",
+ "theme": "Tema",
+ "saved": "Configurações salvas"
+}
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
new file mode 100644
index 0000000..7d88242
--- /dev/null
+++ b/apps/web/src/index.css
@@ -0,0 +1,23 @@
+@import 'tailwindcss';
+
+@theme {
+ --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
+ --color-surface: #0f1419;
+ --color-surface-elevated: #1a2332;
+ --color-border: #2d3a4f;
+ --color-accent: #3b82f6;
+ --color-accent-hover: #2563eb;
+ --color-success: #22c55e;
+ --color-error: #ef4444;
+ --color-muted: #94a3b8;
+ --color-foreground: #e2e8f0;
+}
+
+body {
+ @apply m-0 min-h-screen bg-surface font-sans text-foreground antialiased;
+}
+
+#root {
+ @apply min-h-screen;
+}
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
new file mode 100644
index 0000000..a41871f
--- /dev/null
+++ b/apps/web/src/main.tsx
@@ -0,0 +1,14 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { AppProviders } from './app/providers';
+import { AppRouter } from './app/router';
+import './index.css';
+import './i18n';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+);
diff --git a/apps/web/src/shared/components/Layout.tsx b/apps/web/src/shared/components/Layout.tsx
new file mode 100644
index 0000000..67d770b
--- /dev/null
+++ b/apps/web/src/shared/components/Layout.tsx
@@ -0,0 +1,54 @@
+import { Link, NavLink } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { cn } from '@/shared/lib/cn';
+
+export function Header() {
+ const { t } = useTranslation('common');
+
+ return (
+
+
+
+
⌨
+
{t('appName')}
+
+
+
+
+
+ );
+}
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/apps/web/src/shared/components/ui.tsx b/apps/web/src/shared/components/ui.tsx
new file mode 100644
index 0000000..af4d54d
--- /dev/null
+++ b/apps/web/src/shared/components/ui.tsx
@@ -0,0 +1,32 @@
+import { cn } from '@/shared/lib/cn';
+
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: 'primary' | 'secondary' | 'ghost';
+}
+
+export function Button({ className, variant = 'primary', ...props }: ButtonProps) {
+ return (
+
+ );
+}
+
+export function Card({ className, ...props }: React.HTMLAttributes) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/shared/lib/cn.ts b/apps/web/src/shared/lib/cn.ts
new file mode 100644
index 0000000..2819a83
--- /dev/null
+++ b/apps/web/src/shared/lib/cn.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/web/src/shared/stores/settings.store.ts b/apps/web/src/shared/stores/settings.store.ts
new file mode 100644
index 0000000..f57838c
--- /dev/null
+++ b/apps/web/src/shared/stores/settings.store.ts
@@ -0,0 +1,46 @@
+import { create } from 'zustand';
+import type { Locale } from '@coderkeys/schemas';
+import { getSettings, saveSettings } from '@/db/repositories/settings.repo';
+
+interface SettingsState {
+ uiLocale: Locale;
+ contentLocale: Locale;
+ strictMode: boolean;
+ loaded: boolean;
+ load: () => Promise;
+ setUiLocale: (locale: Locale) => Promise;
+ setContentLocale: (locale: Locale) => Promise;
+ setStrictMode: (strict: boolean) => Promise;
+}
+
+export const useSettingsStore = create((set) => ({
+ uiLocale: 'en-US',
+ contentLocale: 'en-US',
+ strictMode: false,
+ loaded: false,
+
+ load: async () => {
+ const settings = await getSettings();
+ set({
+ uiLocale: settings.uiLocale,
+ contentLocale: settings.contentLocale,
+ strictMode: settings.strictMode,
+ loaded: true,
+ });
+ },
+
+ setUiLocale: async (locale) => {
+ await saveSettings({ uiLocale: locale });
+ set({ uiLocale: locale });
+ },
+
+ setContentLocale: async (locale) => {
+ await saveSettings({ contentLocale: locale });
+ set({ contentLocale: locale });
+ },
+
+ setStrictMode: async (strict) => {
+ await saveSettings({ strictMode: strict });
+ set({ strictMode: strict });
+ },
+}));
diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts
new file mode 100644
index 0000000..b1e93b2
--- /dev/null
+++ b/apps/web/src/vite-env.d.ts
@@ -0,0 +1,13 @@
+///
+
+declare module '@content/manifest.json' {
+ import type { Manifest } from '@coderkeys/schemas';
+ const manifest: Manifest;
+ export default manifest;
+}
+
+declare module '@content/tracks/**/*.json' {
+ import type { Lesson, TrackMeta } from '@coderkeys/schemas';
+ const value: { default: Lesson | TrackMeta };
+ export default value;
+}
diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json
new file mode 100644
index 0000000..1d252e8
--- /dev/null
+++ b/apps/web/tsconfig.app.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "resolveJsonModule": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"],
+ "@content/*": ["../../content/*"],
+ "@coderkeys/engine": ["../../packages/engine/src/index.ts"],
+ "@coderkeys/schemas": ["../../packages/schemas/src/index.ts"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/apps/web/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json
new file mode 100644
index 0000000..8455dcb
--- /dev/null
+++ b/apps/web/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
new file mode 100644
index 0000000..53f69c3
--- /dev/null
+++ b/apps/web/vite.config.ts
@@ -0,0 +1,21 @@
+import path from 'node:path';
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import tailwindcss from '@tailwindcss/vite';
+
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ '@content': path.resolve(__dirname, '../../content'),
+ '@coderkeys/engine': path.resolve(__dirname, '../../packages/engine/src/index.ts'),
+ '@coderkeys/schemas': path.resolve(__dirname, '../../packages/schemas/src/index.ts'),
+ },
+ },
+ server: {
+ fs: {
+ allow: ['..', '../..'],
+ },
+ },
+});
diff --git a/content/CONTRIBUTING-CONTENT.md b/content/CONTRIBUTING-CONTENT.md
new file mode 100644
index 0000000..cc96c73
--- /dev/null
+++ b/content/CONTRIBUTING-CONTENT.md
@@ -0,0 +1,44 @@
+# Contributing Content
+
+Thank you for contributing lessons to CoderKeys!
+
+## Where to add lessons
+
+```
+content/tracks/{track}/{locale}/{module}/{id}.json
+```
+
+Example:
+
+```
+content/tracks/programmers/en-US/syntax-brackets/006-destructuring.json
+```
+
+## Lesson file format
+
+See `packages/schemas/src/lesson.schema.ts` for the full schema.
+
+Required fields:
+
+- `id` — format `001-slug` (three-digit prefix + kebab-case slug)
+- `track`, `module`, `locale` — must match the folder path
+- `title`, `description` — in the lesson's language
+- `difficulty` — 1 to 5
+- `content.text` — exact text the user must type (min 20 characters)
+- `mode` — `code`, `prose`, or `translation`
+
+## Validation
+
+Before opening a PR:
+
+```bash
+pnpm validate-content
+```
+
+## Guidelines
+
+- Use realistic code snippets or professional technical prose
+- No secrets, credentials, or offensive content
+- Keep lessons between 50–500 characters (ideal: 100–200)
+- Progress difficulty gradually within a module
+- Add the lesson slug to `content/manifest.json` under the correct module
diff --git a/content/manifest.json b/content/manifest.json
new file mode 100644
index 0000000..e8c482b
--- /dev/null
+++ b/content/manifest.json
@@ -0,0 +1,57 @@
+{
+ "version": "1.0.0",
+ "tracks": [
+ {
+ "id": "programmers",
+ "icon": "code",
+ "modules": [
+ {
+ "id": "syntax-brackets",
+ "skills": ["brackets", "symbols", "curly-braces"],
+ "lessons": {
+ "en-US": [
+ "001-curly-basics",
+ "002-array-brackets",
+ "003-pipe-chain",
+ "004-template-literals",
+ "005-arrow-functions"
+ ],
+ "pt-BR": [
+ "001-chaves-basico",
+ "002-colchetes-array",
+ "003-cadeia-pipe",
+ "004-template-literals",
+ "005-arrow-functions"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "technical-writers",
+ "icon": "document",
+ "modules": [
+ {
+ "id": "requirements",
+ "skills": ["user-stories", "acceptance-criteria"],
+ "lessons": {
+ "en-US": [
+ "001-user-story",
+ "002-acceptance-criteria",
+ "003-given-when-then",
+ "004-api-requirement",
+ "005-edge-case"
+ ],
+ "pt-BR": [
+ "001-historia-usuario",
+ "002-criterios-aceite",
+ "003-dado-quando-entao",
+ "004-requisito-api",
+ "005-caso-limite"
+ ]
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/content/tracks/programmers/_meta.json b/content/tracks/programmers/_meta.json
new file mode 100644
index 0000000..58d4f41
--- /dev/null
+++ b/content/tracks/programmers/_meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "programmers",
+ "name": {
+ "en-US": "Programmers",
+ "pt-BR": "Programadores"
+ },
+ "description": {
+ "en-US": "Master code syntax, symbols, and common patterns.",
+ "pt-BR": "Domine sintaxe de código, símbolos e padrões comuns."
+ },
+ "defaultModule": "syntax-brackets"
+}
diff --git a/content/tracks/programmers/en-US/syntax-brackets/001-curly-basics.json b/content/tracks/programmers/en-US/syntax-brackets/001-curly-basics.json
new file mode 100644
index 0000000..37950c5
--- /dev/null
+++ b/content/tracks/programmers/en-US/syntax-brackets/001-curly-basics.json
@@ -0,0 +1,23 @@
+{
+ "id": "001-curly-basics",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "en-US",
+ "title": "Curly Braces Basics",
+ "description": "Practice typing opening and closing curly braces in common patterns.",
+ "difficulty": 1,
+ "estimatedMinutes": 2,
+ "skills": ["curly-braces", "indentation"],
+ "tags": ["javascript", "typescript"],
+ "mode": "code",
+ "content": {
+ "text": "const obj = { key: 'value' };\nif (true) {\n console.log('hello');\n}",
+ "display": "code",
+ "language": "javascript"
+ },
+ "goals": { "minWpm": 25, "minAccuracy": 90 },
+ "hints": { "keyboard": ["{", "}"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/programmers/en-US/syntax-brackets/002-array-brackets.json b/content/tracks/programmers/en-US/syntax-brackets/002-array-brackets.json
new file mode 100644
index 0000000..8d83ed0
--- /dev/null
+++ b/content/tracks/programmers/en-US/syntax-brackets/002-array-brackets.json
@@ -0,0 +1,23 @@
+{
+ "id": "002-array-brackets",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "en-US",
+ "title": "Array Brackets",
+ "description": "Type array literals and indexed access with square brackets.",
+ "difficulty": 1,
+ "estimatedMinutes": 2,
+ "skills": ["array-brackets", "indexing"],
+ "tags": ["javascript"],
+ "mode": "code",
+ "content": {
+ "text": "const items = ['a', 'b', 'c'];\nconst first = items[0];\nconst nested = [[1], [2]];",
+ "display": "code",
+ "language": "javascript"
+ },
+ "goals": { "minWpm": 30, "minAccuracy": 90 },
+ "hints": { "keyboard": ["[", "]"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/programmers/en-US/syntax-brackets/003-pipe-chain.json b/content/tracks/programmers/en-US/syntax-brackets/003-pipe-chain.json
new file mode 100644
index 0000000..bb1df55
--- /dev/null
+++ b/content/tracks/programmers/en-US/syntax-brackets/003-pipe-chain.json
@@ -0,0 +1,23 @@
+{
+ "id": "003-pipe-chain",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "en-US",
+ "title": "Pipe and Chain",
+ "description": "Practice method chaining and pipe operators.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["method-chaining", "pipes"],
+ "tags": ["typescript", "rxjs"],
+ "mode": "code",
+ "content": {
+ "text": "const result = data\n .filter((x) => x.active)\n .map((x) => x.name)\n .join(', ');",
+ "display": "code",
+ "language": "typescript"
+ },
+ "goals": { "minWpm": 30, "minAccuracy": 88 },
+ "hints": { "keyboard": [".", "(", ")"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/programmers/en-US/syntax-brackets/004-template-literals.json b/content/tracks/programmers/en-US/syntax-brackets/004-template-literals.json
new file mode 100644
index 0000000..7b8f9cc
--- /dev/null
+++ b/content/tracks/programmers/en-US/syntax-brackets/004-template-literals.json
@@ -0,0 +1,23 @@
+{
+ "id": "004-template-literals",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "en-US",
+ "title": "Template Literals",
+ "description": "Type template strings with embedded expressions.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["template-literals", "interpolation"],
+ "tags": ["javascript", "es6"],
+ "mode": "code",
+ "content": {
+ "text": "const name = 'Coder';\nconst msg = `Hello, ${name}!`;\nconst path = `/api/users/${id}`;",
+ "display": "code",
+ "language": "javascript"
+ },
+ "goals": { "minWpm": 28, "minAccuracy": 90 },
+ "hints": { "keyboard": ["`", "${", "}"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/programmers/en-US/syntax-brackets/005-arrow-functions.json b/content/tracks/programmers/en-US/syntax-brackets/005-arrow-functions.json
new file mode 100644
index 0000000..c1e05a7
--- /dev/null
+++ b/content/tracks/programmers/en-US/syntax-brackets/005-arrow-functions.json
@@ -0,0 +1,23 @@
+{
+ "id": "005-arrow-functions",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "en-US",
+ "title": "Arrow Functions",
+ "description": "Practice arrow function syntax and implicit returns.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["arrow-functions", "symbols"],
+ "tags": ["javascript", "typescript"],
+ "mode": "code",
+ "content": {
+ "text": "const add = (a, b) => a + b;\nconst greet = (name) => `Hi, ${name}`;\nitems.map((x) => x.id);",
+ "display": "code",
+ "language": "javascript"
+ },
+ "goals": { "minWpm": 32, "minAccuracy": 90 },
+ "hints": { "keyboard": ["=>", "(", ")"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/programmers/pt-BR/syntax-brackets/001-chaves-basico.json b/content/tracks/programmers/pt-BR/syntax-brackets/001-chaves-basico.json
new file mode 100644
index 0000000..632de98
--- /dev/null
+++ b/content/tracks/programmers/pt-BR/syntax-brackets/001-chaves-basico.json
@@ -0,0 +1,23 @@
+{
+ "id": "001-chaves-basico",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "pt-BR",
+ "title": "Chaves Básicas",
+ "description": "Pratique digitar chaves de abertura e fechamento em padrões comuns.",
+ "difficulty": 1,
+ "estimatedMinutes": 2,
+ "skills": ["curly-braces", "indentation"],
+ "tags": ["javascript", "typescript"],
+ "mode": "code",
+ "content": {
+ "text": "const obj = { chave: 'valor' };\nif (true) {\n console.log('ola');\n}",
+ "display": "code",
+ "language": "javascript"
+ },
+ "goals": { "minWpm": 25, "minAccuracy": 90 },
+ "hints": { "keyboard": ["{", "}"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/programmers/pt-BR/syntax-brackets/002-colchetes-array.json b/content/tracks/programmers/pt-BR/syntax-brackets/002-colchetes-array.json
new file mode 100644
index 0000000..a90b215
--- /dev/null
+++ b/content/tracks/programmers/pt-BR/syntax-brackets/002-colchetes-array.json
@@ -0,0 +1,23 @@
+{
+ "id": "002-colchetes-array",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "pt-BR",
+ "title": "Colchetes de Array",
+ "description": "Digite literais de array e acesso por índice com colchetes.",
+ "difficulty": 1,
+ "estimatedMinutes": 2,
+ "skills": ["array-brackets", "indexing"],
+ "tags": ["javascript"],
+ "mode": "code",
+ "content": {
+ "text": "const itens = ['a', 'b', 'c'];\nconst primeiro = itens[0];\nconst aninhado = [[1], [2]];",
+ "display": "code",
+ "language": "javascript"
+ },
+ "goals": { "minWpm": 30, "minAccuracy": 90 },
+ "hints": { "keyboard": ["[", "]"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/programmers/pt-BR/syntax-brackets/003-cadeia-pipe.json b/content/tracks/programmers/pt-BR/syntax-brackets/003-cadeia-pipe.json
new file mode 100644
index 0000000..e4556a7
--- /dev/null
+++ b/content/tracks/programmers/pt-BR/syntax-brackets/003-cadeia-pipe.json
@@ -0,0 +1,23 @@
+{
+ "id": "003-cadeia-pipe",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "pt-BR",
+ "title": "Cadeia de Métodos",
+ "description": "Pratique encadeamento de métodos e operadores pipe.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["method-chaining", "pipes"],
+ "tags": ["typescript"],
+ "mode": "code",
+ "content": {
+ "text": "const resultado = dados\n .filter((x) => x.ativo)\n .map((x) => x.nome)\n .join(', ');",
+ "display": "code",
+ "language": "typescript"
+ },
+ "goals": { "minWpm": 30, "minAccuracy": 88 },
+ "hints": { "keyboard": [".", "(", ")"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/programmers/pt-BR/syntax-brackets/004-template-literals.json b/content/tracks/programmers/pt-BR/syntax-brackets/004-template-literals.json
new file mode 100644
index 0000000..ed68f70
--- /dev/null
+++ b/content/tracks/programmers/pt-BR/syntax-brackets/004-template-literals.json
@@ -0,0 +1,23 @@
+{
+ "id": "004-template-literals",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "pt-BR",
+ "title": "Template Literals",
+ "description": "Digite template strings com expressões embutidas.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["template-literals", "interpolation"],
+ "tags": ["javascript", "es6"],
+ "mode": "code",
+ "content": {
+ "text": "const nome = 'Coder';\nconst msg = `Ola, ${nome}!`;\nconst rota = `/api/usuarios/${id}`;",
+ "display": "code",
+ "language": "javascript"
+ },
+ "goals": { "minWpm": 28, "minAccuracy": 90 },
+ "hints": { "keyboard": ["`", "${", "}"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/programmers/pt-BR/syntax-brackets/005-arrow-functions.json b/content/tracks/programmers/pt-BR/syntax-brackets/005-arrow-functions.json
new file mode 100644
index 0000000..a0fd598
--- /dev/null
+++ b/content/tracks/programmers/pt-BR/syntax-brackets/005-arrow-functions.json
@@ -0,0 +1,23 @@
+{
+ "id": "005-arrow-functions",
+ "module": "syntax-brackets",
+ "track": "programmers",
+ "locale": "pt-BR",
+ "title": "Arrow Functions",
+ "description": "Pratique sintaxe de arrow functions e retornos implícitos.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["arrow-functions", "symbols"],
+ "tags": ["javascript", "typescript"],
+ "mode": "code",
+ "content": {
+ "text": "const somar = (a, b) => a + b;\nconst saudar = (nome) => `Oi, ${nome}`;\nitens.map((x) => x.id);",
+ "display": "code",
+ "language": "javascript"
+ },
+ "goals": { "minWpm": 32, "minAccuracy": 90 },
+ "hints": { "keyboard": ["=>", "(", ")"] },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/_meta.json b/content/tracks/technical-writers/_meta.json
new file mode 100644
index 0000000..09d508d
--- /dev/null
+++ b/content/tracks/technical-writers/_meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "technical-writers",
+ "name": {
+ "en-US": "Technical Writers",
+ "pt-BR": "Escritores Técnicos"
+ },
+ "description": {
+ "en-US": "Practice technical writing, requirements, and documentation.",
+ "pt-BR": "Pratique escrita técnica, requisitos e documentação."
+ },
+ "defaultModule": "requirements"
+}
diff --git a/content/tracks/technical-writers/en-US/requirements/001-user-story.json b/content/tracks/technical-writers/en-US/requirements/001-user-story.json
new file mode 100644
index 0000000..c794579
--- /dev/null
+++ b/content/tracks/technical-writers/en-US/requirements/001-user-story.json
@@ -0,0 +1,21 @@
+{
+ "id": "001-user-story",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "en-US",
+ "title": "User Story Format",
+ "description": "Practice typing standard user story sentences.",
+ "difficulty": 1,
+ "estimatedMinutes": 2,
+ "skills": ["user-stories"],
+ "tags": ["agile", "requirements"],
+ "mode": "prose",
+ "content": {
+ "text": "As a registered user, I want to reset my password so that I can regain access to my account.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 40, "minAccuracy": 95 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/en-US/requirements/002-acceptance-criteria.json b/content/tracks/technical-writers/en-US/requirements/002-acceptance-criteria.json
new file mode 100644
index 0000000..d3b7dce
--- /dev/null
+++ b/content/tracks/technical-writers/en-US/requirements/002-acceptance-criteria.json
@@ -0,0 +1,21 @@
+{
+ "id": "002-acceptance-criteria",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "en-US",
+ "title": "Acceptance Criteria",
+ "description": "Type clear acceptance criteria for feature requirements.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["acceptance-criteria"],
+ "tags": ["agile"],
+ "mode": "prose",
+ "content": {
+ "text": "Given the user is on the login page, when they enter valid credentials, then they should be redirected to the dashboard.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 38, "minAccuracy": 95 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/en-US/requirements/003-given-when-then.json b/content/tracks/technical-writers/en-US/requirements/003-given-when-then.json
new file mode 100644
index 0000000..4ee21db
--- /dev/null
+++ b/content/tracks/technical-writers/en-US/requirements/003-given-when-then.json
@@ -0,0 +1,21 @@
+{
+ "id": "003-given-when-then",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "en-US",
+ "title": "Given-When-Then",
+ "description": "Practice BDD-style requirement formatting.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["given-when-then", "bdd"],
+ "tags": ["bdd"],
+ "mode": "prose",
+ "content": {
+ "text": "Given an empty shopping cart, when the user adds a product, then the cart total should update immediately.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 38, "minAccuracy": 95 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/en-US/requirements/004-api-requirement.json b/content/tracks/technical-writers/en-US/requirements/004-api-requirement.json
new file mode 100644
index 0000000..1e0e38d
--- /dev/null
+++ b/content/tracks/technical-writers/en-US/requirements/004-api-requirement.json
@@ -0,0 +1,21 @@
+{
+ "id": "004-api-requirement",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "en-US",
+ "title": "API Requirement",
+ "description": "Write technical API requirement descriptions.",
+ "difficulty": 3,
+ "estimatedMinutes": 3,
+ "skills": ["api-docs", "requirements"],
+ "tags": ["api", "rest"],
+ "mode": "prose",
+ "content": {
+ "text": "The endpoint must return a JSON payload with status code 200 and include the fields id, name, and createdAt in ISO 8601 format.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 35, "minAccuracy": 93 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/en-US/requirements/005-edge-case.json b/content/tracks/technical-writers/en-US/requirements/005-edge-case.json
new file mode 100644
index 0000000..206619f
--- /dev/null
+++ b/content/tracks/technical-writers/en-US/requirements/005-edge-case.json
@@ -0,0 +1,21 @@
+{
+ "id": "005-edge-case",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "en-US",
+ "title": "Edge Case Documentation",
+ "description": "Document edge cases and error handling requirements.",
+ "difficulty": 3,
+ "estimatedMinutes": 3,
+ "skills": ["edge-cases", "requirements"],
+ "tags": ["documentation"],
+ "mode": "prose",
+ "content": {
+ "text": "If the upload exceeds the 10 MB limit, the system shall reject the file and display an error message without partial storage.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 35, "minAccuracy": 93 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/pt-BR/requirements/001-historia-usuario.json b/content/tracks/technical-writers/pt-BR/requirements/001-historia-usuario.json
new file mode 100644
index 0000000..e979746
--- /dev/null
+++ b/content/tracks/technical-writers/pt-BR/requirements/001-historia-usuario.json
@@ -0,0 +1,21 @@
+{
+ "id": "001-historia-usuario",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "pt-BR",
+ "title": "Formato de História de Usuário",
+ "description": "Pratique digitar histórias de usuário no formato padrão.",
+ "difficulty": 1,
+ "estimatedMinutes": 2,
+ "skills": ["user-stories"],
+ "tags": ["agile", "requirements"],
+ "mode": "prose",
+ "content": {
+ "text": "Como usuário cadastrado, eu quero redefinir minha senha para recuperar o acesso à minha conta.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 40, "minAccuracy": 95 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/pt-BR/requirements/002-criterios-aceite.json b/content/tracks/technical-writers/pt-BR/requirements/002-criterios-aceite.json
new file mode 100644
index 0000000..06b567e
--- /dev/null
+++ b/content/tracks/technical-writers/pt-BR/requirements/002-criterios-aceite.json
@@ -0,0 +1,21 @@
+{
+ "id": "002-criterios-aceite",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "pt-BR",
+ "title": "Critérios de Aceite",
+ "description": "Digite critérios de aceite claros para requisitos de funcionalidades.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["acceptance-criteria"],
+ "tags": ["agile"],
+ "mode": "prose",
+ "content": {
+ "text": "Dado que o usuário está na página de login, quando ele informar credenciais válidas, então deve ser redirecionado ao painel.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 38, "minAccuracy": 95 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/pt-BR/requirements/003-dado-quando-entao.json b/content/tracks/technical-writers/pt-BR/requirements/003-dado-quando-entao.json
new file mode 100644
index 0000000..a9c9968
--- /dev/null
+++ b/content/tracks/technical-writers/pt-BR/requirements/003-dado-quando-entao.json
@@ -0,0 +1,21 @@
+{
+ "id": "003-dado-quando-entao",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "pt-BR",
+ "title": "Dado-Quando-Então",
+ "description": "Pratique formatação de requisitos no estilo BDD.",
+ "difficulty": 2,
+ "estimatedMinutes": 2,
+ "skills": ["given-when-then", "bdd"],
+ "tags": ["bdd"],
+ "mode": "prose",
+ "content": {
+ "text": "Dado um carrinho de compras vazio, quando o usuário adicionar um produto, então o total do carrinho deve ser atualizado imediatamente.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 38, "minAccuracy": 95 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/pt-BR/requirements/004-requisito-api.json b/content/tracks/technical-writers/pt-BR/requirements/004-requisito-api.json
new file mode 100644
index 0000000..db759f7
--- /dev/null
+++ b/content/tracks/technical-writers/pt-BR/requirements/004-requisito-api.json
@@ -0,0 +1,21 @@
+{
+ "id": "004-requisito-api",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "pt-BR",
+ "title": "Requisito de API",
+ "description": "Escreva descrições técnicas de requisitos de API.",
+ "difficulty": 3,
+ "estimatedMinutes": 3,
+ "skills": ["api-docs", "requirements"],
+ "tags": ["api", "rest"],
+ "mode": "prose",
+ "content": {
+ "text": "O endpoint deve retornar um payload JSON com código de status 200 e incluir os campos id, nome e criadoEm no formato ISO 8601.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 35, "minAccuracy": 93 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/content/tracks/technical-writers/pt-BR/requirements/005-caso-limite.json b/content/tracks/technical-writers/pt-BR/requirements/005-caso-limite.json
new file mode 100644
index 0000000..7a0482d
--- /dev/null
+++ b/content/tracks/technical-writers/pt-BR/requirements/005-caso-limite.json
@@ -0,0 +1,21 @@
+{
+ "id": "005-caso-limite",
+ "module": "requirements",
+ "track": "technical-writers",
+ "locale": "pt-BR",
+ "title": "Documentação de Caso Limite",
+ "description": "Documente casos limite e requisitos de tratamento de erros.",
+ "difficulty": 3,
+ "estimatedMinutes": 3,
+ "skills": ["edge-cases", "requirements"],
+ "tags": ["documentation"],
+ "mode": "prose",
+ "content": {
+ "text": "Se o upload exceder o limite de 10 MB, o sistema deve rejeitar o arquivo e exibir uma mensagem de erro sem armazenamento parcial.",
+ "display": "prose"
+ },
+ "goals": { "minWpm": 35, "minAccuracy": 93 },
+ "author": "coderkeys-team",
+ "source": null,
+ "version": 1
+}
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..0f69c4b
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,32 @@
+import js from '@eslint/js';
+import globals from 'globals';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config(
+ { ignores: ['**/dist', '**/node_modules'] },
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2022,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
+ },
+ },
+ {
+ files: ['packages/**/*.ts', 'scripts/**/*.ts'],
+ languageOptions: {
+ globals: globals.node,
+ },
+ },
+);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8234966
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "coderkeys",
+ "private": true,
+ "version": "0.1.0",
+ "description": "Open-source, local-first typing tutor for programmers and technical writers",
+ "license": "MIT",
+ "scripts": {
+ "dev": "pnpm --filter @coderkeys/schemas build && pnpm --filter @coderkeys/engine build && pnpm --filter @coderkeys/web dev",
+ "build": "pnpm -r build",
+ "test": "pnpm -r test",
+ "lint": "pnpm -r lint",
+ "typecheck": "pnpm --filter @coderkeys/schemas build && pnpm --filter @coderkeys/engine build && pnpm -r typecheck",
+ "validate-content": "pnpm --filter @coderkeys/schemas build && tsx scripts/validate-content.ts",
+ "check-i18n": "tsx scripts/check-i18n-keys.ts"
+ },
+ "devDependencies": {
+ "@coderkeys/schemas": "workspace:*",
+ "@eslint/js": "^9.22.0",
+ "eslint": "^9.22.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^16.0.0",
+ "prettier": "^3.5.3",
+ "tsx": "^4.19.3",
+ "typescript": "^5.8.2",
+ "typescript-eslint": "^8.26.1"
+ },
+ "packageManager": "pnpm@9.15.9",
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/packages/engine/package.json b/packages/engine/package.json
new file mode 100644
index 0000000..9d40926
--- /dev/null
+++ b/packages/engine/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@coderkeys/engine",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "lint": "eslint src --max-warnings 0 || true"
+ },
+ "devDependencies": {
+ "typescript": "^5.8.2",
+ "vitest": "^3.0.9"
+ }
+}
diff --git a/packages/engine/src/diff.ts b/packages/engine/src/diff.ts
new file mode 100644
index 0000000..127c655
--- /dev/null
+++ b/packages/engine/src/diff.ts
@@ -0,0 +1,51 @@
+import type { CharState } from './types';
+
+export function buildCharStates(passage: string, input: string): CharState[] {
+ const states: CharState[] = [];
+
+ for (let i = 0; i < passage.length; i++) {
+ const char = passage[i]!;
+ if (i < input.length) {
+ states.push({
+ char,
+ status: input[i] === char ? 'correct' : 'incorrect',
+ });
+ } else if (i === input.length) {
+ states.push({ char, status: 'current' });
+ } else {
+ states.push({ char, status: 'pending' });
+ }
+ }
+
+ return states;
+}
+
+export function countCorrectChars(passage: string, input: string): number {
+ let correct = 0;
+ for (let i = 0; i < input.length; i++) {
+ if (input[i] === passage[i]) {
+ correct++;
+ }
+ }
+ return correct;
+}
+
+export function getErrorPositions(passage: string, input: string): number[] {
+ const positions: number[] = [];
+ for (let i = 0; i < input.length; i++) {
+ if (input[i] !== passage[i]) {
+ positions.push(i);
+ }
+ }
+ return positions;
+}
+
+export function getErrorChars(passage: string, input: string): string[] {
+ const chars = new Set();
+ for (let i = 0; i < input.length; i++) {
+ if (input[i] !== passage[i]) {
+ chars.add(passage[i] ?? input[i] ?? '');
+ }
+ }
+ return [...chars].filter(Boolean);
+}
diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts
new file mode 100644
index 0000000..8c64932
--- /dev/null
+++ b/packages/engine/src/index.ts
@@ -0,0 +1,19 @@
+export { TypingSession } from './session';
+export { buildCharStates, countCorrectChars, getErrorPositions, getErrorChars } from './diff';
+export {
+ calculateWpm,
+ calculateRawWpm,
+ calculateAccuracy,
+ calculateAwpm,
+ getRollingWpm,
+ getLiveMetrics,
+} from './metrics/wpm';
+export type {
+ SessionStatus,
+ KeystrokeEvent,
+ CharState,
+ LiveMetrics,
+ SessionResult,
+ TypingSessionOptions,
+ TypingSessionState,
+} from './types';
diff --git a/packages/engine/src/metrics/accuracy.ts b/packages/engine/src/metrics/accuracy.ts
new file mode 100644
index 0000000..c4f6f31
--- /dev/null
+++ b/packages/engine/src/metrics/accuracy.ts
@@ -0,0 +1,8 @@
+export {
+ calculateWpm,
+ calculateRawWpm,
+ calculateAccuracy,
+ calculateAwpm,
+ getRollingWpm,
+ getLiveMetrics,
+} from './wpm';
diff --git a/packages/engine/src/metrics/wpm.ts b/packages/engine/src/metrics/wpm.ts
new file mode 100644
index 0000000..ce37d62
--- /dev/null
+++ b/packages/engine/src/metrics/wpm.ts
@@ -0,0 +1,66 @@
+import type { KeystrokeEvent } from '../types';
+
+const CHARS_PER_WORD = 5;
+const LIVE_WINDOW_MS = 5000;
+const MIN_ELAPSED_MS = 2000;
+const MIN_SPAN_MS = 200;
+
+export function calculateWpm(correctChars: number, elapsedMs: number): number {
+ if (elapsedMs <= 0) return 0;
+ const minutes = elapsedMs / 60_000;
+ return Math.round(correctChars / CHARS_PER_WORD / minutes);
+}
+
+export function calculateRawWpm(totalChars: number, elapsedMs: number): number {
+ if (elapsedMs <= 0) return 0;
+ const minutes = elapsedMs / 60_000;
+ return Math.round(totalChars / CHARS_PER_WORD / minutes);
+}
+
+export function calculateAccuracy(events: KeystrokeEvent[]): number {
+ if (events.length === 0) return 100;
+ const correct = events.filter((e) => e.correct).length;
+ return Math.round((correct / events.length) * 100);
+}
+
+export function calculateAwpm(wpm: number, accuracy: number): number {
+ return Math.round(wpm * (accuracy / 100));
+}
+
+export function getRollingWpm(events: KeystrokeEvent[], nowMs: number): number {
+ if (nowMs < MIN_ELAPSED_MS) return 0;
+
+ const windowStart = nowMs - LIVE_WINDOW_MS;
+ const recentCorrect = events.filter((e) => e.correct && !e.isBackspace && e.timestamp >= windowStart);
+
+ if (recentCorrect.length < 2) return 0;
+
+ const spanMs =
+ recentCorrect[recentCorrect.length - 1]!.timestamp - recentCorrect[0]!.timestamp;
+
+ if (spanMs < MIN_SPAN_MS) return 0;
+
+ return Math.round(recentCorrect.length / CHARS_PER_WORD / (spanMs / 60_000));
+}
+
+export function getLiveMetrics(
+ events: KeystrokeEvent[],
+ passage: string,
+ input: string,
+ elapsedMs: number,
+ nowMs: number,
+): {
+ wpm: number;
+ rawWpm: number;
+ accuracy: number;
+ awpm: number;
+ correctChars: number;
+} {
+ const correctChars = [...input].filter((c, i) => c === passage[i]).length;
+ const wpm = getRollingWpm(events, nowMs) || calculateWpm(correctChars, elapsedMs);
+ const rawWpm = calculateRawWpm(input.length, elapsedMs);
+ const accuracy = calculateAccuracy(events);
+ const awpm = calculateAwpm(wpm, accuracy);
+
+ return { wpm, rawWpm, accuracy, awpm, correctChars };
+}
diff --git a/packages/engine/src/session.ts b/packages/engine/src/session.ts
new file mode 100644
index 0000000..b85a050
--- /dev/null
+++ b/packages/engine/src/session.ts
@@ -0,0 +1,210 @@
+import { getErrorChars, getErrorPositions } from './diff';
+import {
+ calculateAccuracy,
+ calculateAwpm,
+ calculateRawWpm,
+ calculateWpm,
+ getLiveMetrics,
+} from './metrics/wpm';
+import type {
+ LiveMetrics,
+ SessionResult,
+ TypingSessionOptions,
+ TypingSessionState,
+} from './types';
+
+function createInitialState(passage: string, strictMode: boolean): TypingSessionState {
+ return {
+ status: 'idle',
+ passage,
+ input: '',
+ cursor: 0,
+ events: [],
+ startedAt: null,
+ pausedAt: null,
+ totalPausedMs: 0,
+ strictMode,
+ };
+}
+
+export class TypingSession {
+ private state: TypingSessionState;
+ private readonly now: () => number;
+
+ constructor(options: TypingSessionOptions) {
+ this.now = options.now ?? (() => performance.now());
+ this.state = createInitialState(options.passage, options.strictMode ?? false);
+ }
+
+ getState(): Readonly {
+ return this.state;
+ }
+
+ prepare(): void {
+ if (this.state.status !== 'idle') return;
+ this.state = { ...this.state, status: 'ready' };
+ }
+
+ start(): void {
+ if (this.state.status !== 'ready' && this.state.status !== 'idle') return;
+ this.state = {
+ ...this.state,
+ status: 'active',
+ startedAt: this.now(),
+ };
+ }
+
+ pause(): void {
+ if (this.state.status !== 'active') return;
+ this.state = {
+ ...this.state,
+ status: 'paused',
+ pausedAt: this.now(),
+ };
+ }
+
+ resume(): void {
+ if (this.state.status !== 'paused' || this.state.pausedAt === null) return;
+ const pausedDuration = this.now() - this.state.pausedAt;
+ this.state = {
+ ...this.state,
+ status: 'active',
+ pausedAt: null,
+ totalPausedMs: this.state.totalPausedMs + pausedDuration,
+ };
+ }
+
+ reset(): void {
+ this.state = createInitialState(this.state.passage, this.state.strictMode);
+ }
+
+ handleKey(key: string): boolean {
+ if (this.state.status === 'idle') {
+ this.prepare();
+ this.start();
+ } else if (this.state.status === 'ready') {
+ this.start();
+ }
+
+ if (this.state.status !== 'active') return false;
+
+ const timestamp = this.now();
+
+ if (key === 'Backspace') {
+ if (this.state.input.length === 0) return false;
+ const newInput = this.state.input.slice(0, -1);
+ this.state = {
+ ...this.state,
+ input: newInput,
+ cursor: newInput.length,
+ events: [
+ ...this.state.events,
+ {
+ char: 'Backspace',
+ correct: false,
+ position: newInput.length,
+ timestamp,
+ isBackspace: true,
+ },
+ ],
+ };
+ return true;
+ }
+
+ if (key.length !== 1) return false;
+
+ const position = this.state.input.length;
+ if (position >= this.state.passage.length) return false;
+
+ const expected = this.state.passage[position]!;
+ const correct = key === expected;
+
+ if (this.state.strictMode && !correct) {
+ this.state = {
+ ...this.state,
+ events: [
+ ...this.state.events,
+ {
+ char: key,
+ correct: false,
+ position,
+ timestamp,
+ isBackspace: false,
+ },
+ ],
+ };
+ return true;
+ }
+
+ const newInput = this.state.input + key;
+ const isComplete = newInput.length === this.state.passage.length;
+
+ this.state = {
+ ...this.state,
+ input: newInput,
+ cursor: newInput.length,
+ status: isComplete ? 'complete' : 'active',
+ events: [
+ ...this.state.events,
+ {
+ char: key,
+ correct,
+ position,
+ timestamp,
+ isBackspace: false,
+ },
+ ],
+ };
+
+ return true;
+ }
+
+ getElapsedMs(): number {
+ if (this.state.startedAt === null) return 0;
+ const end = this.state.status === 'paused' && this.state.pausedAt
+ ? this.state.pausedAt
+ : this.now();
+ return Math.max(0, end - this.state.startedAt - this.state.totalPausedMs);
+ }
+
+ getLiveMetrics(): LiveMetrics {
+ const elapsedMs = this.getElapsedMs();
+ const metrics = getLiveMetrics(
+ this.state.events,
+ this.state.passage,
+ this.state.input,
+ elapsedMs,
+ elapsedMs,
+ );
+
+ return {
+ ...metrics,
+ totalKeystrokes: this.state.events.length,
+ elapsedMs,
+ };
+ }
+
+ getResult(): SessionResult {
+ const elapsedMs = this.getElapsedMs();
+ const correctChars = [...this.state.input].filter(
+ (c, i) => c === this.state.passage[i],
+ ).length;
+ const wpm = calculateWpm(correctChars, elapsedMs);
+ const rawWpm = calculateRawWpm(this.state.input.length, elapsedMs);
+ const accuracy = calculateAccuracy(this.state.events);
+ const awpm = calculateAwpm(wpm, accuracy);
+
+ return {
+ wpm,
+ rawWpm,
+ accuracy,
+ awpm,
+ correctChars,
+ totalKeystrokes: this.state.events.length,
+ durationMs: elapsedMs,
+ errorPositions: getErrorPositions(this.state.passage, this.state.input),
+ errorChars: getErrorChars(this.state.passage, this.state.input),
+ completed: this.state.status === 'complete',
+ };
+ }
+}
diff --git a/packages/engine/src/types.ts b/packages/engine/src/types.ts
new file mode 100644
index 0000000..22d8205
--- /dev/null
+++ b/packages/engine/src/types.ts
@@ -0,0 +1,55 @@
+export type SessionStatus = 'idle' | 'ready' | 'active' | 'paused' | 'complete';
+
+export interface KeystrokeEvent {
+ char: string;
+ correct: boolean;
+ position: number;
+ timestamp: number;
+ isBackspace: boolean;
+}
+
+export interface CharState {
+ char: string;
+ status: 'pending' | 'correct' | 'incorrect' | 'current';
+}
+
+export interface LiveMetrics {
+ wpm: number;
+ rawWpm: number;
+ accuracy: number;
+ awpm: number;
+ correctChars: number;
+ totalKeystrokes: number;
+ elapsedMs: number;
+}
+
+export interface SessionResult {
+ wpm: number;
+ rawWpm: number;
+ accuracy: number;
+ awpm: number;
+ correctChars: number;
+ totalKeystrokes: number;
+ durationMs: number;
+ errorPositions: number[];
+ errorChars: string[];
+ completed: boolean;
+}
+
+export interface TypingSessionOptions {
+ passage: string;
+ strictMode?: boolean;
+ now?: () => number;
+}
+
+export interface TypingSessionState {
+ status: SessionStatus;
+ passage: string;
+ input: string;
+ cursor: number;
+ events: KeystrokeEvent[];
+ startedAt: number | null;
+ pausedAt: number | null;
+ totalPausedMs: number;
+ strictMode: boolean;
+}
diff --git a/packages/engine/tests/diff.test.ts b/packages/engine/tests/diff.test.ts
new file mode 100644
index 0000000..ecca801
--- /dev/null
+++ b/packages/engine/tests/diff.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from 'vitest';
+import { buildCharStates, countCorrectChars, getErrorPositions } from '../src/diff';
+
+describe('diff utilities', () => {
+ const passage = 'hello';
+
+ it('builds char states for partial input', () => {
+ const states = buildCharStates(passage, 'hel');
+ expect(states.map((s) => s.status)).toEqual([
+ 'correct',
+ 'correct',
+ 'correct',
+ 'current',
+ 'pending',
+ ]);
+ });
+
+ it('marks incorrect characters', () => {
+ const states = buildCharStates(passage, 'hxl');
+ expect(states[1]?.status).toBe('incorrect');
+ });
+
+ it('counts correct characters', () => {
+ expect(countCorrectChars(passage, 'hxllo')).toBe(4);
+ });
+
+ it('finds error positions', () => {
+ expect(getErrorPositions(passage, 'hxllo')).toEqual([1]);
+ });
+});
diff --git a/packages/engine/tests/session.test.ts b/packages/engine/tests/session.test.ts
new file mode 100644
index 0000000..a57a958
--- /dev/null
+++ b/packages/engine/tests/session.test.ts
@@ -0,0 +1,66 @@
+import { describe, expect, it } from 'vitest';
+import { TypingSession } from '../src/session';
+
+describe('TypingSession', () => {
+ it('completes a passage when all characters are typed', () => {
+ let time = 0;
+ const session = new TypingSession({
+ passage: 'abcdefghij',
+ now: () => time,
+ });
+
+ session.prepare();
+ session.start();
+ time = 12_000;
+ for (const char of 'abcdefghij') {
+ session.handleKey(char);
+ }
+
+ expect(session.getState().status).toBe('complete');
+ expect(session.getResult().completed).toBe(true);
+ expect(session.getResult().wpm).toBe(10);
+ expect(session.getResult().accuracy).toBe(100);
+ });
+
+ it('handles backspace', () => {
+ const session = new TypingSession({ passage: 'abc' });
+ session.prepare();
+ session.start();
+ session.handleKey('a');
+ session.handleKey('x');
+ session.handleKey('Backspace');
+ session.handleKey('b');
+ session.handleKey('c');
+
+ expect(session.getState().input).toBe('abc');
+ expect(session.getState().status).toBe('complete');
+ });
+
+ it('blocks progress in strict mode on incorrect key', () => {
+ const session = new TypingSession({ passage: 'ab', strictMode: true });
+ session.prepare();
+ session.start();
+ session.handleKey('x');
+
+ expect(session.getState().input).toBe('');
+ expect(session.getState().status).toBe('active');
+ });
+
+ it('pauses and resumes timing', () => {
+ let time = 0;
+ const session = new TypingSession({
+ passage: 'ab',
+ now: () => time,
+ });
+
+ session.prepare();
+ session.start();
+ time = 10_000;
+ session.pause();
+ time = 30_000;
+ session.resume();
+ time = 40_000;
+
+ expect(session.getElapsedMs()).toBe(20_000);
+ });
+});
diff --git a/packages/engine/tests/wpm.test.ts b/packages/engine/tests/wpm.test.ts
new file mode 100644
index 0000000..5a7c3dd
--- /dev/null
+++ b/packages/engine/tests/wpm.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from 'vitest';
+import { calculateAccuracy, calculateAwpm, calculateRawWpm, calculateWpm, getRollingWpm } from '../src/metrics/wpm';
+import type { KeystrokeEvent } from '../src/types';
+
+describe('WPM calculations', () => {
+ it('calculates WPM from correct characters', () => {
+ // 50 correct chars in 60 seconds = 10 WPM
+ expect(calculateWpm(50, 60_000)).toBe(10);
+ });
+
+ it('returns 0 for zero elapsed time', () => {
+ expect(calculateWpm(50, 0)).toBe(0);
+ });
+
+ it('calculates raw WPM from total characters', () => {
+ expect(calculateRawWpm(50, 60_000)).toBe(10);
+ });
+
+ it('calculates accuracy from keystroke events', () => {
+ const events: KeystrokeEvent[] = [
+ { char: 'a', correct: true, position: 0, timestamp: 0, isBackspace: false },
+ { char: 'b', correct: false, position: 1, timestamp: 100, isBackspace: false },
+ { char: 'Backspace', correct: false, position: 0, timestamp: 200, isBackspace: true },
+ ];
+ expect(calculateAccuracy(events)).toBe(33);
+ });
+
+ it('returns 100% accuracy for empty events', () => {
+ expect(calculateAccuracy([])).toBe(100);
+ });
+
+ it('calculates AWPM', () => {
+ expect(calculateAwpm(100, 90)).toBe(90);
+ });
+
+ it('calculates rolling WPM using event window', () => {
+ const events: KeystrokeEvent[] = [
+ { char: 'a', correct: true, position: 0, timestamp: 1000, isBackspace: false },
+ { char: 'b', correct: true, position: 1, timestamp: 1500, isBackspace: false },
+ { char: 'c', correct: true, position: 2, timestamp: 2000, isBackspace: false },
+ { char: 'd', correct: true, position: 3, timestamp: 2500, isBackspace: false },
+ { char: 'e', correct: true, position: 4, timestamp: 3000, isBackspace: false },
+ ];
+ // 5 chars in 2 seconds = 30 WPM
+ expect(getRollingWpm(events, 3000)).toBe(30);
+ });
+});
diff --git a/packages/engine/tsconfig.json b/packages/engine/tsconfig.json
new file mode 100644
index 0000000..792172f
--- /dev/null
+++ b/packages/engine/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": ["src"]
+}
diff --git a/packages/engine/vitest.config.ts b/packages/engine/vitest.config.ts
new file mode 100644
index 0000000..8363e16
--- /dev/null
+++ b/packages/engine/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ include: ['tests/**/*.test.ts'],
+ },
+});
diff --git a/packages/schemas/package.json b/packages/schemas/package.json
new file mode 100644
index 0000000..e4dfe67
--- /dev/null
+++ b/packages/schemas/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@coderkeys/schemas",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "typecheck": "tsc --noEmit",
+ "test": "echo \"No tests yet\"",
+ "lint": "eslint src"
+ },
+ "dependencies": {
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "typescript": "^5.8.2"
+ }
+}
diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts
new file mode 100644
index 0000000..7718ac7
--- /dev/null
+++ b/packages/schemas/src/index.ts
@@ -0,0 +1,15 @@
+export {
+ LocaleSchema,
+ LessonModeSchema,
+ LessonContentSchema,
+ LessonGoalsSchema,
+ LessonHintsSchema,
+ LessonSchema,
+ TrackMetaSchema,
+ ManifestModuleSchema,
+ ManifestTrackSchema,
+ ManifestSchema,
+ SessionResultSchema,
+} from './lesson.schema';
+
+export type { Locale, Lesson, TrackMeta, Manifest, SessionResult } from './lesson.schema';
diff --git a/packages/schemas/src/lesson.schema.ts b/packages/schemas/src/lesson.schema.ts
new file mode 100644
index 0000000..419df16
--- /dev/null
+++ b/packages/schemas/src/lesson.schema.ts
@@ -0,0 +1,102 @@
+import { z } from 'zod';
+
+export const LocaleSchema = z.enum(['en-US', 'pt-BR']);
+
+export const LessonModeSchema = z.enum(['code', 'prose', 'translation']);
+
+export const LessonContentSchema = z.object({
+ text: z.string().min(20, 'Lesson text must be at least 20 characters'),
+ display: z.enum(['code', 'prose']),
+ language: z.string().optional(),
+ sourceText: z.string().optional(),
+});
+
+export const LessonGoalsSchema = z.object({
+ minWpm: z.number().min(0).optional(),
+ minAccuracy: z.number().min(0).max(100).optional(),
+});
+
+export const LessonHintsSchema = z.object({
+ keyboard: z.array(z.string()).optional(),
+ shortcuts: z
+ .array(
+ z.object({
+ keys: z.string(),
+ description: z.string(),
+ }),
+ )
+ .optional(),
+});
+
+export const LessonSchema = z
+ .object({
+ id: z.string().regex(/^\d{3}-[a-z0-9-]+$/, 'ID must match pattern 001-slug'),
+ module: z.string().min(1),
+ track: z.string().min(1),
+ locale: LocaleSchema,
+ title: z.string().min(1),
+ description: z.string().min(1),
+ difficulty: z.number().int().min(1).max(5),
+ estimatedMinutes: z.number().min(1),
+ skills: z.array(z.string()).min(1),
+ tags: z.array(z.string()).optional(),
+ mode: LessonModeSchema,
+ content: LessonContentSchema,
+ goals: LessonGoalsSchema.optional(),
+ hints: LessonHintsSchema.optional(),
+ author: z.string().min(1),
+ source: z.string().nullable().optional(),
+ version: z.number().int().min(1),
+ })
+ .superRefine((lesson, ctx) => {
+ if (lesson.mode === 'translation' && !lesson.content.sourceText) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'Translation lessons require content.sourceText',
+ path: ['content', 'sourceText'],
+ });
+ }
+ });
+
+export const TrackMetaSchema = z.object({
+ id: z.string().min(1),
+ name: z.record(LocaleSchema, z.string()),
+ description: z.record(LocaleSchema, z.string()),
+ defaultModule: z.string().min(1),
+});
+
+export const ManifestModuleSchema = z.object({
+ id: z.string().min(1),
+ skills: z.array(z.string()).min(1),
+ lessons: z.record(LocaleSchema, z.array(z.string())),
+});
+
+export const ManifestTrackSchema = z.object({
+ id: z.string().min(1),
+ icon: z.string().min(1),
+ modules: z.array(ManifestModuleSchema).min(1),
+});
+
+export const ManifestSchema = z.object({
+ version: z.string().min(1),
+ tracks: z.array(ManifestTrackSchema).min(1),
+});
+
+export const SessionResultSchema = z.object({
+ id: z.string().uuid(),
+ lessonId: z.string().min(1),
+ wpm: z.number().min(0),
+ rawWpm: z.number().min(0),
+ accuracy: z.number().min(0).max(100),
+ awpm: z.number().min(0),
+ durationMs: z.number().min(0),
+ errorPositions: z.array(z.number()),
+ errorChars: z.array(z.string()),
+ completedAt: z.string().datetime(),
+});
+
+export type Locale = z.infer;
+export type Lesson = z.infer;
+export type TrackMeta = z.infer;
+export type Manifest = z.infer;
+export type SessionResult = z.infer;
diff --git a/packages/schemas/tsconfig.json b/packages/schemas/tsconfig.json
new file mode 100644
index 0000000..792172f
--- /dev/null
+++ b/packages/schemas/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": ["src"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..09874e3
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,3132 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ devDependencies:
+ '@coderkeys/schemas':
+ specifier: workspace:*
+ version: link:packages/schemas
+ '@eslint/js':
+ specifier: ^9.22.0
+ version: 9.39.4
+ eslint:
+ specifier: ^9.22.0
+ version: 9.39.4(jiti@2.7.0)
+ eslint-plugin-react-hooks:
+ specifier: ^5.2.0
+ version: 5.2.0(eslint@9.39.4(jiti@2.7.0))
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.19
+ version: 0.4.26(eslint@9.39.4(jiti@2.7.0))
+ globals:
+ specifier: ^16.0.0
+ version: 16.5.0
+ prettier:
+ specifier: ^3.5.3
+ version: 3.9.3
+ tsx:
+ specifier: ^4.19.3
+ version: 4.22.4
+ typescript:
+ specifier: ^5.8.2
+ version: 5.9.3
+ typescript-eslint:
+ specifier: ^8.26.1
+ version: 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+
+ apps/web:
+ dependencies:
+ '@coderkeys/engine':
+ specifier: workspace:*
+ version: link:../../packages/engine
+ '@coderkeys/schemas':
+ specifier: workspace:*
+ version: link:../../packages/schemas
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ dexie:
+ specifier: ^4.0.11
+ version: 4.4.4
+ i18next:
+ specifier: ^24.2.3
+ version: 24.2.3(typescript@5.9.3)
+ i18next-browser-languagedetector:
+ specifier: ^8.0.4
+ version: 8.2.1
+ react:
+ specifier: ^19.2.7
+ version: 19.2.7
+ react-dom:
+ specifier: ^19.2.7
+ version: 19.2.7(react@19.2.7)
+ react-i18next:
+ specifier: ^15.4.1
+ version: 15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)
+ react-router-dom:
+ specifier: ^7.4.0
+ version: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ tailwind-merge:
+ specifier: ^3.0.2
+ version: 3.6.0
+ zustand:
+ specifier: ^5.0.3
+ version: 5.0.14(@types/react@19.2.17)(react@19.2.7)
+ devDependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.0.14
+ version: 4.3.2(vite@8.1.0(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4))
+ '@types/node':
+ specifier: ^22.14.0
+ version: 22.20.0
+ '@types/react':
+ specifier: ^19.2.17
+ version: 19.2.17
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.17)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.2
+ version: 6.0.3(vite@8.1.0(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4))
+ tailwindcss:
+ specifier: ^4.0.14
+ version: 4.3.2
+ typescript:
+ specifier: ^5.8.2
+ version: 5.9.3
+ vite:
+ specifier: ^8.1.0
+ version: 8.1.0(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)
+ vitest:
+ specifier: ^3.0.9
+ version: 3.2.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)
+
+ packages/engine:
+ devDependencies:
+ typescript:
+ specifier: ^5.8.2
+ version: 5.9.3
+ vitest:
+ specifier: ^3.0.9
+ version: 3.2.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)
+
+ packages/schemas:
+ dependencies:
+ zod:
+ specifier: ^3.24.2
+ version: 3.25.76
+ devDependencies:
+ typescript:
+ specifier: ^5.8.2
+ version: 5.9.3
+
+packages:
+
+ '@babel/runtime@7.29.7':
+ resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
+ engines: {node: '>=6.9.0'}
+
+ '@emnapi/core@1.11.1':
+ resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
+
+ '@emnapi/runtime@1.11.1':
+ resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
+
+ '@emnapi/wasi-threads@1.2.2':
+ resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+
+ '@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.1':
+ resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@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.1':
+ resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.1':
+ resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ 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.1':
+ resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@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.1':
+ resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.28.1':
+ resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@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.1':
+ resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.28.1':
+ resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm64@0.28.1':
+ resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.1':
+ resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ 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.1':
+ resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ 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.1':
+ resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ 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.1':
+ resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ 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.1':
+ resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ 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.1':
+ resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ 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.1':
+ resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ 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.1':
+ resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
+ 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.1':
+ resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.1':
+ resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
+ 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.1':
+ resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.1':
+ resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
+ 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.1':
+ resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/sunos-x64@0.28.1':
+ resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@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.1':
+ resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.28.1':
+ resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ 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.1':
+ resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.21.2':
+ resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.4.2':
+ resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.17.0':
+ resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.5':
+ resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.39.4':
+ resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.7':
+ resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.4.1':
+ resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@humanfs/core@0.19.2':
+ resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.8':
+ resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/types@0.15.0':
+ resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@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.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/wasm-runtime@1.1.6':
+ resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.137.0':
+ resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==}
+
+ '@rolldown/binding-android-arm64@1.1.3':
+ resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.1.3':
+ resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.1.3':
+ resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.1.3':
+ resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.3':
+ resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.3':
+ resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-musl@1.1.3':
+ resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.3':
+ resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.3':
+ resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-gnu@1.1.3':
+ resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-musl@1.1.3':
+ resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-openharmony-arm64@1.1.3':
+ resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.1.3':
+ resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.3':
+ resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.1.3':
+ resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@rollup/rollup-android-arm-eabi@4.62.2':
+ resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.62.2':
+ resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.62.2':
+ resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.62.2':
+ resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.62.2':
+ resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.62.2':
+ resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.2':
+ resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.62.2':
+ resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.62.2':
+ resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.62.2':
+ resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-gnu@4.62.2':
+ resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-musl@4.62.2':
+ resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.62.2':
+ resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-musl@4.62.2':
+ resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.62.2':
+ resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.62.2':
+ resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.62.2':
+ resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.62.2':
+ resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.62.2':
+ resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-openbsd-x64@4.62.2':
+ resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.62.2':
+ resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.62.2':
+ resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.62.2':
+ resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.62.2':
+ resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.62.2':
+ resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/node@4.3.2':
+ resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==}
+
+ '@tailwindcss/oxide-android-arm64@4.3.2':
+ resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.2':
+ resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.3.2':
+ resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.2':
+ resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2':
+ resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.2':
+ resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.2':
+ resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.2':
+ resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.2':
+ resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.2':
+ resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==}
+ 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.2':
+ resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.2':
+ resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.3.2':
+ resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/vite@4.3.2':
+ resolution: {integrity: sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
+ '@tybys/wasm-util@0.10.3':
+ resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
+
+ '@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/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/node@22.20.0':
+ resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.17':
+ resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
+
+ '@typescript-eslint/eslint-plugin@8.62.1':
+ resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.62.1
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/parser@8.62.1':
+ resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/project-service@8.62.1':
+ resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/scope-manager@8.62.1':
+ resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.62.1':
+ resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/type-utils@8.62.1':
+ resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/types@8.62.1':
+ resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.62.1':
+ resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/utils@8.62.1':
+ resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/visitor-keys@8.62.1':
+ resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react@6.0.3':
+ resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
+ babel-plugin-react-compiler: ^1.0.0
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@rolldown/plugin-babel':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+
+ '@vitest/expect@3.2.6':
+ resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==}
+
+ '@vitest/mocker@3.2.6':
+ resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@3.2.6':
+ resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==}
+
+ '@vitest/runner@3.2.6':
+ resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==}
+
+ '@vitest/snapshot@3.2.6':
+ resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==}
+
+ '@vitest/spy@3.2.6':
+ resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==}
+
+ '@vitest/utils@3.2.6':
+ resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.17.0:
+ resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.15.0:
+ resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ brace-expansion@1.1.15:
+ resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==}
+
+ brace-expansion@5.0.7:
+ resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
+ engines: {node: 18 || 20 || >=22}
+
+ cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ chai@5.3.3:
+ resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
+ engines: {node: '>=18'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ check-error@2.1.3:
+ resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
+ engines: {node: '>= 16'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-eql@5.0.2:
+ resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+ engines: {node: '>=6'}
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ dexie@4.4.4:
+ resolution: {integrity: sha512-jIwsYI8Os2hgnqc6O49YwFDKGc5v5QjGx0wPVp543ip1F53VFAKMLthV2pQosQcVTv3eAskTWYspOx195PM0FQ==}
+
+ enhanced-resolve@5.21.6:
+ resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==}
+ engines: {node: '>=10.13.0'}
+
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ esbuild@0.28.1:
+ resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react-hooks@5.2.0:
+ resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+ eslint-plugin-react-refresh@0.4.26:
+ resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==}
+ peerDependencies:
+ eslint: '>=8.40'
+
+ eslint-scope@8.4.0:
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.1:
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint@9.39.4:
+ resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+ engines: {node: '>=12.0.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.4.2:
+ resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@16.5.0:
+ resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
+ engines: {node: '>=18'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ html-parse-stringify@3.0.1:
+ resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
+
+ i18next-browser-languagedetector@8.2.1:
+ resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==}
+
+ i18next@24.2.3:
+ resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==}
+ peerDependencies:
+ typescript: ^5
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
+ hasBin: true
+
+ js-tokens@9.0.1:
+ resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+
+ js-yaml@4.3.0:
+ resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ 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-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ 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-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ 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]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ 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]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ 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@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ loupe@3.2.1:
+ resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ minimatch@10.2.5:
+ resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
+ engines: {node: 18 || 20 || >=22}
+
+ minimatch@3.1.5:
+ resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.15:
+ resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ pathval@2.0.1:
+ resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
+ engines: {node: '>= 14.16'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.16:
+ resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prettier@3.9.3:
+ resolution: {integrity: sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ react-dom@19.2.7:
+ resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==}
+ peerDependencies:
+ react: ^19.2.7
+
+ react-i18next@15.7.4:
+ resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==}
+ peerDependencies:
+ i18next: '>= 23.4.0'
+ react: '>= 16.8.0'
+ react-dom: '*'
+ react-native: '*'
+ typescript: ^5
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ typescript:
+ optional: true
+
+ react-router-dom@7.18.0:
+ resolution: {integrity: sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.18.0:
+ resolution: {integrity: sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react@19.2.7:
+ resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ rolldown@1.1.3:
+ resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ rollup@4.62.2:
+ resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ set-cookie-parser@2.7.2:
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ strip-literal@3.1.0:
+ resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ tailwind-merge@3.6.0:
+ resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
+
+ tailwindcss@4.3.2:
+ resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==}
+
+ tapable@2.3.3:
+ resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
+ engines: {node: '>=6'}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@0.3.2:
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tinypool@1.1.1:
+ resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+
+ tinyrainbow@2.0.0:
+ resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
+ engines: {node: '>=14.0.0'}
+
+ tinyspy@4.0.4:
+ resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
+ engines: {node: '>=14.0.0'}
+
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ tsx@4.22.4:
+ resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.62.1:
+ resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@6.21.0:
+ resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ vite-node@3.2.4:
+ resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+
+ vite@6.4.3:
+ resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vite@8.1.0:
+ resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.3.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@3.2.6:
+ resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/debug': ^4.1.12
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ '@vitest/browser': 3.2.6
+ '@vitest/ui': 3.2.6
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/debug':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ void-elements@3.1.0:
+ resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
+ engines: {node: '>=0.10.0'}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod@3.25.76:
+ resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+
+ zustand@5.0.14:
+ resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
+snapshots:
+
+ '@babel/runtime@7.29.7': {}
+
+ '@emnapi/core@1.11.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.2
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/aix-ppc64@0.28.1':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.28.1':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.28.1':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.28.1':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.28.1':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.28.1':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.28.1':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.28.1':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.28.1':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.28.1':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.28.1':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.28.1':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.28.1':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.28.1':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.28.1':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.28.1':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))':
+ dependencies:
+ eslint: 9.39.4(jiti@2.7.0)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint/config-array@0.21.2':
+ dependencies:
+ '@eslint/object-schema': 2.1.7
+ debug: 4.4.3
+ minimatch: 3.1.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.4.2':
+ dependencies:
+ '@eslint/core': 0.17.0
+
+ '@eslint/core@0.17.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.5':
+ dependencies:
+ ajv: 6.15.0
+ debug: 4.4.3
+ espree: 10.4.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.3.0
+ minimatch: 3.1.5
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.39.4': {}
+
+ '@eslint/object-schema@2.1.7': {}
+
+ '@eslint/plugin-kit@0.4.1':
+ dependencies:
+ '@eslint/core': 0.17.0
+ levn: 0.4.1
+
+ '@humanfs/core@0.19.2':
+ dependencies:
+ '@humanfs/types': 0.15.0
+
+ '@humanfs/node@0.16.8':
+ dependencies:
+ '@humanfs/core': 0.19.2
+ '@humanfs/types': 0.15.0
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanfs/types@0.15.0': {}
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@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.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@tybys/wasm-util': 0.10.3
+ optional: true
+
+ '@oxc-project/types@0.137.0': {}
+
+ '@rolldown/binding-android-arm64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.1.3':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.1.3':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.3':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.1.3':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@rollup/rollup-android-arm-eabi@4.62.2':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.62.2':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.62.2':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.62.2':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.62.2':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.62.2':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.62.2':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.62.2':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.62.2':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.62.2':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.62.2':
+ optional: true
+
+ '@tailwindcss/node@4.3.2':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.21.6
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.3.2
+
+ '@tailwindcss/oxide-android-arm64@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.2':
+ optional: true
+
+ '@tailwindcss/oxide@4.3.2':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.3.2
+ '@tailwindcss/oxide-darwin-arm64': 4.3.2
+ '@tailwindcss/oxide-darwin-x64': 4.3.2
+ '@tailwindcss/oxide-freebsd-x64': 4.3.2
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2
+ '@tailwindcss/oxide-linux-arm64-musl': 4.3.2
+ '@tailwindcss/oxide-linux-x64-gnu': 4.3.2
+ '@tailwindcss/oxide-linux-x64-musl': 4.3.2
+ '@tailwindcss/oxide-wasm32-wasi': 4.3.2
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2
+ '@tailwindcss/oxide-win32-x64-msvc': 4.3.2
+
+ '@tailwindcss/vite@4.3.2(vite@8.1.0(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4))':
+ dependencies:
+ '@tailwindcss/node': 4.3.2
+ '@tailwindcss/oxide': 4.3.2
+ tailwindcss: 4.3.2
+ vite: 8.1.0(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)
+
+ '@tybys/wasm-util@0.10.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@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/json-schema@7.0.15': {}
+
+ '@types/node@22.20.0':
+ dependencies:
+ undici-types: 6.21.0
+
+ '@types/react-dom@19.2.3(@types/react@19.2.17)':
+ dependencies:
+ '@types/react': 19.2.17
+
+ '@types/react@19.2.17':
+ dependencies:
+ csstype: 3.2.3
+
+ '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.62.1
+ '@typescript-eslint/type-utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.62.1
+ eslint: 9.39.4(jiti@2.7.0)
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.5.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.62.1
+ '@typescript-eslint/types': 8.62.1
+ '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.62.1
+ debug: 4.4.3
+ eslint: 9.39.4(jiti@2.7.0)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.62.1(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.62.1
+ debug: 4.4.3
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.62.1':
+ dependencies:
+ '@typescript-eslint/types': 8.62.1
+ '@typescript-eslint/visitor-keys': 8.62.1
+
+ '@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.9.3)':
+ dependencies:
+ typescript: 5.9.3
+
+ '@typescript-eslint/type-utils@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.62.1
+ '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+ debug: 4.4.3
+ eslint: 9.39.4(jiti@2.7.0)
+ ts-api-utils: 2.5.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.62.1': {}
+
+ '@typescript-eslint/typescript-estree@8.62.1(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.62.1(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.62.1
+ '@typescript-eslint/visitor-keys': 8.62.1
+ debug: 4.4.3
+ minimatch: 10.2.5
+ semver: 7.8.5
+ tinyglobby: 0.2.17
+ ts-api-utils: 2.5.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0))
+ '@typescript-eslint/scope-manager': 8.62.1
+ '@typescript-eslint/types': 8.62.1
+ '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3)
+ eslint: 9.39.4(jiti@2.7.0)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.62.1':
+ dependencies:
+ '@typescript-eslint/types': 8.62.1
+ eslint-visitor-keys: 5.0.1
+
+ '@vitejs/plugin-react@6.0.3(vite@8.1.0(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.1
+ vite: 8.1.0(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)
+
+ '@vitest/expect@3.2.6':
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/spy': 3.2.6
+ '@vitest/utils': 3.2.6
+ chai: 5.3.3
+ tinyrainbow: 2.0.0
+
+ '@vitest/mocker@3.2.6(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))':
+ dependencies:
+ '@vitest/spy': 3.2.6
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)
+
+ '@vitest/pretty-format@3.2.6':
+ dependencies:
+ tinyrainbow: 2.0.0
+
+ '@vitest/runner@3.2.6':
+ dependencies:
+ '@vitest/utils': 3.2.6
+ pathe: 2.0.3
+ strip-literal: 3.1.0
+
+ '@vitest/snapshot@3.2.6':
+ dependencies:
+ '@vitest/pretty-format': 3.2.6
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@3.2.6':
+ dependencies:
+ tinyspy: 4.0.4
+
+ '@vitest/utils@3.2.6':
+ dependencies:
+ '@vitest/pretty-format': 3.2.6
+ loupe: 3.2.1
+ tinyrainbow: 2.0.0
+
+ acorn-jsx@5.3.2(acorn@8.17.0):
+ dependencies:
+ acorn: 8.17.0
+
+ acorn@8.17.0: {}
+
+ ajv@6.15.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ assertion-error@2.0.1: {}
+
+ balanced-match@1.0.2: {}
+
+ balanced-match@4.0.4: {}
+
+ brace-expansion@1.1.15:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@5.0.7:
+ dependencies:
+ balanced-match: 4.0.4
+
+ cac@6.7.14: {}
+
+ callsites@3.1.0: {}
+
+ chai@5.3.3:
+ dependencies:
+ assertion-error: 2.0.1
+ check-error: 2.1.3
+ deep-eql: 5.0.2
+ loupe: 3.2.1
+ pathval: 2.0.1
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ check-error@2.1.3: {}
+
+ clsx@2.1.1: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ concat-map@0.0.1: {}
+
+ cookie@1.1.1: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.2.3: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ deep-eql@5.0.2: {}
+
+ deep-is@0.1.4: {}
+
+ detect-libc@2.1.2: {}
+
+ dexie@4.4.4: {}
+
+ enhanced-resolve@5.21.6:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.3
+
+ es-module-lexer@1.7.0: {}
+
+ 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.1:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.1
+ '@esbuild/android-arm': 0.28.1
+ '@esbuild/android-arm64': 0.28.1
+ '@esbuild/android-x64': 0.28.1
+ '@esbuild/darwin-arm64': 0.28.1
+ '@esbuild/darwin-x64': 0.28.1
+ '@esbuild/freebsd-arm64': 0.28.1
+ '@esbuild/freebsd-x64': 0.28.1
+ '@esbuild/linux-arm': 0.28.1
+ '@esbuild/linux-arm64': 0.28.1
+ '@esbuild/linux-ia32': 0.28.1
+ '@esbuild/linux-loong64': 0.28.1
+ '@esbuild/linux-mips64el': 0.28.1
+ '@esbuild/linux-ppc64': 0.28.1
+ '@esbuild/linux-riscv64': 0.28.1
+ '@esbuild/linux-s390x': 0.28.1
+ '@esbuild/linux-x64': 0.28.1
+ '@esbuild/netbsd-arm64': 0.28.1
+ '@esbuild/netbsd-x64': 0.28.1
+ '@esbuild/openbsd-arm64': 0.28.1
+ '@esbuild/openbsd-x64': 0.28.1
+ '@esbuild/openharmony-arm64': 0.28.1
+ '@esbuild/sunos-x64': 0.28.1
+ '@esbuild/win32-arm64': 0.28.1
+ '@esbuild/win32-ia32': 0.28.1
+ '@esbuild/win32-x64': 0.28.1
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.7.0)):
+ dependencies:
+ eslint: 9.39.4(jiti@2.7.0)
+
+ eslint-plugin-react-refresh@0.4.26(eslint@9.39.4(jiti@2.7.0)):
+ dependencies:
+ eslint: 9.39.4(jiti@2.7.0)
+
+ eslint-scope@8.4.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint-visitor-keys@5.0.1: {}
+
+ eslint@9.39.4(jiti@2.7.0):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.21.2
+ '@eslint/config-helpers': 0.4.2
+ '@eslint/core': 0.17.0
+ '@eslint/eslintrc': 3.3.5
+ '@eslint/js': 9.39.4
+ '@eslint/plugin-kit': 0.4.1
+ '@humanfs/node': 0.16.8
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.9
+ ajv: 6.15.0
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.5
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.7.0
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.17.0
+ acorn-jsx: 5.3.2(acorn@8.17.0)
+ eslint-visitor-keys: 4.2.1
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ esutils@2.0.3: {}
+
+ expect-type@1.4.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.4.2
+ keyv: 4.5.4
+
+ flatted@3.4.2: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@14.0.0: {}
+
+ globals@16.5.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ has-flag@4.0.0: {}
+
+ html-parse-stringify@3.0.1:
+ dependencies:
+ void-elements: 3.1.0
+
+ i18next-browser-languagedetector@8.2.1:
+ dependencies:
+ '@babel/runtime': 7.29.7
+
+ i18next@24.2.3(typescript@5.9.3):
+ dependencies:
+ '@babel/runtime': 7.29.7
+ optionalDependencies:
+ typescript: 5.9.3
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ isexe@2.0.0: {}
+
+ jiti@2.7.0: {}
+
+ js-tokens@9.0.1: {}
+
+ js-yaml@4.3.0:
+ dependencies:
+ argparse: 2.0.1
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.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
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.merge@4.6.2: {}
+
+ loupe@3.2.1: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ minimatch@10.2.5:
+ dependencies:
+ brace-expansion: 5.0.7
+
+ minimatch@3.1.5:
+ dependencies:
+ brace-expansion: 1.1.15
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.15: {}
+
+ natural-compare@1.4.0: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ pathe@2.0.3: {}
+
+ pathval@2.0.1: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.4: {}
+
+ postcss@8.5.16:
+ dependencies:
+ nanoid: 3.3.15
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ prettier@3.9.3: {}
+
+ punycode@2.3.1: {}
+
+ react-dom@19.2.7(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ scheduler: 0.27.0
+
+ react-i18next@15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3):
+ dependencies:
+ '@babel/runtime': 7.29.7
+ html-parse-stringify: 3.0.1
+ i18next: 24.2.3(typescript@5.9.3)
+ react: 19.2.7
+ optionalDependencies:
+ react-dom: 19.2.7(react@19.2.7)
+ typescript: 5.9.3
+
+ react-router-dom@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+
+ react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ cookie: 1.1.1
+ react: 19.2.7
+ set-cookie-parser: 2.7.2
+ optionalDependencies:
+ react-dom: 19.2.7(react@19.2.7)
+
+ react@19.2.7: {}
+
+ resolve-from@4.0.0: {}
+
+ rolldown@1.1.3:
+ dependencies:
+ '@oxc-project/types': 0.137.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.1.3
+ '@rolldown/binding-darwin-arm64': 1.1.3
+ '@rolldown/binding-darwin-x64': 1.1.3
+ '@rolldown/binding-freebsd-x64': 1.1.3
+ '@rolldown/binding-linux-arm-gnueabihf': 1.1.3
+ '@rolldown/binding-linux-arm64-gnu': 1.1.3
+ '@rolldown/binding-linux-arm64-musl': 1.1.3
+ '@rolldown/binding-linux-ppc64-gnu': 1.1.3
+ '@rolldown/binding-linux-s390x-gnu': 1.1.3
+ '@rolldown/binding-linux-x64-gnu': 1.1.3
+ '@rolldown/binding-linux-x64-musl': 1.1.3
+ '@rolldown/binding-openharmony-arm64': 1.1.3
+ '@rolldown/binding-wasm32-wasi': 1.1.3
+ '@rolldown/binding-win32-arm64-msvc': 1.1.3
+ '@rolldown/binding-win32-x64-msvc': 1.1.3
+
+ rollup@4.62.2:
+ dependencies:
+ '@types/estree': 1.0.9
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.62.2
+ '@rollup/rollup-android-arm64': 4.62.2
+ '@rollup/rollup-darwin-arm64': 4.62.2
+ '@rollup/rollup-darwin-x64': 4.62.2
+ '@rollup/rollup-freebsd-arm64': 4.62.2
+ '@rollup/rollup-freebsd-x64': 4.62.2
+ '@rollup/rollup-linux-arm-gnueabihf': 4.62.2
+ '@rollup/rollup-linux-arm-musleabihf': 4.62.2
+ '@rollup/rollup-linux-arm64-gnu': 4.62.2
+ '@rollup/rollup-linux-arm64-musl': 4.62.2
+ '@rollup/rollup-linux-loong64-gnu': 4.62.2
+ '@rollup/rollup-linux-loong64-musl': 4.62.2
+ '@rollup/rollup-linux-ppc64-gnu': 4.62.2
+ '@rollup/rollup-linux-ppc64-musl': 4.62.2
+ '@rollup/rollup-linux-riscv64-gnu': 4.62.2
+ '@rollup/rollup-linux-riscv64-musl': 4.62.2
+ '@rollup/rollup-linux-s390x-gnu': 4.62.2
+ '@rollup/rollup-linux-x64-gnu': 4.62.2
+ '@rollup/rollup-linux-x64-musl': 4.62.2
+ '@rollup/rollup-openbsd-x64': 4.62.2
+ '@rollup/rollup-openharmony-arm64': 4.62.2
+ '@rollup/rollup-win32-arm64-msvc': 4.62.2
+ '@rollup/rollup-win32-ia32-msvc': 4.62.2
+ '@rollup/rollup-win32-x64-gnu': 4.62.2
+ '@rollup/rollup-win32-x64-msvc': 4.62.2
+ fsevents: 2.3.3
+
+ scheduler@0.27.0: {}
+
+ semver@7.8.5: {}
+
+ set-cookie-parser@2.7.2: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ siginfo@2.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ stackback@0.0.2: {}
+
+ std-env@3.10.0: {}
+
+ strip-json-comments@3.1.1: {}
+
+ strip-literal@3.1.0:
+ dependencies:
+ js-tokens: 9.0.1
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ tailwind-merge@3.6.0: {}
+
+ tailwindcss@4.3.2: {}
+
+ tapable@2.3.3: {}
+
+ tinybench@2.9.0: {}
+
+ tinyexec@0.3.2: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ tinypool@1.1.1: {}
+
+ tinyrainbow@2.0.0: {}
+
+ tinyspy@4.0.4: {}
+
+ ts-api-utils@2.5.0(typescript@5.9.3):
+ dependencies:
+ typescript: 5.9.3
+
+ tslib@2.8.1:
+ optional: true
+
+ tsx@4.22.4:
+ dependencies:
+ esbuild: 0.28.1
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+ '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+ '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+ eslint: 9.39.4(jiti@2.7.0)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.9.3: {}
+
+ undici-types@6.21.0: {}
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ vite-node@3.2.4(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ pathe: 2.0.3
+ vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)
+ transitivePeerDependencies:
+ - '@types/node'
+ - jiti
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
+ vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4):
+ dependencies:
+ esbuild: 0.25.12
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+ postcss: 8.5.16
+ rollup: 4.62.2
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 22.20.0
+ fsevents: 2.3.3
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ tsx: 4.22.4
+
+ vite@8.1.0(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.16
+ rolldown: 1.1.3
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 22.20.0
+ esbuild: 0.28.1
+ fsevents: 2.3.3
+ jiti: 2.7.0
+ tsx: 4.22.4
+
+ vitest@3.2.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4):
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/expect': 3.2.6
+ '@vitest/mocker': 3.2.6(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))
+ '@vitest/pretty-format': 3.2.6
+ '@vitest/runner': 3.2.6
+ '@vitest/snapshot': 3.2.6
+ '@vitest/spy': 3.2.6
+ '@vitest/utils': 3.2.6
+ chai: 5.3.3
+ debug: 4.4.3
+ expect-type: 1.4.0
+ magic-string: 0.30.21
+ pathe: 2.0.3
+ picomatch: 4.0.4
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.17
+ tinypool: 1.1.1
+ tinyrainbow: 2.0.0
+ vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)
+ vite-node: 3.2.4(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 22.20.0
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
+ void-elements@3.1.0: {}
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ word-wrap@1.2.5: {}
+
+ yocto-queue@0.1.0: {}
+
+ zod@3.25.76: {}
+
+ zustand@5.0.14(@types/react@19.2.17)(react@19.2.7):
+ optionalDependencies:
+ '@types/react': 19.2.17
+ react: 19.2.7
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..e9b0dad
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,3 @@
+packages:
+ - 'apps/*'
+ - 'packages/*'
diff --git a/scripts/check-i18n-keys.ts b/scripts/check-i18n-keys.ts
new file mode 100644
index 0000000..d3b4e7d
--- /dev/null
+++ b/scripts/check-i18n-keys.ts
@@ -0,0 +1,78 @@
+import { readdir, readFile } from 'node:fs/promises';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const localesDir = path.join(__dirname, '../apps/web/src/i18n/locales');
+
+async function loadKeys(dir: string): Promise> {
+ const locales = await readdir(dir, { withFileTypes: true });
+ const result: Record = {};
+
+ for (const locale of locales.filter((d) => d.isDirectory())) {
+ const localePath = path.join(dir, locale.name);
+ const files = await readdir(localePath);
+ const keys: string[] = [];
+
+ for (const file of files.filter((f) => f.endsWith('.json'))) {
+ const content = JSON.parse(await readFile(path.join(localePath, file), 'utf-8'));
+ const prefix = file.replace('.json', '');
+ flattenKeys(content, prefix, keys);
+ }
+
+ result[locale.name] = keys.sort();
+ }
+
+ return result;
+}
+
+function flattenKeys(obj: Record, prefix: string, out: string[]) {
+ for (const [key, value] of Object.entries(obj)) {
+ const fullKey = prefix ? `${prefix}.${key}` : key;
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
+ flattenKeys(value as Record, fullKey, out);
+ } else {
+ out.push(fullKey);
+ }
+ }
+}
+
+async function main() {
+ const keysByLocale = await loadKeys(localesDir);
+ const locales = Object.keys(keysByLocale);
+
+ if (locales.length < 2) {
+ console.log('Only one locale found, skipping parity check.');
+ return;
+ }
+
+ const [base, ...others] = locales;
+ const baseKeys = new Set(keysByLocale[base]);
+ let ok = true;
+
+ for (const locale of others) {
+ const localeKeys = new Set(keysByLocale[locale]);
+
+ for (const key of baseKeys) {
+ if (!localeKeys.has(key)) {
+ console.error(`Missing in ${locale}: ${key}`);
+ ok = false;
+ }
+ }
+
+ for (const key of localeKeys) {
+ if (!baseKeys.has(key)) {
+ console.error(`Extra in ${locale}: ${key}`);
+ ok = false;
+ }
+ }
+ }
+
+ if (!ok) process.exit(1);
+ console.log(`✓ i18n keys match across ${locales.join(', ')}`);
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/scripts/validate-content.ts b/scripts/validate-content.ts
new file mode 100644
index 0000000..2ae512c
--- /dev/null
+++ b/scripts/validate-content.ts
@@ -0,0 +1,62 @@
+import { readdir, readFile } from 'node:fs/promises';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { LessonSchema, ManifestSchema } from '@coderkeys/schemas';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const root = path.resolve(__dirname, '..');
+const contentDir = path.join(root, 'content');
+
+async function collectJsonFiles(dir: string): Promise {
+ const entries = await readdir(dir, { withFileTypes: true });
+ const files: string[] = [];
+
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ files.push(...(await collectJsonFiles(fullPath)));
+ } else if (entry.name.endsWith('.json') && entry.name !== 'manifest.json') {
+ files.push(fullPath);
+ }
+ }
+
+ return files;
+}
+
+async function main() {
+ const manifestPath = path.join(contentDir, 'manifest.json');
+ const manifestRaw = JSON.parse(await readFile(manifestPath, 'utf-8'));
+ ManifestSchema.parse(manifestRaw);
+
+ const lessonFiles = (await collectJsonFiles(path.join(contentDir, 'tracks'))).filter(
+ (f) => !f.endsWith('_meta.json'),
+ );
+
+ const ids = new Set();
+ let count = 0;
+
+ for (const file of lessonFiles) {
+ const raw = JSON.parse(await readFile(file, 'utf-8'));
+ const lesson = LessonSchema.parse(raw);
+
+ const localeFromPath = file.split(`${path.sep}tracks${path.sep}`)[1]?.split(path.sep)[1];
+ if (localeFromPath && lesson.locale !== localeFromPath) {
+ throw new Error(`Locale mismatch in ${file}: expected ${localeFromPath}, got ${lesson.locale}`);
+ }
+
+ const key = `${lesson.track}/${lesson.module}/${lesson.locale}/${lesson.id}`;
+ if (ids.has(key)) {
+ throw new Error(`Duplicate lesson id: ${key}`);
+ }
+ ids.add(key);
+ count++;
+ }
+
+ console.log(`✓ Manifest valid`);
+ console.log(`✓ ${count} lessons validated`);
+}
+
+main().catch((err) => {
+ console.error('Content validation failed:', err);
+ process.exit(1);
+});
diff --git a/tsconfig.base.json b/tsconfig.base.json
new file mode 100644
index 0000000..20a38ed
--- /dev/null
+++ b/tsconfig.base.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ }
+}