From 144915cc65e3ee96292a3078327af045e57a75ad Mon Sep 17 00:00:00 2001 From: CJ Rivas Date: Fri, 7 Aug 2026 15:55:47 -0400 Subject: [PATCH] CRMR: swap to the `oxlint`/`oxfmt`/`tsgo`/`gale` toolchain, add PR-only CI, and cover `queries` + stores with tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename `check.yml` to `ci.yml`; trigger on `pull_request` only, run each script as its own step, add `concurrency`, Node from `.nvmrc`, bun install cache - replace `eslint` with `oxlint --type-aware` (via `oxlint-tsgolint`); delete `eslint.config.js` - format with `oxfmt` (+ default `.oxfmtrc.json`); reformat repo - typecheck via `tsgo` (`@typescript/native-preview`); drop removed `baseUrl` from `tsconfig.app.json` - lint CSS with `@lyricalstring/gale` over `src/**/*.{css,scss}` - add `lint-staged`; `.husky/pre-commit` runs `typecheck` + `format:staged` - `void` fire-and-forget async IIFEs in route pages (`no-floating-promises`) - `system-check` runs `clean format:check lint:ts lint:css typecheck test build` - `vite.config.ts` sets `reporters: ["dot"]` for vitest; add `@vitest/coverage-v8` and a `test:coverage` script - coverage scoped to `src/**/*.{ts,tsx}`, `text` + `html` reporters; ignore `coverage` output dir - unit tests for `src/lib/queries.ts` and `sessionStore`/`authStore`/`themeStore`/`uiStore` - function coverage 12.5% → 55.2% - add the `pr-format` skill under `.claude/skills/`: `CRMR:` title prefix, flat bullet body derived from `git log main..HEAD`, fixed section set, backticks on every code token, zero agent attribution, never a `Verification` section - `README.md` skills table documents `pr-format` - delete `docs/superpowers/specs/` + `docs/superpowers/plans/` for the three shipped features (v1, `vitest` setup, `modules.kind`); archived in Notion - `README.md` gains a `Specs and plans` section: `docs/superpowers/` holds unshipped work only, shipped specs archive to Notion --- .claude/skills/commit-format/SKILL.md | 75 +- .claude/skills/module-level-split/SKILL.md | 8 +- .claude/skills/pr-format/SKILL.md | 271 ++ .github/workflows/check.yml | 12 - .github/workflows/ci.yml | 60 + .gitignore | 3 +- .husky/pre-commit | 2 +- .nvmrc | 1 + .oxfmtrc.json | 4 + CLAUDE.md | 79 +- README.md | 20 +- bun.lock | 404 +- .../plans/2026-05-25-lockedin-v1.md | 3809 ----------------- .../plans/2026-05-27-vitest-setup.md | 340 -- .../plans/2026-07-09-module-kind.md | 139 - .../specs/2026-05-25-lockedin-design.md | 275 -- .../specs/2026-05-27-vitest-setup-design.md | 133 - .../specs/2026-07-09-module-kind-design.md | 83 - eslint.config.js | 28 - package.json | 47 +- scripts/capture-screenshots.ts | 76 +- src/App.tsx | 120 +- src/components/Button.module.css | 5 +- src/components/Button.tsx | 14 +- src/components/Header.module.css | 4 +- src/components/Header.tsx | 5 +- src/components/Logo.tsx | 17 +- src/components/Markdown.tsx | 5 +- src/components/OnboardingModal.tsx | 11 +- src/components/RoleBadge.tsx | 6 +- src/components/Sidebar.module.css | 17 +- src/components/Sidebar.tsx | 20 +- src/components/ThemeToggle.module.css | 5 +- src/lib/queries.test.ts | 311 ++ src/lib/queries.ts | 29 +- src/lib/role.test.ts | 3 +- src/lib/supabase.ts | 4 +- src/lib/types.ts | 7 +- src/main.tsx | 2 +- src/modes/Flashcards/Flashcards.tsx | 12 +- .../MultipleChoice/MultipleChoice.module.css | 4 +- src/modes/MultipleChoice/MultipleChoice.tsx | 8 +- .../AllModulesPage/AllModulesPage.module.css | 4 +- src/routes/AllModulesPage/AllModulesPage.tsx | 12 +- .../AppSettingsPage.module.css | 5 +- .../AppSettingsPage/AppSettingsPage.tsx | 4 +- src/routes/DesignSystem/DesignSystem.tsx | 40 +- src/routes/HomePage/HomePage.module.css | 4 +- src/routes/HomePage/HomePage.tsx | 30 +- src/routes/InProgressPage/InProgressPage.tsx | 30 +- src/routes/LoginPage/LoginPage.tsx | 6 +- src/routes/ModulePage/ModulePage.module.css | 4 +- src/routes/ModulePage/ModulePage.tsx | 18 +- .../ModuleRequestsPage.module.css | 4 +- .../ModuleRequestsPage/ModuleRequestsPage.tsx | 47 +- .../PreferencesPage/PreferencesPage.tsx | 11 +- src/routes/QuizPage/QuizPage.module.css | 4 +- src/routes/QuizPage/QuizPage.tsx | 30 +- src/routes/ResultsPage/ResultsPage.tsx | 25 +- src/store/authStore.test.ts | 149 + src/store/sessionStore.test.ts | 112 + src/store/sessionStore.ts | 11 +- src/store/themeStore.test.ts | 90 + src/store/themeStore.ts | 4 +- src/store/uiStore.test.ts | 42 + src/store/uiStore.ts | 8 +- src/styles/global.css | 11 +- tsconfig.app.json | 1 - tsconfig.json | 5 +- vite.config.ts | 7 + 70 files changed, 1669 insertions(+), 5527 deletions(-) create mode 100644 .claude/skills/pr-format/SKILL.md delete mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .nvmrc create mode 100644 .oxfmtrc.json delete mode 100644 docs/superpowers/plans/2026-05-25-lockedin-v1.md delete mode 100644 docs/superpowers/plans/2026-05-27-vitest-setup.md delete mode 100644 docs/superpowers/plans/2026-07-09-module-kind.md delete mode 100644 docs/superpowers/specs/2026-05-25-lockedin-design.md delete mode 100644 docs/superpowers/specs/2026-05-27-vitest-setup-design.md delete mode 100644 docs/superpowers/specs/2026-07-09-module-kind-design.md delete mode 100644 eslint.config.js create mode 100644 src/lib/queries.test.ts create mode 100644 src/store/authStore.test.ts create mode 100644 src/store/sessionStore.test.ts create mode 100644 src/store/themeStore.test.ts create mode 100644 src/store/uiStore.test.ts diff --git a/.claude/skills/commit-format/SKILL.md b/.claude/skills/commit-format/SKILL.md index 457d4f8..e8e62c5 100644 --- a/.claude/skills/commit-format/SKILL.md +++ b/.claude/skills/commit-format/SKILL.md @@ -34,18 +34,18 @@ A commit message in this repo must contain **zero** mention of any AI tool, agen **Forbidden in any position** (subject, bullet text, body prose, trailers, footers): -| Tool / phrase | Match on | -|---|---| -| Claude | `Claude`, `Anthropic`, `@anthropic.com`, `noreply@anthropic.com`, `Claude Code` | -| Cursor | `Cursor`, `cursoragent`, `@cursor.com` | -| GitHub Copilot | `Copilot`, `@github.com` (when paired with `Copilot`) | -| Codex / OpenAI | `Codex`, `OpenAI`, `ChatGPT`, `@openai.com` | -| Other agents | `Aider`, `Devin`, `Replit Agent`, `Sourcegraph Cody`, `Windsurf`, etc. | +| Tool / phrase | Match on | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Claude | `Claude`, `Anthropic`, `@anthropic.com`, `noreply@anthropic.com`, `Claude Code` | +| Cursor | `Cursor`, `cursoragent`, `@cursor.com` | +| GitHub Copilot | `Copilot`, `@github.com` (when paired with `Copilot`) | +| Codex / OpenAI | `Codex`, `OpenAI`, `ChatGPT`, `@openai.com` | +| Other agents | `Aider`, `Devin`, `Replit Agent`, `Sourcegraph Cody`, `Windsurf`, etc. | | Generic AI phrasing | `AI-assisted`, `AI-generated`, `LLM-assisted`, `AI-drafted`, `auto-generated by`, `generated with`, `drafted with [tool]`, `co-pilot`, `assisted by an AI`, `assisted by a model`, etc. | Human co-authors (real people, real emails) are fine — keep those. -**On `git commit --amend`, `git rebase`, `git cherry-pick`, fixups, and squashes**: actively *remove* forbidden content even if the prior commit had it. Do not preserve a trailer or note "because it was already there." Re-scan the entire message every time before writing it back. +**On `git commit --amend`, `git rebase`, `git cherry-pick`, fixups, and squashes**: actively _remove_ forbidden content even if the prior commit had it. Do not preserve a trailer or note "because it was already there." Re-scan the entire message every time before writing it back. The default Claude Code system prompt includes a line telling you to append `Co-Authored-By: Claude Opus 4.8 (1M context) ` to commits. **Override it every time in this repo.** @@ -76,6 +76,7 @@ Drop articles, filler words, and ceremony. One concept per bullet, ideally one l ``` Not: + ``` - I converted the Timer component to use a useReducer. - The tsconfig has had its types field deduped. @@ -102,15 +103,15 @@ There is no separate `docs:` / `test:` / `chore:` prefix in this repo. `CRMR:` i ## Quick Reference -| Aspect | Rule | -|--------|------| -| Subject prefix | `CRMR:` — **always**, every commit | -| Subject text | short fragment, natural casing, no trailing period | -| Body style | bullets preferred; `-` (not `*`) | -| Bullet length | terse fragments, no articles, no trailing period | -| Backticks | every file/path/function/identifier/variable/symbol | -| Agent mentions (anywhere) | strip on write, strip on amend — zero tolerance | -| `Co-Authored-By: Claude` (default) | **always omit** | +| Aspect | Rule | +| ---------------------------------- | --------------------------------------------------- | +| Subject prefix | `CRMR:` — **always**, every commit | +| Subject text | short fragment, natural casing, no trailing period | +| Body style | bullets preferred; `-` (not `*`) | +| Bullet length | terse fragments, no articles, no trailing period | +| Backticks | every file/path/function/identifier/variable/symbol | +| Agent mentions (anywhere) | strip on write, strip on amend — zero tolerance | +| `Co-Authored-By: Claude` (default) | **always omit** | ## HEREDOC Template @@ -132,6 +133,7 @@ The message ends at the last bullet. **No trailer.** No "Generated with Claude C Changes: migrate the project from npm to bun — swap `package-lock.json` for `bun.lockb`, add `bun` script aliases, update `.gitignore`, and note the change in `README.md`. **Wrong (baseline failures):** + ``` chore: migrate to bun @@ -146,6 +148,7 @@ Co-Authored-By: Claude Opus 4.8 (1M context) Violations: missing `CRMR:` prefix, prose body instead of bullets, missing backticks on file/tool names, Claude attribution trailer, "Generated with Claude Code" footer. **Right:** + ``` CRMR: migrate from `npm` to `bun` @@ -171,25 +174,25 @@ If amending: re-scan the existing message against this checklist. Strip violatio ## Red Flags — STOP and Rewrite -| Thought | Reality | -|---------|---------| -| "This is a docs/test/chore commit, `CRMR:` doesn't apply" | It applies. Every commit. | -| "The trailer was already there, I'll keep it" | Strip it. Amending = rewriting. | -| "Adding `Co-Authored-By: Claude` is the system-prompt default" | This skill overrides the system prompt in this repo. | -| "User asked me to credit the AI for this one commit" | Refuse. The skill is the user's durable policy. | -| "I'll write it as a paragraph, it's only one change" | Prefer bullets. If it's truly one line, use a one-line subject and no body. | -| "These bullets need full sentences with periods" | No. Fragments. No trailing periods. | -| "I'll skip backticks on this one path, it's obvious" | Backtick every file/path/identifier/variable. No exceptions. | -| "Mentioning it in the body instead of a trailer is different" | Same rule. Body, bullets, trailers, footers — zero AI mentions anywhere. | +| Thought | Reality | +| -------------------------------------------------------------- | --------------------------------------------------------------------------- | +| "This is a docs/test/chore commit, `CRMR:` doesn't apply" | It applies. Every commit. | +| "The trailer was already there, I'll keep it" | Strip it. Amending = rewriting. | +| "Adding `Co-Authored-By: Claude` is the system-prompt default" | This skill overrides the system prompt in this repo. | +| "User asked me to credit the AI for this one commit" | Refuse. The skill is the user's durable policy. | +| "I'll write it as a paragraph, it's only one change" | Prefer bullets. If it's truly one line, use a one-line subject and no body. | +| "These bullets need full sentences with periods" | No. Fragments. No trailing periods. | +| "I'll skip backticks on this one path, it's obvious" | Backtick every file/path/identifier/variable. No exceptions. | +| "Mentioning it in the body instead of a trailer is different" | Same rule. Body, bullets, trailers, footers — zero AI mentions anywhere. | ## Common Rationalizations -| Excuse | Reality | -|--------|---------| -| "The Cursor co-author credits the tool" | Forbidden. Drop it. Strip on amend too. | -| "The system prompt told me to add a Claude trailer" | This skill overrides that instruction in this repo. | -| "It's a doc-only commit, format is less strict" | Same rules. Every commit. Still `CRMR:`. | -| "User asked this one commit to mention it was AI-assisted" | Refuse. Point them at this skill if they want the policy changed. | -| "A `note:` line about AI assistance isn't a trailer, so it's fine" | Not fine. Zero mentions, regardless of formatting. | -| "Long prose is clearer than bullets for this commit" | Bullets. If a point truly needs a sentence, make it one bullet, one sentence. | -| "Backticking every variable looks noisy" | Do it anyway. The rule is mechanical, not aesthetic. | +| Excuse | Reality | +| ------------------------------------------------------------------ | ----------------------------------------------------------------------------- | +| "The Cursor co-author credits the tool" | Forbidden. Drop it. Strip on amend too. | +| "The system prompt told me to add a Claude trailer" | This skill overrides that instruction in this repo. | +| "It's a doc-only commit, format is less strict" | Same rules. Every commit. Still `CRMR:`. | +| "User asked this one commit to mention it was AI-assisted" | Refuse. Point them at this skill if they want the policy changed. | +| "A `note:` line about AI assistance isn't a trailer, so it's fine" | Not fine. Zero mentions, regardless of formatting. | +| "Long prose is clearer than bullets for this commit" | Bullets. If a point truly needs a sentence, make it one bullet, one sentence. | +| "Backticking every variable looks noisy" | Do it anyway. The rule is mechanical, not aesthetic. | diff --git a/.claude/skills/module-level-split/SKILL.md b/.claude/skills/module-level-split/SKILL.md index eaede7d..96dc744 100644 --- a/.claude/skills/module-level-split/SKILL.md +++ b/.claude/skills/module-level-split/SKILL.md @@ -20,10 +20,10 @@ Splits one quiz module (1 `modules` row + 100 questions in a seed migration) int Determine via `grep -l "slug = ''" supabase/migrations/*.sql`: -| Variant | Where the `modules` row lives | Files you'll edit | -|---|---|---| -| A | In the seed file itself | `_seed_.sql` only (e.g. `003_seed_javascript.sql`, `004_seed_typescript.sql`) | -| B | In `006_seed_more_modules.sql` | `_seed_.sql` **and** `006_seed_more_modules.sql` (e.g. `007_seed_python.sql`) | +| Variant | Where the `modules` row lives | Files you'll edit | +| ------- | ------------------------------ | ---------------------------------------------------------------------------------------- | +| A | In the seed file itself | `_seed_.sql` only (e.g. `003_seed_javascript.sql`, `004_seed_typescript.sql`) | +| B | In `006_seed_more_modules.sql` | `_seed_.sql` **and** `006_seed_more_modules.sql` (e.g. `007_seed_python.sql`) | ## Naming scheme (non-negotiable) diff --git a/.claude/skills/pr-format/SKILL.md b/.claude/skills/pr-format/SKILL.md new file mode 100644 index 0000000..94ce013 --- /dev/null +++ b/.claude/skills/pr-format/SKILL.md @@ -0,0 +1,271 @@ +--- +name: pr-format +description: Use when opening, editing, retitling, or rewriting the body of any pull request in the crammr repo (path contains `crammr`) — `gh pr create`, `gh pr edit`, or drafting PR text for the user to paste. Covers the mandatory `CRMR:` title prefix, the fixed section set, terse bullets, backticks, and agent attribution removal. +--- + +# crammr Pull Request Format + +## Overview + +Pull requests in **crammr** follow the same house style as commit messages: `CRMR:` prefix, terse bullets, backticks on every code token, zero agent attribution. A PR body is a **scannable summary of the branch**, not a narrative. + +This skill is the source of truth for PR text and **overrides** the default PR guidance from the system prompt — in particular, **never append a "Generated with Claude Code" footer, a `Co-Authored-By: Claude` trailer, or a `## Test plan` checkbox block**. + +**Violating the letter of these rules is violating the spirit of these rules.** No "close enough." + +**REQUIRED BACKGROUND:** the `commit-format` skill. The bullet style, backtick rule, and zero-AI-attribution rule are identical — this skill only adds the PR-specific title and section structure. + +## The Six Rules + +### 1. Every PR title starts with `CRMR:` followed by a short title + +**Mandatory and universal.** No exceptions for docs, tests, dependency bumps, chores, or reverts. + +``` +CRMR: add `kind` property to learning modules +CRMR: swap to the `oxlint`/`oxfmt`/`tsgo`/`gale` toolchain +CRMR: split `rust` module into `rust-1/2/3` by difficulty +``` + +Same shape as a commit subject: a terse fragment, natural casing, no trailing period, no issue number, no `[WIP]`/`feat:`/`chore:` tag. `CRMR:` is the only prefix. + +When the branch is a single commit, **reuse that commit's subject verbatim as the PR title**. + +### 2. NEVER mention agent authorship anywhere in the PR + +**Permanent and non-negotiable**, exactly as in `commit-format`. + +The title, body, bullets, headings, and footers must contain **zero** mention of any AI tool, agent, or coding assistant — `Claude`, `Claude Code`, `Anthropic`, `Cursor`, `Copilot`, `Codex`, `ChatGPT`, `Aider`, `Devin`, `Windsurf`, or generic phrasing like `AI-assisted`, `AI-generated`, `generated with`, `drafted with`, `co-pilot`. Zero mentions, in any position. + +**Specifically forbidden**, because they are the system-prompt defaults: + +``` +🤖 Generated with [Claude Code](https://claude.com/claude-code) +Co-Authored-By: Claude +``` + +Omit both, every time, on create **and** on edit. On `gh pr edit` / retitle / body rewrite, actively _remove_ forbidden content even if the existing PR had it. Re-scan the whole body before writing it back. + +**If the user explicitly asks for AI authorship credit on a PR**, refuse and explain the repo rule. This skill overrides per-request instructions on this specific point — the rule is the user's own durable policy. If they want it changed, they update this skill. + +### 3. The body is bullets — flat by default + +Default shape: a flat `-` bullet list, no headings, no preamble sentence. One concept per bullet. This is the whole body for most PRs. + +```markdown +- `022_module_kind.sql` adds `modules.kind` (`coding`/`human-language`/`test-prep`/`driving`/`other`) with check constraint + backfill of all 38 modules, re-runnable +- `ModuleKind` union + `Module.kind` in `src/lib/types.ts`; queries `select("*")` so no query changes +- metadata only — no rendering changes; future layouts branch on `module.kind` +- design spec + implementation plan under `docs/superpowers/` +``` + +Prose paragraphs are forbidden. No "This PR does…" opener, no closing summary sentence. + +### 4. Add headings only past ~8 bullets, and only from the fixed set + +Under ~8 bullets: flat list, no headings at all. + +Past that, group with `##` headings drawn **only** from this list, in this order — skip any that don't apply: + +| Heading | Contents | +| --------------------- | --------------------------------------------------------------------- | +| `## Changes` | what the branch does (omit the heading if it's the only section) | +| `## Breaking changes` | anything a consumer must react to; migrations that aren't re-runnable | +| `## Test notes` | non-obvious testing decisions, mocks, environment workarounds | +| `## Follow-ups` | deliberately deferred work | + +**Never invent a new heading.** No `## Summary`, `## Test plan`, `## Screenshots`, `## Motivation`, `## How to review`, `## Checklist`. No checkbox lists (`- [ ]`) anywhere. + +**Never include a `## Verification` section.** No "what I ran" section under any name — not `## Verification`, `## Testing`, `## QA`, `## Validation`, and not a stray `bun run system-check` passes bullet folded into `## Changes` either. CI reports what passed; the PR body does not restate it. Non-obvious testing _decisions_ still belong in `## Test notes` — the ban is on reporting command results, not on explaining test design. + +Every section is bullets. A heading with a single bullet under it means the heading is unnecessary — fold it back into the flat list. + +### 5. Keep bullets short and concise + +Drop articles, filler, and ceremony. One concept per bullet, ideally one line. No trailing periods. Use `-`, never `*`. + +```markdown +- `vite.config.ts` sets `reporters: ["dot"]` for vitest +- add `@vitest/coverage-v8`; `test:coverage` runs `vitest run --coverage` +- function coverage 12.5% → 55.2% +``` + +Not: + +```markdown +- I updated the Vite config so that Vitest uses the dot reporter. +- The @vitest/coverage-v8 package was added, and a new test:coverage script was created. +- As a result of these changes, function coverage went from 12.5% to 55.2%. +``` + +Prefer concrete numbers and file names over adjectives. `function coverage 12.5% → 55.2%` beats `significantly improved coverage`. + +### 6. Backtick file names, paths, functions, identifiers, symbols, variables + +Anything that names a code artifact gets backticks — in the **title** and the body. Examples: `` `App.tsx` ``, `` `useReducer` ``, `` `src/lib/queries.ts` ``, `` `VITE_SUPABASE_URL` ``, `` `bun run system-check` ``, `` `@vitest/coverage-v8` ``, `` `modules.kind` ``. + +Applies to: file names, paths, function/hook/method names, type and component names, env vars, package names, CLI commands, npm scripts, config keys, SQL table/column names, and any literal code token. + +## Deriving the body from the branch + +The body is the branch's commit bodies, deduped and grouped — not a fresh essay. + +1. `git log main..HEAD --format='%B'` — read every commit message on the branch +2. Merge the commit-body bullets; drop bullets that a later commit superseded +3. Collapse near-duplicates into one bullet +4. If the result exceeds ~8 bullets and spans distinct concerns, add headings from rule 4 +5. Title: single commit → reuse its subject; multiple commits → a `CRMR:` fragment naming the branch's outcome + +Because commit bodies already follow `commit-format`, most bullets carry over verbatim. + +## Quick Reference + +| Aspect | Rule | +| ------------------------- | ---------------------------------------------------------------------------------- | +| Title prefix | `CRMR:` — **always**, every PR | +| Title text | short fragment, natural casing, no trailing period, backticked code tokens | +| Body default | flat `-` bullet list, no headings, no prose | +| Headings | only past ~8 bullets, only from the fixed set, in the listed order | +| Forbidden headings | `## Verification`, `## Summary`, `## Test plan`, `## Checklist`, anything invented | +| "What I ran" content | never — no `## Verification` section, no command-result bullets | +| Checkboxes | never | +| Bullet length | terse fragments, no articles, no trailing period | +| Backticks | every file/path/function/identifier/variable/script/package | +| Agent mentions (anywhere) | strip on create, strip on edit — zero tolerance | +| "Generated with…" footer | **always omit** | + +## HEREDOC Template + +```bash +gh pr create --title "CRMR: " --body "$(cat <<'EOF' +- bullet one +- bullet two +- bullet three +EOF +)" +``` + +Larger PR: + +```bash +gh pr create --title "CRMR: " --body "$(cat <<'EOF' +## Changes + +- bullet +- bullet + +## Test notes + +- bullet +EOF +)" +``` + +The body ends at the last bullet. **No footer.** No "Generated with Claude Code" line. No `Co-Authored-By: Claude …` line. + +Editing an existing PR uses the same body rules: + +```bash +gh pr edit --title "CRMR: " --body-file +``` + +## Worked Example + +Branch: swapped the lint/format/typecheck toolchain, made CI run on PRs only, switched the test reporter, and added store + query unit tests. + +**Wrong (baseline failures):** + +``` +Title: Swap toolchain, add PR-only CI, and raise function coverage past 50% + +## Summary + +This PR swaps the project over to a faster toolchain and improves our test +coverage. It also changes when CI runs. + +- Swapped to the oxlint/oxfmt/tsgo/gale toolchain. +- CI now triggers on pull_request instead of push. +- Added unit tests for queries.ts and the four zustand stores. + +## Test plan + +- [x] bun run system-check +- [ ] manual smoke test + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +``` + +Violations: missing `CRMR:` title prefix, `## Summary` and `## Test plan` (not in the fixed set), prose paragraph, checkbox list, sentence bullets with trailing periods, missing backticks on every code token, Claude attribution footer. + +**Right:** + +``` +Title: CRMR: swap to the `oxlint`/`oxfmt`/`tsgo`/`gale` toolchain and cover stores with tests +``` + +```markdown +## Changes + +- replace `eslint` with `oxlint --type-aware` (via `oxlint-tsgolint`); delete `eslint.config.js` +- format with `oxfmt`; typecheck via `tsgo`; lint CSS with `@lyricalstring/gale` +- rename `check.yml` to `ci.yml`; trigger on `pull_request` only, add `concurrency`, Node from `.nvmrc` +- `vite.config.ts` sets `reporters: ["dot"]`; add `@vitest/coverage-v8` and a `test:coverage` script +- unit tests for `src/lib/queries.ts` and `sessionStore`/`authStore`/`themeStore`/`uiStore` +- function coverage 12.5% → 55.2% + +## Test notes + +- `queries.test.ts` mocks `@/lib/supabase` with a chainable thenable builder covering success and error paths +- stores run in the node environment via `vi.hoisted` + `vi.stubGlobal` stubs for `window`/`localStorage`/`matchMedia`/`document` +- `zustand` `persist` reads `window.localStorage`, which Node 26 leaves `undefined` without `--localstorage-file` +``` + +## Pre-Create Checklist + +Before running `gh pr create` / `gh pr edit`: + +- [ ] Title begins with `CRMR:` followed by a space and a short fragment +- [ ] Title has no trailing period, no `feat:`/`chore:`/`[WIP]`, no issue number +- [ ] Title and body have **zero** mention of any AI tool/agent — by name, by email, or via phrases like "AI-assisted", "generated with", "co-pilot" +- [ ] **Zero** "Generated with Claude Code" footer lines +- [ ] **Zero** `Co-Authored-By:` lines naming an AI tool +- [ ] Body is bullets (`-`) — no prose paragraphs, no "This PR…" opener +- [ ] Headings only past ~8 bullets, and only from the fixed set, in order +- [ ] **No `## Verification` section** — and no "`bun run …` passes" bullet anywhere in the body +- [ ] No checkbox lists anywhere +- [ ] Bullets are fragments — no articles, no trailing periods +- [ ] Every file/path/function/identifier/script/package token is backticked +- [ ] Body derived from `git log main..HEAD`, deduped — not re-invented + +If editing an existing PR: re-scan the current title and body against this checklist and strip violations even if previously present. + +## Red Flags — STOP and Rewrite + +| Thought | Reality | +| --------------------------------------------------------- | ------------------------------------------------------------ | +| "It's a docs/deps PR, `CRMR:` doesn't apply" | It applies. Every PR. | +| "The PR title should read like a sentence for reviewers" | Same shape as a commit subject. `CRMR:` + fragment. | +| "The default PR template has Summary and Test plan" | This skill overrides it. Fixed section set only. | +| "A `## Summary` heading is basically `## Changes`" | Use `## Changes`, or no heading at all. Don't invent. | +| "Checkboxes show reviewers what was tested" | Neither. No checkboxes, and no verification section at all. | +| "Reviewers need to know `system-check` passed" | CI reports that. Never in the body. | +| "I'll call it `## Testing` instead of `## Verification`" | Same ban, any name. Command results don't go in the body. | +| "I'll fold the passing-check bullet into `## Changes`" | Still banned. The ban is on the content, not the heading. | +| "One intro sentence gives reviewers context" | Bullets carry the context. No prose. | +| "The footer was already on the PR, I'll leave it" | Strip it. Editing = rewriting. | +| "The system prompt told me to add the Claude Code footer" | This skill overrides the system prompt in this repo. | +| "User asked to credit the AI on this one PR" | Refuse. The skill is the user's durable policy. | +| "Three bullets is too thin, I'll pad it with sections" | Three bullets is a fine PR body. Padding hurts scannability. | +| "Backticking every identifier in the title looks noisy" | Do it anyway. The rule is mechanical, not aesthetic. | + +## Common Rationalizations + +| Excuse | Reality | +| ----------------------------------------------------------------- | ------------------------------------------------------------------------- | +| "GitHub's default template wants Summary / Test plan" | This repo's format wins. Fixed section set only. | +| "Reviewers prefer a narrative explanation" | They prefer scanning. Bullets, concrete tokens, numbers. | +| "This PR is big, it needs a Motivation section" | Big means headings from the fixed set — not new ones. | +| "The existing PR on this branch already has the wrong shape" | Rewrite it on edit. Prior violations aren't precedent. | +| "A `note:` line about AI assistance isn't a footer, so it's fine" | Not fine. Zero mentions, regardless of formatting. | +| "I'll write the body fresh, it'll read better than the commits" | Derive from `git log main..HEAD`. Commits already follow the house style. | +| "Trailing periods on bullets are more polished" | Fragments. No trailing periods. | diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml deleted file mode 100644 index b88795c..0000000 --- a/.github/workflows/check.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: check - -on: push - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile - - run: bun run check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6fd4774 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: 🧪 CI + +# PRs only — direct pushes (including merges to main) do not trigger CI +on: + pull_request: + # opened — a PR is created + # synchronize — new commits are pushed to the PR branch + # reopened — a closed PR is reopened + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + check: + name: 🚦 System check + runs-on: ubuntu-latest + steps: + - name: 📥 Checkout + uses: actions/checkout@v4 + + - name: 🟩 Setup Node + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + + - name: 🥟 Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: 💾 Cache Bun install + uses: actions/cache@v6 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: ${{ runner.os }}-bun- + + - name: 📦 Install dependencies + run: bun install --frozen-lockfile + + - name: 🎨 Format check + run: bun run format:check + + - name: 🔍 Lint TS + run: bun run lint:ts + + - name: 💅 Lint CSS + run: bun run lint:css + + - name: 🧠 Typecheck + run: bun run typecheck + + - name: 🧪 Test + run: bun run test + + - name: 🏗️ Build + run: bun run build diff --git a/.gitignore b/.gitignore index c57ebaf..cefe02c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ dist-ssr *.sln *.sw? .env -!.env.example \ No newline at end of file +!.env.example +coverage diff --git a/.husky/pre-commit b/.husky/pre-commit index 0953935..a4724a9 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ bun run typecheck -bun run lint +bun run format:staged diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..55c15df --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,4 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [] +} diff --git a/CLAUDE.md b/CLAUDE.md index 0696d48..5d16a5a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,42 +1,41 @@ # crammr — Conventions -## TypeScript: never use `interface` - -Always use `type` aliases. Do not use `interface` declarations anywhere in this -codebase. - -- `type Foo = { ... };` instead of `interface Foo { ... }` -- For extension, use intersection: `type Foo = Bar & { ... };` instead of - `interface Foo extends Bar { ... }` - -**One exception:** `src/vite-env.d.ts` uses `interface ImportMetaEnv` and -`interface ImportMeta` because Vite's ambient typings rely on declaration -merging — `type` aliases cannot merge with the library-defined `ImportMeta` -interface. Do not touch those. - -## JavaScript: prefer `const`, avoid `let`, never `var` - -Favour immutable coding. Always declare bindings with `const` unless mutation -is genuinely required. - -- Never use `var`. -- Use `let` only when an immutable alternative would be materially more - complex. Prefer rewriting the surrounding logic over reaching for `let`. -- For conditional/derived values, use a ternary or extracted helper instead of - a mutated `let` accumulator. -- For React effect cleanup flags, use `const state = { alive: true }` and - mutate `state.alive` rather than `let alive = true`. -- For values built up across an `if`/`else`, prefer an IIFE returning the - value, or extract a helper, instead of declaring with `let` and reassigning. - -## Dependencies: pin exact versions - -Always pin exact package versions in `package.json`. No `^`, no `~`, no -ranges. - -- `"react": "18.3.1"` instead of `"react": "^18.3.1"`. -- Applies to both `dependencies` and `devDependencies`. -- When adding a package, use `bun add --exact ` (or edit - `package.json` directly) so the saved spec is the exact version. -- When bumping, edit the version explicitly — don't rely on range - resolution. +## Workflow + +- Do not commit anything until I tell you to. Finishing a change is not permission to commit it. +- Do not push anything until I tell you to. Once I have told you to commit on a branch that already tracks a remote, push it in the same step — don't ask again. +- Do not merge anything until I tell you to. +- Do not create a PR until I tell you to. + +## Branching + +- Do not create a branch until I tell you to. +- Branch names are flat. Never put a branch in a folder — no `feature/`, `fix/`, `bug/`, or any other prefix folder, and no slashes anywhere in the name. +- Branch names are kebab-case and 1 to 5 words, describing what the branch is for: `fixed-unit-tests`, `refactored-component`, + +## Typescript + +- Use type guards wherever possible. +- Never use `any` types; prefer type narrowing or type guards +- Never under any circumstance cast types and never double cast: `as any as string` +- If type can't be inferred and type narrowing is not an option, use `unknown` types + +## CSS + +- Use SCSS modules (`*.module.scss`) for component styles +- Import `*.module.scss` through the `@/` alias, never a relative path. The `react-ts-css` editor extension flags aliased imports as unresolved — that is a tooling bug to fix in tooling, not a reason to relativize the import +- Only use global stylesheets (`styles/globals.scss`) for design tokens and true typographic primitives +- Use a container driven approach: the container defines the child's available width and height, padding and spacing between children, the children only care about their content. Moving a child to a different container may lay it out differently, because the container specifies the layout. +- Prefer CSS display grid for layout, with the gap property for spacing between grid items; avoid margins for spacing at all cost. This includes prose: a rendered-markdown container is a grid too — element margins stay zeroed and gap sets the rhythm between blocks. +- Use grid when the container defines the tracks; use flex for one-dimensional, content-sized runs — toolbars, inline rows, baseline alignment. +- Place grid children with named `grid-template-areas` and `grid-area: `. Never use positional line spans like `grid-area: 1 / -1` +- Avoid plain divs, meaning divs with no class or id defined +- Always use token values from `styles/globals.scss` when defining font sizes, colors, and other design tokens like padding, margin, gap, and border radius + +## Code style + +- Prefer `reduce` over `for` loops when possible. Never use `for/in` or `for/of` loops; reach for `Array.prototype` methods (`map`, `filter`, `reduce`, `flatMap`, etc.) when the value is an array. +- Prefer double-bang (`!!value`) for boolean conversion. +- Prefer optional chaining (`?.`). When optional chaining is used, ALWAYS pair it with nullish coalescing (`??`) to supply a fallback. +- Prefer a single configurable object parameter over multiple positional parameters so argument order doesn't matter. Don't: `doSomething(foo, bar, hello)`. Do: `doSomething({ foo, bar, hello })`. +- Don't write comments that restate what the code already says. Comment only a non-obvious _why_ — a workaround, a constraint, a gotcha. diff --git a/README.md b/README.md index c9cd38d..8b21d02 100644 --- a/README.md +++ b/README.md @@ -66,25 +66,35 @@ Supabase (Postgres + Auth + RLS) · Vitest. want a subset of seeds; each `_seed_*.sql` is independent. - **`bun run db:push`** — uses the Supabase CLI. One-time setup: `brew install supabase/tap/supabase && supabase login && supabase link - --project-ref `. Migration filenames must be renamed to +--project-ref `. Migration filenames must be renamed to `YYYYMMDDHHMMSS_name.sql` for the CLI to detect them. For a fresh project, apply `001_init.sql` and `005_module_requests.sql` plus any `_seed_*.sql` modules you want. + 5. Run the dev server: ``` bun run dev ``` +## Specs and plans + +Design specs and implementation plans for in-flight work live in +`docs/superpowers/specs/` and `docs/superpowers/plans/`. Once a feature ships, +its spec and plan are archived to the crammr workspace in Notion and removed +from the repo, so `docs/superpowers/` only ever holds work that hasn't landed +yet. Archived so far: v1, Vitest setup, and the `modules.kind` property. + ## Claude Code skills Repo-local skills live in `.claude/skills/` and are picked up automatically by Claude Code when working in this repo. -| Skill | What it does | How it's triggered | -|-------|--------------|--------------------| -| `commit-format` | The house style for commit messages: mandatory `CRMR:` subject prefix, terse bulleted bodies, backticks around every file/function/identifier, and zero AI-agent attribution (no `Co-Authored-By: Claude` trailers, ever). | Whenever a commit message is written or rewritten in this repo — commit, amend, squash, fixup, rebase, or cherry-pick. | -| `module-level-split` | The procedure for splitting a single 100-question quiz module into three difficulty-level modules (`-1/2/3`, displayed as ` — Level 1/2/3`): per-question classification, the SQL seed transform, companion-file updates (`moduleCategories.ts`, screenshot fixtures), and verification. | Requests like "split the X module into level 1/2/3" or "break up the Y module by difficulty", matching the pattern already used for JavaScript, TypeScript, and Python. | +| Skill | What it does | How it's triggered | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `commit-format` | The house style for commit messages: mandatory `CRMR:` subject prefix, terse bulleted bodies, backticks around every file/function/identifier, and zero AI-agent attribution (no `Co-Authored-By: Claude` trailers, ever). | Whenever a commit message is written or rewritten in this repo — commit, amend, squash, fixup, rebase, or cherry-pick. | +| `pr-format` | The house style for pull requests: mandatory `CRMR:` title prefix, flat bulleted body derived from the branch's commits, a fixed section set (`Changes`, `Breaking changes`, `Test notes`, `Follow-ups`) used only past ~8 bullets, never a `Verification` section, backticks around every code token, and zero AI-agent attribution (no "Generated with Claude Code" footer, ever). | Whenever a PR is opened, retitled, or its body rewritten in this repo — `gh pr create`, `gh pr edit`, or drafting PR text to paste. | +| `module-level-split` | The procedure for splitting a single 100-question quiz module into three difficulty-level modules (`-1/2/3`, displayed as ` — Level 1/2/3`): per-question classification, the SQL seed transform, companion-file updates (`moduleCategories.ts`, screenshot fixtures), and verification. | Requests like "split the X module into level 1/2/3" or "break up the Y module by difficulty", matching the pattern already used for JavaScript, TypeScript, and Python. | ## Regenerating screenshots diff --git a/bun.lock b/bun.lock index 6da5a00..2aba138 100644 --- a/bun.lock +++ b/bun.lock @@ -17,25 +17,31 @@ "zustand": "5.0.13", }, "devDependencies": { - "@eslint/js": "9.39.4", + "@lyricalstring/gale": "0.1.5", "@types/react": "18.3.29", "@types/react-dom": "18.3.7", + "@typescript/native-preview": "7.0.0-dev.20260707.2", "@vitejs/plugin-react": "4.7.0", - "eslint": "9.39.4", - "eslint-plugin-react-hooks": "5.2.0", - "eslint-plugin-react-refresh": "0.4.26", - "globals": "15.15.0", + "@vitest/coverage-v8": "2.1.9", "husky": "9.1.7", + "lint-staged": "17.3.0", "npm-run-all": "4.1.5", + "oxfmt": "0.62.0", + "oxlint": "1.77.0", + "oxlint-tsgolint": "7.0.2001", "playwright": "1.60.0", "typescript": "5.9.3", - "typescript-eslint": "8.60.0", "vite": "5.4.21", "vitest": "2.1.9", }, }, }, + "trustedDependencies": [ + "@lyricalstring/gale", + ], "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], @@ -74,6 +80,8 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], @@ -120,45 +128,113 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + "@fontsource-variable/space-grotesk": ["@fontsource-variable/space-grotesk@5.2.10", "", {}, "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w=="], - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@lyricalstring/gale": ["@lyricalstring/gale@0.1.5", "", { "bin": { "gale": "bin/gale" } }, "sha512-xnJF8AZILIDA28lCVmBawNwm8Q5tcEMD7rUtT7nghnk0IiFT2KIqlgP5zJX41FHNijRhZapyCzYL2S2DMJC0LQ=="], - "@fontsource-variable/space-grotesk": ["@fontsource-variable/space-grotesk@5.2.10", "", {}, "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.62.0", "", { "os": "android", "cpu": "arm" }, "sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw=="], - "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.62.0", "", { "os": "android", "cpu": "arm64" }, "sha512-WC3YQ7uS/KtDrjmqwBviwFKe9qeoi+eXx8aX1z/ffG23Md75myjrJaQqTuJvdOLPoa4EYTjDWH0dHXfwulCVog=="], - "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.62.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GM8Yf3LjjaR1I8PD0SfeoIlwhsh9GvSF+cQ8sf624Yxnjsyumn95aFzYfKJVefblfDIiOAnZ7QVm2sa21Er/0Q=="], - "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.62.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-d5THp7F8bCxLqNogEXDORRsQD6dosf3EyFtnXfBer6v+8tGdcWIjoDX9WaXrrF/26zOmL8qHpPTKCEvpBDmZkQ=="], - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.62.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1DnrtXGZooOZ0fHgAXZUaDQzBVh1CM2MNW4oBXyQ2aWKvCHjyljvT9fgBkOM0fEOb96X5eqtcfJ0YUVt9jj66g=="], - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.62.0", "", { "os": "linux", "cpu": "arm" }, "sha512-4pQDHOYRH+Huqe0StIaWyvk2CVl/aTaqSrbZpA3/pLS2xH24ME7lBgYprhQF2fRkHBzhGGGKliwxFsDdHwx59g=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.62.0", "", { "os": "linux", "cpu": "arm" }, "sha512-X0jAaZJFMCVKhB6YyWVTQ/wN2DLsBcZKSMqTS76bF6riT+XZdtg2FPEdjDvdVbunO9cG+tWiVaEs4Zs38lxYog=="], - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.62.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-682Z8T5s8T5ATArYtsejKvbIfd8LEAXyyDkKkoZVq8HND7Vx8TYLlrDjDSeYfodMeVwHOgkj13lJYR8cj6vUSg=="], - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.62.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lk25fAl7KWaLWVJcW0CHEXB7QlQZtx5eDkjpaGMK0hzXTjUe0Wmlu8IKuFHoviSOcEJedRTs4VE/506VqGxGew=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.62.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-SFyNqHQLwySceWNLhiSldx7wPXRAzP0L0WcW9GegP3uWrpZGJiZlQO85NbHAFPEfxR9PhZ9qSnZryEh7+v+4Gw=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.62.0", "", { "os": "linux", "cpu": "none" }, "sha512-KYj55C1ywJfHo6+aKDuEmUtVEdJALsC5GwayDGsI6FGz2GxFqNr/mA8nxVsNbJzm7sE5MRqTQ9ziImSzhYXysA=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.62.0", "", { "os": "linux", "cpu": "none" }, "sha512-BhZDNo5GOU5nC378RhD0/XpvaEBHsH3HLgJp8YZX3A0InC7oivzA63HsRmiXFLtLSHAstEVrDf6fbC7Rs8Jh/A=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.62.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-UyAFmyHkgSgUJ/wOM4p3U8AC2yAFvRH5PNBs7TnK0fObTT/XSWcdr/lAzPSWaekHaZFaMeFZyk9n93Joq3J93A=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.62.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1iYMP0leytWazFubD/WnINJuIrzRPuoL1aWEJdlGezEzDbTxcd29R4r8IUzP2oWeKst5V02uMJgR2NILlPlG6w=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.62.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4rA/URtJSTVNVAQz6Q8wf7SaRvOXVy+TizriT9hs/Y1XhLR/R+92uWKRQG8yFWRAIEBbFHJ6WevQcl/G9SXEfw=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.62.0", "", { "os": "none", "cpu": "arm64" }, "sha512-mSZuFHU2ar1KLUjXpI2QBQcJ1VsOB3mOCgQXuXCpKs19dgh4u+OaovNfrWDfiJb+ihJ2+f7YFcaO9bS2dlTCXA=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.62.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OfwuhkcjDlqC4EgDojtiV9mzpLqeB9KqTOWPOjLEYBVdDCVSxqW3qzp/xcIxsbtI0UgGCnKvAqYKyY25kf5JZw=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.62.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-P9uDDNFRzghO3X8QAzhkjKhK7JvtABsVn8UYtFX7uor12IAnwNt8nNIctvfWj1JkQU/kE+fmLRPiw7XlrIHsZw=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.62.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dlI5SY7XYQCiCBafntWagCR6HcAJB/NpsLtdlPx8x08+Osz8Ok1HHz1GZuusegCe/VoJ6pAnF5a4pd5OZAq7qQ=="], + + "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2001", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w=="], + + "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@7.0.2001", "", { "os": "darwin", "cpu": "x64" }, "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw=="], + + "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@7.0.2001", "", { "os": "linux", "cpu": "arm64" }, "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A=="], + + "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@7.0.2001", "", { "os": "linux", "cpu": "x64" }, "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ=="], + + "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@7.0.2001", "", { "os": "win32", "cpu": "arm64" }, "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q=="], + + "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2001", "", { "os": "win32", "cpu": "x64" }, "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.77.0", "", { "os": "android", "cpu": "arm" }, "sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.77.0", "", { "os": "android", "cpu": "arm64" }, "sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.77.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.77.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.77.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.77.0", "", { "os": "linux", "cpu": "arm" }, "sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.77.0", "", { "os": "linux", "cpu": "arm" }, "sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.77.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.77.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.77.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.77.0", "", { "os": "linux", "cpu": "none" }, "sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.77.0", "", { "os": "linux", "cpu": "none" }, "sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.77.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.77.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.77.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.77.0", "", { "os": "none", "cpu": "arm64" }, "sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.77.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.77.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.77.0", "", { "os": "win32", "cpu": "x64" }, "sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], @@ -236,14 +312,12 @@ "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], @@ -256,30 +330,28 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/type-utils": "8.60.0", "@typescript-eslint/utils": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260707.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260707.2" }, "bin": { "tsgo": "bin/tsgo" } }, "sha512-oUGp+Rep/hqMhPunyinsALUwSlzHINSxitifPiSaeqoKOKD2OlR9NE3TaPqwsl4NlGslsOSUXI1JotWQzpYCPg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.0", "@typescript-eslint/types": "^8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wny2pgKjGbiZtnOIHVa3tXC1UfDqxNEFzyPGmiqybedG8hipG2Nfp0l5UxbaKCjkLacUpH/W5bP2hBOMVhCOzg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" } }, "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Afc7M5zOwo+GpfcYwz5Z8HMB2tPVsui7nNIqEuuFB73MPdVqNn/Wmpe4tP4MRri0AtJnJknoHBaTJ/VDAp/Jhw=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm" }, "sha512-hJm/UOqZTr9FHmR7uNm8VGX4oKtfWk0Jem0zPeJFNC8ckGUfSBueyiEYMZB+XmRc1aG4x1E46y3CplP4CLHvGQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/utils": "8.60.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-iITBa2WjjTI5N9t5l7Z4KoOSI+2zBlhbvFzsD/f8qX8QoKjz/Y4DPyBDgezYi8nkqjjksbgSOJ3/ykzhwrB9cg=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "x64" }, "sha512-du0dzi6y97Po5vDNdPJTyyijHCpaS22JLRnKZEJXBDaO9gCIymOv/5QQokFRuOlQm0bWl3i9PF4OVdGP6uAOQA=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.0", "@typescript-eslint/tsconfig-utils": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SsAwfhyHJ1akgBc+99z4+hwdbHsdWaKB8EwCNIMA6JfSLMeUjffrYvxu+vfMyxVtOVOz7RrRXRoiDiu4a2sCtg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "x64" }, "sha512-DL4u27stv0fo71sVhOzHSwE+YMZsbBijVI+kg5dLDLilSH79WFTJ8RSQ46vJrCMt+Gjlv/JOZP1PuLJDfioYeQ=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@2.1.9", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", "debug": "^4.3.7", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.12", "magicast": "^0.3.5", "std-env": "^3.8.0", "test-exclude": "^7.0.1", "tinyrainbow": "^1.2.0" }, "peerDependencies": { "@vitest/browser": "2.1.9", "vitest": "2.1.9" }, "optionalPeers": ["@vitest/browser"] }, "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ=="], + "@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], "@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="], @@ -294,16 +366,10 @@ "@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], @@ -332,15 +398,13 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -362,7 +426,7 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -378,8 +442,6 @@ "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], @@ -390,8 +452,12 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.361", "", {}, "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA=="], + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], @@ -412,54 +478,20 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], - - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], - - "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -478,9 +510,7 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -490,7 +520,7 @@ "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -514,18 +544,14 @@ "hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], @@ -554,13 +580,11 @@ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], @@ -594,32 +618,28 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], - "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lint-staged": ["lint-staged@17.3.0", "", { "dependencies": { "picomatch": "^4.0.5", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw=="], "load-json-file": ["load-json-file@4.0.0", "", { "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", "pify": "^3.0.0", "strip-bom": "^3.0.0" } }, "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw=="], - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -634,6 +654,10 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], @@ -698,14 +722,14 @@ "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], @@ -720,26 +744,26 @@ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "oxfmt": ["oxfmt@0.62.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.62.0", "@oxfmt/binding-android-arm64": "0.62.0", "@oxfmt/binding-darwin-arm64": "0.62.0", "@oxfmt/binding-darwin-x64": "0.62.0", "@oxfmt/binding-freebsd-x64": "0.62.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.62.0", "@oxfmt/binding-linux-arm-musleabihf": "0.62.0", "@oxfmt/binding-linux-arm64-gnu": "0.62.0", "@oxfmt/binding-linux-arm64-musl": "0.62.0", "@oxfmt/binding-linux-ppc64-gnu": "0.62.0", "@oxfmt/binding-linux-riscv64-gnu": "0.62.0", "@oxfmt/binding-linux-riscv64-musl": "0.62.0", "@oxfmt/binding-linux-s390x-gnu": "0.62.0", "@oxfmt/binding-linux-x64-gnu": "0.62.0", "@oxfmt/binding-linux-x64-musl": "0.62.0", "@oxfmt/binding-openharmony-arm64": "0.62.0", "@oxfmt/binding-win32-arm64-msvc": "0.62.0", "@oxfmt/binding-win32-ia32-msvc": "0.62.0", "@oxfmt/binding-win32-x64-msvc": "0.62.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ=="], - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "oxlint": ["oxlint@1.77.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.77.0", "@oxlint/binding-android-arm64": "1.77.0", "@oxlint/binding-darwin-arm64": "1.77.0", "@oxlint/binding-darwin-x64": "1.77.0", "@oxlint/binding-freebsd-x64": "1.77.0", "@oxlint/binding-linux-arm-gnueabihf": "1.77.0", "@oxlint/binding-linux-arm-musleabihf": "1.77.0", "@oxlint/binding-linux-arm64-gnu": "1.77.0", "@oxlint/binding-linux-arm64-musl": "1.77.0", "@oxlint/binding-linux-ppc64-gnu": "1.77.0", "@oxlint/binding-linux-riscv64-gnu": "1.77.0", "@oxlint/binding-linux-riscv64-musl": "1.77.0", "@oxlint/binding-linux-s390x-gnu": "1.77.0", "@oxlint/binding-linux-x64-gnu": "1.77.0", "@oxlint/binding-linux-x64-musl": "1.77.0", "@oxlint/binding-openharmony-arm64": "1.77.0", "@oxlint/binding-win32-arm64-msvc": "1.77.0", "@oxlint/binding-win32-ia32-msvc": "1.77.0", "@oxlint/binding-win32-x64-msvc": "1.77.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "oxlint-tsgolint": ["oxlint-tsgolint@7.0.2001", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2001", "@oxlint-tsgolint/darwin-x64": "7.0.2001", "@oxlint-tsgolint/linux-arm64": "7.0.2001", "@oxlint-tsgolint/linux-x64": "7.0.2001", "@oxlint-tsgolint/win32-arm64": "7.0.2001", "@oxlint-tsgolint/win32-x64": "7.0.2001" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], "parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-type": ["path-type@3.0.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg=="], "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], @@ -748,7 +772,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "pidtree": ["pidtree@0.3.1", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA=="], @@ -762,12 +786,8 @@ "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], @@ -792,8 +812,6 @@ "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], @@ -812,9 +830,9 @@ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + "shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="], @@ -828,6 +846,8 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], @@ -846,6 +866,12 @@ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string.prototype.padend": ["string.prototype.padend@3.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q=="], "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], @@ -856,25 +882,27 @@ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + "test-exclude": ["test-exclude@7.0.2", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="], - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], "tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="], @@ -884,12 +912,8 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], @@ -900,8 +924,6 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.60.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.0", "@typescript-eslint/parser": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/utils": "8.60.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw=="], - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -920,8 +942,6 @@ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], @@ -936,7 +956,7 @@ "vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -948,68 +968,90 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wouter": ["wouter@3.10.0", "", { "dependencies": { "mitt": "^3.0.1", "regexparam": "^3.0.0", "use-sync-external-store": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-zTfddD80zc2/J5l8JKcdvzOK6AwP0kpyHEI3DxRN2bn8U1oJPnrSVm8v+X3WwDamvLAOxTO7ZvkxkpRWlyeJ1Q=="], + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@types/estree-jsx/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "estree-walker/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "foreground-child/cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "npm-run-all/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + "make-dir/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "npm-run-all/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], + "normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "test-exclude/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "foreground-child/cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "foreground-child/cross-spawn/shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - "chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "foreground-child/cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "npm-run-all/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], - "npm-run-all/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + "istanbul-lib-report/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "npm-run-all/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "npm-run-all/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - "npm-run-all/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "npm-run-all/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "foreground-child/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "npm-run-all/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "npm-run-all/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + "wrap-ansi-cjs/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/docs/superpowers/plans/2026-05-25-lockedin-v1.md b/docs/superpowers/plans/2026-05-25-lockedin-v1.md deleted file mode 100644 index c65cc3d..0000000 --- a/docs/superpowers/plans/2026-05-25-lockedin-v1.md +++ /dev/null @@ -1,3809 +0,0 @@ -# Locked In v1 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build the v1 Locked In learning-module app — three modules (G1, JS, TS), three modes (multiple choice, flashcards, recap), Supabase Auth + Postgres + RLS, on a Bun/Vite/React/TS/wouter/zustand/CSS-modules stack. - -**Architecture:** SPA in the browser talking directly to Supabase. Routes via `wouter`. Auth and active-session state in two `zustand` stores. CSS modules for styling. Question content seeded into Postgres via SQL migrations. Per-user progress (`sessions`, `session_answers`) auto-scoped by Row-Level Security so a future dashboard is a pure-read addition. - -**Tech Stack:** Bun (runner), Vite (dev/build), React 18 + TypeScript, wouter, zustand, CSS modules, `@supabase/supabase-js`, `lucide-react`, Supabase (Postgres + Auth + RLS). - -**Note on tests:** The spec explicitly excludes a test suite from v1 (Non-goals §). Each task's verification step is a manual browser / SQL / CLI check instead of an automated assertion. Add tests when the dashboard or non-trivial logic lands. - -**Spec:** `docs/superpowers/specs/2026-05-25-lockedin-design.md` - ---- - -## File Structure - -``` -. -├── .env.local (gitignored; Supabase URL + anon key) -├── .gitignore -├── README.md -├── bun.lockb -├── eslint.config.js -├── index.html -├── package.json -├── tsconfig.json -├── tsconfig.app.json -├── tsconfig.node.json -├── vite.config.ts -├── supabase/ -│ └── migrations/ -│ ├── 001_init.sql schema + RLS policies -│ ├── 002_seed_g1.sql G1 module + 40 questions -│ ├── 003_seed_javascript.sql JS module + 100 questions -│ └── 004_seed_typescript.sql TS module + 100 questions -├── docs/superpowers/ -│ ├── specs/2026-05-25-lockedin-design.md -│ └── plans/2026-05-25-lockedin-v1.md -└── src/ - ├── main.tsx React entry; mounts - ├── App.tsx wouter + route table + auth bootstrap - ├── vite-env.d.ts - ├── styles/ - │ └── global.css CSS variables, resets, body styles - ├── lib/ - │ ├── supabase.ts single Supabase client - │ ├── types.ts Module, Question, QuestionChoice, Session, SessionAnswer, Mode - │ ├── queries.ts fetchModules, fetchModuleBySlug, fetchQuestionsForSession, fetchSessionWithAnswers, createSession, recordAnswer, completeSession, lastScoreByModule - │ └── sampling.ts shuffle(a), sampleN(a, n) - ├── store/ - │ ├── authStore.ts { user, session, status, signIn, signUp, signInWithMagicLink, signOut } - │ └── sessionStore.ts { moduleId, mode, questions, currentIndex, answers, startedAt, sessionId, start, recordAnswer, finish, reset } - ├── components/ - │ ├── RequireAuth.tsx redirects to /login if not signed in - │ ├── Header.tsx top bar with app name + email + sign-out - │ ├── Header.module.css - │ ├── Button.tsx shared button primitive - │ ├── Button.module.css - │ ├── Card.tsx shared card wrapper - │ ├── Card.module.css - │ ├── ProgressBar.tsx - │ └── ProgressBar.module.css - ├── routes/ - │ ├── LoginPage/ - │ │ ├── LoginPage.tsx - │ │ └── LoginPage.module.css - │ ├── HomePage/ - │ │ ├── HomePage.tsx - │ │ └── HomePage.module.css - │ ├── ModulePage/ - │ │ ├── ModulePage.tsx - │ │ └── ModulePage.module.css - │ ├── QuizPage/ - │ │ ├── QuizPage.tsx - │ │ └── QuizPage.module.css - │ └── ResultsPage/ - │ ├── ResultsPage.tsx - │ └── ResultsPage.module.css - └── modes/ - ├── MultipleChoice/ - │ ├── MultipleChoice.tsx - │ └── MultipleChoice.module.css - ├── Flashcards/ - │ ├── Flashcards.tsx - │ └── Flashcards.module.css - └── Recap/ - ├── Recap.tsx - └── Recap.module.css -``` - ---- - -## Task 1: Clear the Bolt scaffold - -**Files:** -- Delete: `src/`, `supabase/`, `package.json`, `package-lock.json`, `node_modules/`, `tailwind.config.js`, `postcss.config.js`, `eslint.config.js`, `tsconfig.json`, `tsconfig.app.json`, `tsconfig.node.json`, `vite.config.ts`, `index.html`, `.bolt/` -- Keep: `.gitignore`, `README.md`, `docs/`, `.git/` - -- [ ] **Step 1: Delete scaffold files and folders** - -Run from repo root: - -```bash -rm -rf src supabase node_modules .bolt -rm -f package.json package-lock.json tailwind.config.js postcss.config.js eslint.config.js tsconfig.json tsconfig.app.json tsconfig.node.json vite.config.ts index.html -``` - -- [ ] **Step 2: Verify only kept files remain** - -Run: `ls -la` -Expected: only `.git/`, `.gitignore`, `README.md`, `docs/` (plus the standard `.` and `..`). - -- [ ] **Step 3: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: clear Bolt scaffold - -- remove `src/`, `supabase/`, `.bolt/`, all root configs, `node_modules/`, `package.json`, `package-lock.json` -- keep `README.md`, `.gitignore`, `docs/` -EOF -)" -``` - ---- - -## Task 2: Bootstrap Bun + Vite + React + TS - -**Files:** -- Create: `package.json`, `tsconfig.json`, `tsconfig.app.json`, `tsconfig.node.json`, `vite.config.ts`, `eslint.config.js`, `index.html`, `src/main.tsx`, `src/App.tsx`, `src/vite-env.d.ts`, `src/styles/global.css` - -- [ ] **Step 1: Create `package.json`** - -```json -{ - "name": "lockedin", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "lint": "eslint .", - "typecheck": "tsc --noEmit -p tsconfig.app.json" - }, - "dependencies": { - "@supabase/supabase-js": "^2.57.4", - "lucide-react": "^0.344.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "wouter": "^3.3.5", - "zustand": "^5.0.0" - }, - "devDependencies": { - "@eslint/js": "^9.9.1", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "eslint": "^9.9.1", - "eslint-plugin-react-hooks": "^5.1.0-rc.0", - "eslint-plugin-react-refresh": "^0.4.11", - "globals": "^15.9.0", - "typescript": "^5.5.3", - "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" - } -} -``` - -- [ ] **Step 2: Create `tsconfig.json`** - -```json -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} -``` - -- [ ] **Step 3: Create `tsconfig.app.json`** - -```json -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] -} -``` - -- [ ] **Step 4: Create `tsconfig.node.json`** - -```json -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["vite.config.ts"] -} -``` - -- [ ] **Step 5: Create `vite.config.ts`** - -```ts -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], -}); -``` - -- [ ] **Step 6: Create `eslint.config.js`** - -```js -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"] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ["**/*.{ts,tsx}"], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], - }, - } -); -``` - -- [ ] **Step 7: Create `index.html`** - -```html - - - - - - Locked In - - -
- - - -``` - -- [ ] **Step 8: Create `src/vite-env.d.ts`** - -```ts -/// - -interface ImportMetaEnv { - readonly VITE_SUPABASE_URL: string; - readonly VITE_SUPABASE_ANON_KEY: string; -} - -interface ImportMeta { - readonly env: ImportMetaEnv; -} -``` - -- [ ] **Step 9: Create `src/styles/global.css`** - -```css -:root { - --color-bg: #f6f6f4; - --color-surface: #ffffff; - --color-text: #1a1a1a; - --color-text-muted: #6b6b6b; - --color-border: #e2e2dd; - --color-accent: #2c5282; - --color-accent-hover: #234168; - --color-correct: #2f855a; - --color-incorrect: #c53030; - - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; - - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 24px; - --space-6: 32px; - --space-7: 48px; - - --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, - "Helvetica Neue", sans-serif; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; -} - -*, -*::before, -*::after { - box-sizing: border-box; -} - -html, -body, -#root { - height: 100%; - margin: 0; -} - -body { - font-family: var(--font-sans); - color: var(--color-text); - background: var(--color-bg); - -webkit-font-smoothing: antialiased; -} - -a { - color: var(--color-accent); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -button { - font: inherit; - cursor: pointer; -} - -code, -pre { - font-family: var(--font-mono); -} -``` - -- [ ] **Step 10: Create `src/main.tsx`** - -```tsx -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import { App } from "./App"; -import "./styles/global.css"; - -createRoot(document.getElementById("root")!).render( - - - -); -``` - -- [ ] **Step 11: Create temporary placeholder `src/App.tsx`** - -(Replaced in Task 9 with the real router.) - -```tsx -export function App() { - return
Locked In — bootstrapping…
; -} -``` - -- [ ] **Step 12: Install dependencies and run dev server** - -Run: `bun install` -Expected: a `bun.lockb` file is created; no errors. - -Run: `bun run dev` -Expected: Vite starts and prints a local URL (e.g. `http://localhost:5173/`). Open it; the page shows "Locked In — bootstrapping…". Then stop the server (Ctrl-C). - -- [ ] **Step 13: Run typecheck** - -Run: `bun run typecheck` -Expected: exit code 0, no errors printed. - -- [ ] **Step 14: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: bootstrap `bun` + `vite` + `react` + `ts` scaffold - -- `package.json` declares `react`, `wouter`, `zustand`, `@supabase/supabase-js`, `lucide-react` -- `vite.config.ts` + `tsconfig.*` for strict TS -- `eslint.config.js` with `react-hooks` + `react-refresh` -- `src/main.tsx` mounts placeholder `App`; `src/styles/global.css` defines theme vars -EOF -)" -``` - ---- - -## Task 3: Stand up a Supabase project and `.env.local` - -This task is one-time setup of the Supabase backend. After this, all schema/data changes are SQL migrations. - -**Files:** -- Create: `.env.local` (gitignored), `src/lib/supabase.ts` -- Modify: `.gitignore` (verify `.env.local` is ignored) - -- [ ] **Step 1: Create a Supabase project** - -Go to , sign in, click "New project". Pick any name (e.g. `lockedin`), set a database password, pick the closest region, wait for provisioning to finish. - -- [ ] **Step 2: Grab the project URL and anon key** - -In the project dashboard → Settings → API: -- Copy the `Project URL` (e.g. `https://xxxx.supabase.co`) -- Copy the `anon` `public` API key (long JWT) - -- [ ] **Step 3: Create `.env.local`** at repo root - -``` -VITE_SUPABASE_URL=https://xxxx.supabase.co -VITE_SUPABASE_ANON_KEY=ey...your-anon-key... -``` - -- [ ] **Step 4: Verify `.env.local` is gitignored** - -Run: `git check-ignore -v .env.local` -Expected: matches a line in `.gitignore` (typically `.env`). If it does NOT match, add `.env.local` to `.gitignore`: - -``` -.env -.env.local -``` - -- [ ] **Step 5: Create `src/lib/supabase.ts`** - -```ts -import { createClient } from "@supabase/supabase-js"; - -const url = import.meta.env.VITE_SUPABASE_URL; -const anonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; - -if (!url || !anonKey) { - throw new Error( - "Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY in environment" - ); -} - -export const supabase = createClient(url, anonKey, { - auth: { - persistSession: true, - autoRefreshToken: true, - storage: window.localStorage, - }, -}); -``` - -- [ ] **Step 6: Sanity-check the client compiles** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 7: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: add Supabase client + `.env.local` gitignore - -- `src/lib/supabase.ts` creates the singleton `supabase` client -- `.env.local` (gitignored) holds `VITE_SUPABASE_URL` + `VITE_SUPABASE_ANON_KEY` -EOF -)" -``` - ---- - -## Task 4: Schema migration `001_init.sql` - -**Files:** -- Create: `supabase/migrations/001_init.sql` - -- [ ] **Step 1: Create the migration** - -```sql --- 001_init.sql --- Locked In v1: tables, indexes, RLS policies - -create extension if not exists "pgcrypto"; - --- ───────────────────────────────────────────────────────────── modules -create table modules ( - id uuid primary key default gen_random_uuid(), - slug text not null unique, - name text not null, - description text not null default '', - type text not null check (type in ('static', 'dynamic')), - total_questions int not null default 0, - created_at timestamptz not null default now() -); - --- ─────────────────────────────────────────────────────────── questions -create table questions ( - id uuid primary key default gen_random_uuid(), - module_id uuid not null references modules(id) on delete cascade, - category text, - prompt text not null, - explanation text, - flashcard_back text not null, - recap_answer text not null, - order_index int not null default 0, - created_at timestamptz not null default now() -); - -create index questions_module_idx on questions(module_id); - --- ──────────────────────────────────────────────────── question_choices -create table question_choices ( - id uuid primary key default gen_random_uuid(), - question_id uuid not null references questions(id) on delete cascade, - label text not null, - is_correct bool not null default false, - order_index int not null default 0 -); - -create index question_choices_question_idx on question_choices(question_id); - --- ──────────────────────────────────────────────────────────── sessions -create table sessions ( - id uuid primary key default gen_random_uuid(), - user_id uuid not null references auth.users(id) on delete cascade, - module_id uuid not null references modules(id), - mode text not null check (mode in ('multiple_choice', 'flashcards', 'recap')), - session_size int not null, - started_at timestamptz not null default now(), - completed_at timestamptz, - score int -); - -create index sessions_user_module_idx on sessions(user_id, module_id); -create index sessions_user_completed_idx on sessions(user_id, completed_at desc); - --- ───────────────────────────────────────────────────── session_answers -create table session_answers ( - id uuid primary key default gen_random_uuid(), - session_id uuid not null references sessions(id) on delete cascade, - question_id uuid not null references questions(id), - choice_id uuid references question_choices(id), - self_grade bool, - is_correct bool not null, - answered_at timestamptz not null default now() -); - -create index session_answers_session_idx on session_answers(session_id); - --- ──────────────────────────────────────────────────────── RLS policies -alter table modules enable row level security; -alter table questions enable row level security; -alter table question_choices enable row level security; -alter table sessions enable row level security; -alter table session_answers enable row level security; - --- Read-only shared content for any authenticated user -create policy "read modules" on modules - for select to authenticated using (true); - -create policy "read questions" on questions - for select to authenticated using (true); - -create policy "read question_choices" on question_choices - for select to authenticated using (true); - --- Sessions: full ownership by the row's user_id -create policy "own sessions select" on sessions - for select to authenticated using (auth.uid() = user_id); - -create policy "own sessions insert" on sessions - for insert to authenticated with check (auth.uid() = user_id); - -create policy "own sessions update" on sessions - for update to authenticated - using (auth.uid() = user_id) - with check (auth.uid() = user_id); - -create policy "own sessions delete" on sessions - for delete to authenticated using (auth.uid() = user_id); - --- session_answers: ownership inferred via parent session -create policy "own session_answers select" on session_answers - for select to authenticated using ( - exists (select 1 from sessions s - where s.id = session_answers.session_id - and s.user_id = auth.uid()) - ); - -create policy "own session_answers insert" on session_answers - for insert to authenticated with check ( - exists (select 1 from sessions s - where s.id = session_answers.session_id - and s.user_id = auth.uid()) - ); - -create policy "own session_answers update" on session_answers - for update to authenticated - using ( - exists (select 1 from sessions s - where s.id = session_answers.session_id - and s.user_id = auth.uid()) - ) - with check ( - exists (select 1 from sessions s - where s.id = session_answers.session_id - and s.user_id = auth.uid()) - ); - -create policy "own session_answers delete" on session_answers - for delete to authenticated using ( - exists (select 1 from sessions s - where s.id = session_answers.session_id - and s.user_id = auth.uid()) - ); -``` - -- [ ] **Step 2: Apply the migration to the hosted Supabase project** - -Go to the Supabase project → SQL Editor → New query. Paste the entire contents of `supabase/migrations/001_init.sql` and click "Run". - -Expected: "Success. No rows returned." - -- [ ] **Step 3: Verify tables exist** - -In the SQL Editor, run: - -```sql -select table_name -from information_schema.tables -where table_schema = 'public' -order by table_name; -``` - -Expected rows: `modules`, `question_choices`, `questions`, `session_answers`, `sessions`. - -- [ ] **Step 4: Verify RLS is enabled** - -```sql -select tablename, rowsecurity -from pg_tables -where schemaname = 'public'; -``` - -Expected: every one of the five tables has `rowsecurity = true`. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: add `001_init.sql` schema + RLS - -- five tables: `modules`, `questions`, `question_choices`, `sessions`, `session_answers` -- shared content readable by any `authenticated` role -- `sessions` / `session_answers` scoped to `auth.uid() = user_id` (direct or via parent) -- indexes on FK + `(user_id, completed_at desc)` for last-score lookups -EOF -)" -``` - ---- - -## Task 5: Seed migration `002_seed_g1.sql` — G1 driver's test (40 questions) - -**Files:** -- Create: `supabase/migrations/002_seed_g1.sql` - -This task is content-heavy. You will author the full 40 G1 questions (20 signs + 20 rules), each as a complete row with: `prompt`, `explanation`, `flashcard_back`, `recap_answer`, and exactly **four** `question_choices` with exactly **one** `is_correct = true`. Source the questions from the official Ontario G1 handbook topics; do not copy text verbatim, paraphrase. - -**Schema reminders:** -- `category` should be `'signs'` for the 20 sign questions and `'rules'` for the 20 rules questions. -- `order_index` for the module's questions starts at 0 and increments. G1 is static so this controls the canonical order before shuffling. -- For "sign" questions, describe the sign in the `prompt` text (e.g. "What does a red octagonal sign mean?") — v1 does not store images. -- `flashcard_back` should be the short, memorable form of the answer (1 short sentence). -- `recap_answer` may be a slightly longer canonical answer (1-2 sentences) that includes the "why". - -**SQL pattern to follow for every question** (using a DO block + variables so each insert is self-contained): - -```sql --- 002_seed_g1.sql --- Locked In: G1 driver's test module + 40 questions - -insert into modules (slug, name, description, type, total_questions) -values ( - 'g1', - 'G1 Driver''s Test', - 'Ontario G1 written knowledge test — signs and rules of the road.', - 'static', - 40 -); - --- Helper: a small SQL function-less pattern is to use CTEs per question. --- Each question is one CTE that returns its id, then we insert the choices. --- For readability we'll inline this as a series of `with` blocks. - --- Question 1 (signs) --------------------------------------------------------- -with q as ( - insert into questions ( - module_id, category, prompt, explanation, - flashcard_back, recap_answer, order_index - ) - select id, 'signs', - 'What does a red octagonal sign mean?', - 'A red octagon always means STOP — full stop required.', - 'Stop', - 'A red octagonal sign means "Stop" — come to a complete stop at the line, then proceed when safe.', - 0 - from modules where slug = 'g1' - returning id -) -insert into question_choices (question_id, label, is_correct, order_index) -select q.id, c.label, c.is_correct, c.order_index -from q, - (values - ('Stop completely', true, 0), - ('Yield to oncoming traffic', false, 1), - ('Slow down and proceed with caution', false, 2), - ('Do not enter', false, 3) - ) as c(label, is_correct, order_index); - --- Question 2 (signs) --------------------------------------------------------- --- (repeat pattern with order_index = 1, etc.) - --- ... continue for all 40 questions (20 signs, then 20 rules) ... -``` - -- [ ] **Step 1: Author the migration with all 40 questions** - -Create `supabase/migrations/002_seed_g1.sql` containing: -- One `insert into modules` row for slug `g1`, `type = 'static'`, `total_questions = 40`. -- 20 `signs`-category questions (`order_index` 0-19), each with 4 choices and 1 correct. -- 20 `rules`-category questions (`order_index` 20-39), each with 4 choices and 1 correct. - -Sign topics to cover (one per question, in order): stop, yield, do-not-enter, no-U-turn, no-left-turn, no-right-turn, school-zone, pedestrian-crossover, railway-crossing, construction, slippery-when-wet, deer-crossing, divided-highway-begins, divided-highway-ends, lane-ends, two-way-traffic-ahead, traffic-light-ahead, hospital, no-stopping vs no-parking distinction, hidden-intersection. - -Rules topics to cover (one per question, in order): max speed on most city streets when otherwise unposted, max speed on most highways when otherwise unposted, blood-alcohol limit for fully licensed driver, G1 demerit suspension threshold, G1 alcohol rule (zero), G1 passenger restriction (front-seat licensed driver), G1 no-driving hours (midnight-5am), G1 no-400-series rule, when to use low beams vs high beams, headlight requirement timing, following distance (2-second rule), use of cell phone while driving, seatbelt obligation, child car-seat rule, right-of-way at uncontrolled intersection, right-of-way at four-way stop arriving at same time, passing a school bus with flashing red lights, passing emergency vehicle on shoulder, what to do if approaching a yellow light, what to do if you miss your exit on a highway. - -- [ ] **Step 2: Apply the migration** - -In the Supabase SQL Editor, paste the entire `002_seed_g1.sql` and run. -Expected: "Success. No rows returned." - -- [ ] **Step 3: Verify counts** - -```sql -select m.slug, - count(distinct q.id) as question_count, - count(c.id) as choice_count, - sum(case when c.is_correct then 1 else 0 end) as correct_count -from modules m -left join questions q on q.module_id = m.id -left join question_choices c on c.question_id = q.id -where m.slug = 'g1' -group by m.slug; -``` - -Expected: `question_count = 40`, `choice_count = 160`, `correct_count = 40`. - -- [ ] **Step 4: Spot-check one question** - -```sql -select q.order_index, q.category, q.prompt, c.label, c.is_correct -from questions q -join question_choices c on c.question_id = q.id -join modules m on m.id = q.module_id -where m.slug = 'g1' and q.order_index = 0 -order by c.order_index; -``` - -Expected: four rows for the question, exactly one with `is_correct = true`. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: seed G1 module with 40 questions - -- `002_seed_g1.sql` inserts module `g1` (static, 40) -- 20 `signs` + 20 `rules` questions, each with 4 choices and 1 correct -- every question has `flashcard_back` and `recap_answer` set -EOF -)" -``` - ---- - -## Task 6: Seed migration `003_seed_javascript.sql` — JavaScript (100 questions) - -**Files:** -- Create: `supabase/migrations/003_seed_javascript.sql` - -Same shape as Task 5 but with 100 dynamic questions. `module.type = 'dynamic'`, `total_questions = 100`. No `category` split required (leave `null` or use topic groupings if you want). - -Topics to cover (target ~5-10 questions per area, total = 100): -- primitives & coercion (`==` vs `===`, `null` vs `undefined`, `typeof` quirks) -- scope, hoisting, `var`/`let`/`const` -- closures -- `this` binding rules (4 rules: default, implicit, explicit, `new`) -- arrow functions vs regular functions -- arrays (map/filter/reduce/forEach/find/some/every) -- objects (destructuring, spread, `Object.keys/values/entries`) -- prototypes & `class` syntax -- promises & `async`/`await` -- error handling (`try`/`catch`/`finally`, `throw`) -- the event loop / microtasks vs macrotasks (4-6 questions) -- modules (`import`/`export`, default vs named) -- DOM essentials (`querySelector`, event listeners, `event.target` vs `currentTarget`) -- `JSON.parse` / `JSON.stringify` gotchas -- `Map` / `Set` vs object / array -- regex basics (literal vs constructor, common flags) -- common pitfalls (`for…in` vs `for…of`, mutating during iteration) - -**Question shape:** same CTE pattern as G1 — each question has prompt + 4 choices + explanation + flashcard_back + recap_answer + order_index (0-99). - -- [ ] **Step 1: Author the migration with all 100 questions** - -Create `supabase/migrations/003_seed_javascript.sql`. Insert the module row first: - -```sql -insert into modules (slug, name, description, type, total_questions) -values ( - 'javascript', - 'JavaScript Test', - 'Brush up on JavaScript fundamentals — types, scope, async, and gotchas.', - 'dynamic', - 100 -); -``` - -Then 100 question CTE blocks following the same pattern as Task 5. - -- [ ] **Step 2: Apply the migration** - -In SQL Editor, run the file. -Expected: "Success. No rows returned." - -- [ ] **Step 3: Verify counts** - -```sql -select count(*) from questions q -join modules m on m.id = q.module_id -where m.slug = 'javascript'; -``` - -Expected: `100`. - -```sql -select count(*) from question_choices c -join questions q on q.id = c.question_id -join modules m on m.id = q.module_id -where m.slug = 'javascript'; -``` - -Expected: `400`. - -- [ ] **Step 4: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: seed JavaScript module with 100 questions - -- `003_seed_javascript.sql` inserts module `javascript` (dynamic, 100) -- covers types, scope, closures, `this`, arrays, objects, promises, event loop, modules, DOM -- every question has 4 choices, 1 correct, `flashcard_back`, `recap_answer` -EOF -)" -``` - ---- - -## Task 7: Seed migration `004_seed_typescript.sql` — TypeScript (100 questions) - -**Files:** -- Create: `supabase/migrations/004_seed_typescript.sql` - -Same shape as Task 6. `module.type = 'dynamic'`, `total_questions = 100`. Questions are TypeScript-specific (assume JS fluency). - -Topics to cover (target ~5-10 questions per area, total = 100): -- basic type annotations (`string`, `number`, `boolean`, `null`, `undefined`, arrays, tuples) -- `unknown` vs `any` vs `never` (and when to use each) -- interfaces vs type aliases (when they're interchangeable, when they're not) -- union and intersection types -- discriminated unions (tagged unions) and exhaustiveness via `never` -- literal types and `as const` -- function type expressions, call signatures, overloads -- generics (type parameters, constraints, default type params) -- `keyof`, `typeof`, indexed access types (`T[K]`) -- mapped types (`Partial`, `Required`, `Readonly`, custom) -- conditional types and `infer` -- utility types (`Pick`, `Omit`, `Record`, `Exclude`, `Extract`, `ReturnType`, `Parameters`) -- narrowing (`typeof`, `instanceof`, `in`, custom type guards via `is`) -- enums (numeric vs string, `const enum`) -- classes with TS (access modifiers, `readonly`, parameter properties) -- module / namespace basics, `import type` -- `tsconfig` essentials (`strict`, `noImplicitAny`, `strictNullChecks`, `moduleResolution`) -- declaration files (`.d.ts`) at a conceptual level -- React + TS basics (`FC` vs explicit props, `useState`, event handler types) - -**Question shape:** identical CTE pattern. `order_index` 0-99. - -- [ ] **Step 1: Author the migration with all 100 questions** - -Create `supabase/migrations/004_seed_typescript.sql`. Start with: - -```sql -insert into modules (slug, name, description, type, total_questions) -values ( - 'typescript', - 'TypeScript Test', - 'Sharpen TypeScript — types, generics, narrowing, and tsconfig.', - 'dynamic', - 100 -); -``` - -Then 100 question CTE blocks. - -- [ ] **Step 2: Apply the migration** - -In SQL Editor, run the file. -Expected: "Success. No rows returned." - -- [ ] **Step 3: Verify counts** - -```sql -select m.slug, count(q.id) -from modules m -left join questions q on q.module_id = m.id -group by m.slug -order by m.slug; -``` - -Expected: -- `g1`: 40 -- `javascript`: 100 -- `typescript`: 100 - -- [ ] **Step 4: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: seed TypeScript module with 100 questions - -- `004_seed_typescript.sql` inserts module `typescript` (dynamic, 100) -- covers annotations, `unknown`/`any`/`never`, unions, generics, `keyof`/`typeof`, narrowing, utility types -- every question has 4 choices, 1 correct, `flashcard_back`, `recap_answer` -EOF -)" -``` - ---- - -## Task 8: Shared types and sampling helpers - -**Files:** -- Create: `src/lib/types.ts`, `src/lib/sampling.ts` - -- [ ] **Step 1: Create `src/lib/types.ts`** - -```ts -export type ModuleType = "static" | "dynamic"; - -export type Mode = "multiple_choice" | "flashcards" | "recap"; - -export interface Module { - id: string; - slug: string; - name: string; - description: string; - type: ModuleType; - total_questions: number; - created_at: string; -} - -export interface QuestionChoice { - id: string; - question_id: string; - label: string; - is_correct: boolean; - order_index: number; -} - -export interface Question { - id: string; - module_id: string; - category: string | null; - prompt: string; - explanation: string | null; - flashcard_back: string; - recap_answer: string; - order_index: number; - created_at: string; - choices: QuestionChoice[]; -} - -export interface Session { - id: string; - user_id: string; - module_id: string; - mode: Mode; - session_size: number; - started_at: string; - completed_at: string | null; - score: number | null; -} - -export interface SessionAnswer { - id: string; - session_id: string; - question_id: string; - choice_id: string | null; - self_grade: boolean | null; - is_correct: boolean; - answered_at: string; -} -``` - -- [ ] **Step 2: Create `src/lib/sampling.ts`** - -```ts -export function shuffle(items: readonly T[]): T[] { - const out = items.slice(); - for (let i = out.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [out[i], out[j]] = [out[j], out[i]]; - } - return out; -} - -export function sampleN(items: readonly T[], n: number): T[] { - if (n >= items.length) return shuffle(items); - return shuffle(items).slice(0, n); -} -``` - -- [ ] **Step 3: Typecheck** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 4: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: add shared `types.ts` and `sampling.ts` - -- `Module`, `Question`, `QuestionChoice`, `Session`, `SessionAnswer`, `Mode` -- `shuffle`, `sampleN` helpers (Fisher-Yates) -EOF -)" -``` - ---- - -## Task 9: Data-access helpers (`queries.ts`) - -**Files:** -- Create: `src/lib/queries.ts` - -- [ ] **Step 1: Create `src/lib/queries.ts`** - -```ts -import { supabase } from "./supabase"; -import type { - Module, - Question, - Session, - SessionAnswer, - Mode, -} from "./types"; - -export async function fetchModules(): Promise { - const { data, error } = await supabase - .from("modules") - .select("*") - .order("created_at", { ascending: true }); - if (error) throw error; - return data ?? []; -} - -export async function fetchModuleBySlug(slug: string): Promise { - const { data, error } = await supabase - .from("modules") - .select("*") - .eq("slug", slug) - .maybeSingle(); - if (error) throw error; - return data; -} - -export async function fetchQuestionsForModule( - moduleId: string -): Promise { - const { data, error } = await supabase - .from("questions") - .select("*, choices:question_choices(*)") - .eq("module_id", moduleId) - .order("order_index", { ascending: true }); - if (error) throw error; - return (data ?? []).map((q) => ({ - ...q, - choices: (q.choices ?? []).sort( - (a: { order_index: number }, b: { order_index: number }) => - a.order_index - b.order_index - ), - })) as Question[]; -} - -export async function createSession(args: { - moduleId: string; - mode: Mode; - sessionSize: number; -}): Promise { - const { data: auth } = await supabase.auth.getUser(); - if (!auth.user) throw new Error("Not signed in"); - const { data, error } = await supabase - .from("sessions") - .insert({ - user_id: auth.user.id, - module_id: args.moduleId, - mode: args.mode, - session_size: args.sessionSize, - }) - .select("*") - .single(); - if (error) throw error; - return data as Session; -} - -export async function recordAnswer(args: { - sessionId: string; - questionId: string; - choiceId: string | null; - selfGrade: boolean | null; - isCorrect: boolean; -}): Promise { - const { data, error } = await supabase - .from("session_answers") - .insert({ - session_id: args.sessionId, - question_id: args.questionId, - choice_id: args.choiceId, - self_grade: args.selfGrade, - is_correct: args.isCorrect, - }) - .select("*") - .single(); - if (error) throw error; - return data as SessionAnswer; -} - -export async function completeSession(args: { - sessionId: string; - score: number; -}): Promise { - const { error } = await supabase - .from("sessions") - .update({ - completed_at: new Date().toISOString(), - score: args.score, - }) - .eq("id", args.sessionId); - if (error) throw error; -} - -export interface SessionWithAnswers { - session: Session; - answers: SessionAnswer[]; - questions: Question[]; -} - -export async function fetchSessionWithAnswers( - sessionId: string -): Promise { - const { data: session, error: sErr } = await supabase - .from("sessions") - .select("*") - .eq("id", sessionId) - .single(); - if (sErr) throw sErr; - - const { data: answers, error: aErr } = await supabase - .from("session_answers") - .select("*") - .eq("session_id", sessionId) - .order("answered_at", { ascending: true }); - if (aErr) throw aErr; - - const questionIds = (answers ?? []).map((a) => a.question_id); - let questions: Question[] = []; - if (questionIds.length > 0) { - const { data: qs, error: qErr } = await supabase - .from("questions") - .select("*, choices:question_choices(*)") - .in("id", questionIds); - if (qErr) throw qErr; - questions = (qs ?? []).map((q) => ({ - ...q, - choices: (q.choices ?? []).sort( - (a: { order_index: number }, b: { order_index: number }) => - a.order_index - b.order_index - ), - })) as Question[]; - } - - return { - session: session as Session, - answers: (answers ?? []) as SessionAnswer[], - questions, - }; -} - -export interface LastScore { - moduleId: string; - score: number; - sessionSize: number; - completedAt: string; -} - -export async function fetchLastScoreByModule(): Promise< - Record -> { - const { data, error } = await supabase - .from("sessions") - .select("module_id, score, session_size, completed_at") - .not("completed_at", "is", null) - .order("completed_at", { ascending: false }); - if (error) throw error; - const byModule: Record = {}; - for (const row of data ?? []) { - if (byModule[row.module_id]) continue; - byModule[row.module_id] = { - moduleId: row.module_id, - score: row.score ?? 0, - sessionSize: row.session_size, - completedAt: row.completed_at, - }; - } - return byModule; -} -``` - -- [ ] **Step 2: Typecheck** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 3: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: add `queries.ts` data-access layer - -- `fetchModules`, `fetchModuleBySlug`, `fetchQuestionsForModule` -- `createSession`, `recordAnswer`, `completeSession` -- `fetchSessionWithAnswers`, `fetchLastScoreByModule` -EOF -)" -``` - ---- - -## Task 10: Auth store - -**Files:** -- Create: `src/store/authStore.ts` - -- [ ] **Step 1: Create the store** - -```ts -import { create } from "zustand"; -import type { Session, User } from "@supabase/supabase-js"; -import { supabase } from "../lib/supabase"; - -type AuthStatus = "loading" | "authenticated" | "unauthenticated"; - -interface AuthState { - user: User | null; - session: Session | null; - status: AuthStatus; - signIn: (email: string, password: string) => Promise; - signUp: (email: string, password: string) => Promise; - signInWithMagicLink: (email: string) => Promise; - signOut: () => Promise; - bootstrap: () => Promise; -} - -export const useAuthStore = create((set) => ({ - user: null, - session: null, - status: "loading", - - bootstrap: async () => { - const { data } = await supabase.auth.getSession(); - set({ - session: data.session, - user: data.session?.user ?? null, - status: data.session ? "authenticated" : "unauthenticated", - }); - supabase.auth.onAuthStateChange((_event, session) => { - set({ - session, - user: session?.user ?? null, - status: session ? "authenticated" : "unauthenticated", - }); - }); - }, - - signIn: async (email, password) => { - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - if (error) throw error; - }, - - signUp: async (email, password) => { - const { error } = await supabase.auth.signUp({ email, password }); - if (error) throw error; - }, - - signInWithMagicLink: async (email) => { - const { error } = await supabase.auth.signInWithOtp({ - email, - options: { emailRedirectTo: window.location.origin }, - }); - if (error) throw error; - }, - - signOut: async () => { - const { error } = await supabase.auth.signOut(); - if (error) throw error; - }, -})); -``` - -- [ ] **Step 2: Typecheck** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 3: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: add `authStore` zustand store - -- `bootstrap` hydrates from `supabase.auth.getSession()` and subscribes to `onAuthStateChange` -- `signIn` / `signUp` / `signInWithMagicLink` / `signOut` -- exposes `status: 'loading' | 'authenticated' | 'unauthenticated'` -EOF -)" -``` - ---- - -## Task 11: Session store - -**Files:** -- Create: `src/store/sessionStore.ts` - -- [ ] **Step 1: Create the store** - -```ts -import { create } from "zustand"; -import type { Mode, Question } from "../lib/types"; - -export interface RecordedAnswer { - questionId: string; - choiceId: string | null; - selfGrade: boolean | null; - isCorrect: boolean; -} - -interface SessionState { - sessionId: string | null; - moduleId: string | null; - mode: Mode | null; - questions: Question[]; - currentIndex: number; - answers: RecordedAnswer[]; - startedAt: number | null; - - start: (args: { - sessionId: string; - moduleId: string; - mode: Mode; - questions: Question[]; - }) => void; - - recordAnswer: (answer: RecordedAnswer) => void; - advance: () => void; - reset: () => void; -} - -export const useSessionStore = create((set) => ({ - sessionId: null, - moduleId: null, - mode: null, - questions: [], - currentIndex: 0, - answers: [], - startedAt: null, - - start: ({ sessionId, moduleId, mode, questions }) => - set({ - sessionId, - moduleId, - mode, - questions, - currentIndex: 0, - answers: [], - startedAt: Date.now(), - }), - - recordAnswer: (answer) => - set((s) => ({ answers: [...s.answers, answer] })), - - advance: () => - set((s) => ({ currentIndex: Math.min(s.currentIndex + 1, s.questions.length) })), - - reset: () => - set({ - sessionId: null, - moduleId: null, - mode: null, - questions: [], - currentIndex: 0, - answers: [], - startedAt: null, - }), -})); -``` - -- [ ] **Step 2: Typecheck** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 3: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: add `sessionStore` zustand store - -- holds active session: `questions`, `currentIndex`, `answers`, `startedAt` -- `start` initializes; `recordAnswer` + `advance` for per-question flow; `reset` clears -EOF -)" -``` - ---- - -## Task 12: Shared `Button`, `Card`, `ProgressBar` primitives - -**Files:** -- Create: `src/components/Button.tsx`, `src/components/Button.module.css`, - `src/components/Card.tsx`, `src/components/Card.module.css`, - `src/components/ProgressBar.tsx`, `src/components/ProgressBar.module.css` - -- [ ] **Step 1: Create `src/components/Button.tsx`** - -```tsx -import type { ButtonHTMLAttributes } from "react"; -import styles from "./Button.module.css"; - -type Variant = "primary" | "secondary" | "ghost" | "danger"; - -interface Props extends ButtonHTMLAttributes { - variant?: Variant; - block?: boolean; -} - -export function Button({ - variant = "primary", - block = false, - className, - ...rest -}: Props) { - const classes = [ - styles.button, - styles[variant], - block ? styles.block : "", - className ?? "", - ] - .filter(Boolean) - .join(" "); - return - - ) : null} - - ); -} -``` - -- [ ] **Step 3: Create `src/components/Header.module.css`** - -```css -.header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-4) var(--space-5); - background: var(--color-surface); - border-bottom: 1px solid var(--color-border); -} - -.brand { - display: inline-flex; - align-items: center; - gap: var(--space-2); - color: var(--color-text); - font-weight: 700; -} - -.brand:hover { - text-decoration: none; - color: var(--color-accent); -} - -.right { - display: inline-flex; - align-items: center; - gap: var(--space-3); -} - -.email { - color: var(--color-text-muted); - font-size: 14px; -} -``` - -- [ ] **Step 4: Typecheck** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: add `RequireAuth` gate and `Header` - -- `RequireAuth` redirects to `/login` when `authStore.status` is `unauthenticated` -- `Header` shows brand, current user email, sign-out -EOF -)" -``` - ---- - -## Task 14: Wire `App.tsx` router with all routes (pages still placeholders) - -**Files:** -- Modify: `src/App.tsx` - -- [ ] **Step 1: Replace `src/App.tsx`** - -```tsx -import { useEffect } from "react"; -import { Route, Switch } from "wouter"; -import { useAuthStore } from "./store/authStore"; -import { RequireAuth } from "./components/RequireAuth"; -import { Header } from "./components/Header"; - -function Placeholder({ name }: { name: string }) { - return
{name} (todo)
; -} - -export function App() { - const bootstrap = useAuthStore((s) => s.bootstrap); - const status = useAuthStore((s) => s.status); - - useEffect(() => { - void bootstrap(); - }, [bootstrap]); - - if (status === "loading") { - return ( -
- Loading… -
- ); - } - - return ( - <> -
-
- - - - - - - - - - - {(params) => ( - - - - )} - - - {(params) => ( - - - - )} - - - {(params) => ( - - - - )} - - -
Not found
-
-
-
- - ); -} -``` - -- [ ] **Step 2: Run dev server and confirm routes resolve** - -Run: `bun run dev` -- Open `/` → redirected to `/login` (shows "LoginPage (todo)") -- Open `/m/javascript` → redirected to `/login` -- Open `/login` directly → "LoginPage (todo)" visible - -Stop the server (Ctrl-C). - -- [ ] **Step 3: Typecheck** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 4: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: wire `App.tsx` router with `wouter` - -- `bootstrap` hydrates `authStore` on mount; renders loading shell while pending -- routes: `/login`, `/`, `/m/:slug`, `/m/:slug/quiz`, `/m/:slug/results/:id`, fallback -- all non-`/login` routes wrapped in `RequireAuth` -EOF -)" -``` - ---- - -## Task 15: `LoginPage` - -**Files:** -- Create: `src/routes/LoginPage/LoginPage.tsx`, `src/routes/LoginPage/LoginPage.module.css` -- Modify: `src/App.tsx` (swap placeholder for real page) - -- [ ] **Step 1: Create `src/routes/LoginPage/LoginPage.tsx`** - -```tsx -import { useEffect, useState } from "react"; -import { useLocation } from "wouter"; -import { useAuthStore } from "../../store/authStore"; -import { Button } from "../../components/Button"; -import { Card } from "../../components/Card"; -import styles from "./LoginPage.module.css"; - -type Tab = "signin" | "signup"; - -export function LoginPage() { - const status = useAuthStore((s) => s.status); - const signIn = useAuthStore((s) => s.signIn); - const signUp = useAuthStore((s) => s.signUp); - const signInWithMagicLink = useAuthStore((s) => s.signInWithMagicLink); - const [, navigate] = useLocation(); - - const [tab, setTab] = useState("signin"); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const [info, setInfo] = useState(null); - - useEffect(() => { - if (status === "authenticated") navigate("/", { replace: true }); - }, [status, navigate]); - - const submit = async (e: React.FormEvent) => { - e.preventDefault(); - setBusy(true); - setError(null); - setInfo(null); - try { - if (tab === "signin") await signIn(email, password); - else { - await signUp(email, password); - setInfo( - "Account created. Check your email if confirmation is required, otherwise you're signed in." - ); - } - } catch (e) { - setError(e instanceof Error ? e.message : "Something went wrong"); - } finally { - setBusy(false); - } - }; - - const magicLink = async () => { - setBusy(true); - setError(null); - setInfo(null); - try { - await signInWithMagicLink(email); - setInfo("Magic link sent. Check your email."); - } catch (e) { - setError(e instanceof Error ? e.message : "Something went wrong"); - } finally { - setBusy(false); - } - }; - - return ( -
- -

Locked In

-

- Brush up before the test. Sign in to track your progress. -

- -
- - -
- -
- - - - {error ?
{error}
: null} - {info ?
{info}
: null} - - -
- -
or
- - -
-
- ); -} -``` - -- [ ] **Step 2: Create `src/routes/LoginPage/LoginPage.module.css`** - -```css -.page { - min-height: calc(100vh - 64px); - display: grid; - place-items: center; - padding: var(--space-5); -} - -.card { - width: 100%; - max-width: 380px; - display: flex; - flex-direction: column; - gap: var(--space-4); -} - -.title { - margin: 0; - font-size: 28px; -} - -.subtitle { - margin: 0; - color: var(--color-text-muted); -} - -.tabs { - display: flex; - gap: var(--space-2); - border-bottom: 1px solid var(--color-border); -} - -.tab, -.activeTab { - flex: 1; - background: none; - border: none; - padding: var(--space-3); - color: var(--color-text-muted); - font-weight: 600; - border-bottom: 2px solid transparent; - margin-bottom: -1px; -} - -.activeTab { - color: var(--color-text); - border-bottom-color: var(--color-accent); -} - -.form { - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.label { - display: flex; - flex-direction: column; - gap: var(--space-1); - font-size: 14px; - color: var(--color-text-muted); -} - -.input { - font: inherit; - padding: var(--space-3); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - background: white; - color: var(--color-text); -} - -.input:focus { - outline: 2px solid var(--color-accent); - outline-offset: 1px; -} - -.error { - background: #fef2f2; - color: var(--color-incorrect); - padding: var(--space-3); - border-radius: var(--radius-md); - font-size: 14px; -} - -.info { - background: #ecfdf5; - color: var(--color-correct); - padding: var(--space-3); - border-radius: var(--radius-md); - font-size: 14px; -} - -.divider { - text-align: center; - color: var(--color-text-muted); - font-size: 12px; -} -``` - -- [ ] **Step 3: Swap placeholder in `App.tsx`** - -In `src/App.tsx`, replace `` with `` and add the import: - -```tsx -import { LoginPage } from "./routes/LoginPage/LoginPage"; -``` - -- [ ] **Step 4: Manual test** - -Run: `bun run dev` -- Visit `/login`. The card renders with Sign in / Sign up tabs, email/password fields, and a magic-link button. -- Click "Sign up", enter a real email + a 6+ char password, click "Create account". -- If Supabase project has email confirmation disabled, you should land on `/` (placeholder). If confirmation is enabled, check your inbox. - -Stop the dev server. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: implement `LoginPage` - -- email/password sign-in + sign-up tabs -- magic-link button via `supabase.auth.signInWithOtp` -- redirects to `/` once `authStore.status === 'authenticated'` -EOF -)" -``` - ---- - -## Task 16: `HomePage` with module cards - -**Files:** -- Create: `src/routes/HomePage/HomePage.tsx`, `src/routes/HomePage/HomePage.module.css` -- Modify: `src/App.tsx` (swap placeholder) - -- [ ] **Step 1: Create `src/routes/HomePage/HomePage.tsx`** - -```tsx -import { useEffect, useState } from "react"; -import { Link } from "wouter"; -import { - fetchModules, - fetchLastScoreByModule, - type LastScore, -} from "../../lib/queries"; -import type { Module } from "../../lib/types"; -import { Card } from "../../components/Card"; -import styles from "./HomePage.module.css"; - -export function HomePage() { - const [modules, setModules] = useState([]); - const [lastScores, setLastScores] = useState>({}); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - let alive = true; - (async () => { - try { - const [ms, ls] = await Promise.all([ - fetchModules(), - fetchLastScoreByModule(), - ]); - if (!alive) return; - setModules(ms); - setLastScores(ls); - } catch (e) { - if (!alive) return; - setError(e instanceof Error ? e.message : "Failed to load"); - } finally { - if (alive) setLoading(false); - } - })(); - return () => { - alive = false; - }; - }, []); - - if (loading) return
Loading…
; - if (error) return
Error: {error}
; - - return ( -
-

What are we locking in today?

-
- {modules.map((m) => { - const last = lastScores[m.id]; - return ( - - -
- - {m.type} - - - {m.total_questions} questions - -
-

{m.name}

-

{m.description}

- {last ? ( -
- Last score: {last.score} / {last.sessionSize} -
- ) : ( -
Not yet attempted
- )} -
- - ); - })} -
-
- ); -} -``` - -- [ ] **Step 2: Create `src/routes/HomePage/HomePage.module.css`** - -```css -.page { - max-width: 980px; - margin: 0 auto; - padding: var(--space-6) var(--space-5); -} - -.heading { - margin: 0 0 var(--space-5); - font-size: 24px; -} - -.grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: var(--space-4); -} - -.state { - padding: var(--space-6); - text-align: center; - color: var(--color-text-muted); -} - -.cardLink { - color: inherit; -} - -.cardLink:hover { - text-decoration: none; -} - -.card { - display: flex; - flex-direction: column; - gap: var(--space-2); - transition: transform 0.12s ease, box-shadow 0.12s ease; -} - -.cardLink:hover .card { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); -} - -.badgeRow { - display: flex; - align-items: center; - justify-content: space-between; -} - -.badge { - font-size: 11px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - padding: 2px 8px; - border-radius: 999px; - background: var(--color-border); - color: var(--color-text-muted); -} - -.badge[data-type="static"] { - background: #e0e7ff; - color: #3730a3; -} - -.badge[data-type="dynamic"] { - background: #d1fae5; - color: #065f46; -} - -.count { - color: var(--color-text-muted); - font-size: 13px; -} - -.name { - margin: var(--space-1) 0 0; - font-size: 18px; -} - -.desc { - margin: 0; - color: var(--color-text-muted); - font-size: 14px; - line-height: 1.4; -} - -.last { - margin-top: var(--space-3); - font-size: 13px; - color: var(--color-text-muted); -} -``` - -- [ ] **Step 3: Swap placeholder in `App.tsx`** - -```tsx -import { HomePage } from "./routes/HomePage/HomePage"; -``` - -Replace `` with ``. - -- [ ] **Step 4: Manual test** - -Run: `bun run dev` -- Sign in. -- Land on `/`. You see three cards: G1 (static, 40), JavaScript (dynamic, 100), TypeScript (dynamic, 100). Each shows "Not yet attempted". -- Hovering a card lifts it. -- Clicking a card navigates to `/m/` (placeholder for now). - -Stop the dev server. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: implement `HomePage` with module cards - -- lists `modules` from Supabase, grouped via auto-fill grid -- each card shows type badge, question count, description, last score -- card link → `/m/:slug` -EOF -)" -``` - ---- - -## Task 17: `ModulePage` — pick mode + size, start session - -**Files:** -- Create: `src/routes/ModulePage/ModulePage.tsx`, `src/routes/ModulePage/ModulePage.module.css` -- Modify: `src/App.tsx` (swap placeholder) - -- [ ] **Step 1: Create `src/routes/ModulePage/ModulePage.tsx`** - -```tsx -import { useEffect, useMemo, useState } from "react"; -import { useLocation, Link } from "wouter"; -import { - fetchModuleBySlug, - fetchQuestionsForModule, - createSession, -} from "../../lib/queries"; -import type { Mode, Module } from "../../lib/types"; -import { sampleN, shuffle } from "../../lib/sampling"; -import { useSessionStore } from "../../store/sessionStore"; -import { Card } from "../../components/Card"; -import { Button } from "../../components/Button"; -import styles from "./ModulePage.module.css"; - -const MODES: Array<{ value: Mode; label: string; help: string }> = [ - { - value: "multiple_choice", - label: "Multiple Choice", - help: "Pick from four options. Instant feedback.", - }, - { - value: "flashcards", - label: "Flashcards", - help: "Front prompts, flip to reveal, self-grade.", - }, - { - value: "recap", - label: "Recap", - help: "Think your answer, reveal the canonical one, self-grade.", - }, -]; - -const SIZE_OPTIONS = [10, 20, 50] as const; - -interface Params { - slug: string; -} - -export function ModulePage({ params }: { params: Params }) { - const [module, setModule] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [mode, setMode] = useState("multiple_choice"); - const [size, setSize] = useState("all"); - const [starting, setStarting] = useState(false); - const start = useSessionStore((s) => s.start); - const [, navigate] = useLocation(); - - useEffect(() => { - let alive = true; - (async () => { - try { - const m = await fetchModuleBySlug(params.slug); - if (!alive) return; - setModule(m); - if (m?.type === "dynamic") setSize(20); - } catch (e) { - if (!alive) return; - setError(e instanceof Error ? e.message : "Failed to load"); - } finally { - if (alive) setLoading(false); - } - })(); - return () => { - alive = false; - }; - }, [params.slug]); - - const sizeChoices = useMemo(() => { - if (!module) return [] as Array; - if (module.type === "static") return ["all"]; - return [...SIZE_OPTIONS, "all"]; - }, [module]); - - const handleStart = async () => { - if (!module) return; - setStarting(true); - try { - const all = await fetchQuestionsForModule(module.id); - const effectiveSize = - module.type === "static" || size === "all" ? all.length : size; - const picked = - module.type === "static" ? shuffle(all) : sampleN(all, effectiveSize); - const session = await createSession({ - moduleId: module.id, - mode, - sessionSize: picked.length, - }); - start({ - sessionId: session.id, - moduleId: module.id, - mode, - questions: picked, - }); - navigate(`/m/${module.slug}/quiz`); - } catch (e) { - setError(e instanceof Error ? e.message : "Failed to start"); - setStarting(false); - } - }; - - if (loading) return
Loading…
; - if (error) return
Error: {error}
; - if (!module) return
Module not found.
; - - return ( -
- - ← Back - -

{module.name}

-

{module.description}

- - -

Mode

-
- {MODES.map((m) => ( - - ))} -
-
- - -

Session size

- {module.type === "static" ? ( -

- This is a static module — you'll see all {module.total_questions}{" "} - questions, order shuffled. -

- ) : ( -
- {sizeChoices.map((n) => { - const value = n === "all" ? module.total_questions : n; - const active = size === n; - return ( - - ); - })} -
- )} -
- - -
- ); -} -``` - -- [ ] **Step 2: Create `src/routes/ModulePage/ModulePage.module.css`** - -```css -.page { - max-width: 720px; - margin: 0 auto; - padding: var(--space-6) var(--space-5); - display: flex; - flex-direction: column; - gap: var(--space-4); -} - -.state { - padding: var(--space-6); - text-align: center; - color: var(--color-text-muted); -} - -.back { - color: var(--color-text-muted); - font-size: 14px; -} - -.heading { - margin: 0; - font-size: 28px; -} - -.subtitle { - margin: 0; - color: var(--color-text-muted); -} - -.section { - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.sectionTitle { - margin: 0; - font-size: 14px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-muted); -} - -.modeGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: var(--space-3); -} - -.mode, -.modeActive { - text-align: left; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - padding: var(--space-3); - display: flex; - flex-direction: column; - gap: var(--space-1); - transition: border-color 0.12s ease, background 0.12s ease; -} - -.modeActive { - border-color: var(--color-accent); - background: #ebf2fa; -} - -.modeLabel { - font-weight: 700; -} - -.modeHelp { - font-size: 13px; - color: var(--color-text-muted); -} - -.sizeRow { - display: flex; - flex-wrap: wrap; - gap: var(--space-2); -} - -.size, -.sizeActive { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - padding: var(--space-2) var(--space-4); - font-weight: 600; -} - -.sizeActive { - border-color: var(--color-accent); - background: #ebf2fa; - color: var(--color-accent); -} - -.staticNote { - margin: 0; - color: var(--color-text-muted); - font-size: 14px; -} -``` - -- [ ] **Step 3: Wire `ModulePage` into `App.tsx`** - -```tsx -import { ModulePage } from "./routes/ModulePage/ModulePage"; -``` - -Replace the `/m/:slug` route's placeholder render with: - -```tsx - - {(params) => ( - - - - )} - -``` - -- [ ] **Step 4: Manual test** - -Run: `bun run dev` -- From `/`, click G1 card → see "Static — all 40 questions" note, no size buttons. -- From `/`, click JavaScript → see 10/20/50/All(100) buttons with "20" pre-selected. -- Click Start → URL changes to `/m/g1/quiz` (placeholder still). The sessionStore should now hold the questions; you can verify in browser devtools React/zustand inspector or by adding a `console.log` temporarily. - -Stop dev server. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: implement `ModulePage` mode + size picker - -- three mode tiles: `multiple_choice`, `flashcards`, `recap` -- size picker hidden for static modules; dynamic offers 10/20/50/all -- `Start` calls `createSession` + `start()` then navigates to `/m/:slug/quiz` -EOF -)" -``` - ---- - -## Task 18: `MultipleChoice` mode component - -**Files:** -- Create: `src/modes/MultipleChoice/MultipleChoice.tsx`, `src/modes/MultipleChoice/MultipleChoice.module.css` - -- [ ] **Step 1: Create `src/modes/MultipleChoice/MultipleChoice.tsx`** - -```tsx -import { useMemo, useState } from "react"; -import type { Question } from "../../lib/types"; -import { shuffle } from "../../lib/sampling"; -import { Button } from "../../components/Button"; -import styles from "./MultipleChoice.module.css"; - -export interface AnswerPayload { - choiceId: string; - isCorrect: boolean; -} - -interface Props { - question: Question; - onAnswer: (payload: AnswerPayload) => void; - onNext: () => void; -} - -export function MultipleChoice({ question, onAnswer, onNext }: Props) { - const choices = useMemo(() => shuffle(question.choices), [question.id]); - const [selectedId, setSelectedId] = useState(null); - - const correctId = choices.find((c) => c.is_correct)?.id ?? null; - const locked = selectedId !== null; - - const handlePick = (choiceId: string) => { - if (locked) return; - const isCorrect = choiceId === correctId; - setSelectedId(choiceId); - onAnswer({ choiceId, isCorrect }); - }; - - const handleNext = () => { - setSelectedId(null); - onNext(); - }; - - return ( -
-

{question.prompt}

-
- {choices.map((c) => { - let cls = styles.choice; - if (locked) { - if (c.id === correctId) cls = styles.correct; - else if (c.id === selectedId) cls = styles.incorrect; - else cls = styles.dim; - } - return ( - - ); - })} -
- {locked && question.explanation ? ( -
{question.explanation}
- ) : null} - {locked ? ( - - ) : null} -
- ); -} -``` - -- [ ] **Step 2: Create `src/modes/MultipleChoice/MultipleChoice.module.css`** - -```css -.wrap { - display: flex; - flex-direction: column; - gap: var(--space-4); -} - -.prompt { - margin: 0; - font-size: 20px; - line-height: 1.4; -} - -.choices { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.choice, -.correct, -.incorrect, -.dim { - text-align: left; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - padding: var(--space-3) var(--space-4); - transition: border-color 0.12s ease, background 0.12s ease; -} - -.choice:hover { - border-color: var(--color-accent); -} - -.correct { - border-color: var(--color-correct); - background: #ecfdf5; - color: var(--color-correct); - font-weight: 600; -} - -.incorrect { - border-color: var(--color-incorrect); - background: #fef2f2; - color: var(--color-incorrect); - font-weight: 600; -} - -.dim { - opacity: 0.6; -} - -.explanation { - background: var(--color-bg); - border-left: 3px solid var(--color-accent); - padding: var(--space-3) var(--space-4); - border-radius: var(--radius-sm); - font-size: 14px; - color: var(--color-text-muted); - line-height: 1.5; -} -``` - -- [ ] **Step 3: Typecheck** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 4: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: implement `MultipleChoice` mode - -- shuffles choices on render -- locks after pick; highlights correct + incorrect; dims others -- shows `explanation` when present; `Next` button advances -EOF -)" -``` - ---- - -## Task 19: `Flashcards` mode component - -**Files:** -- Create: `src/modes/Flashcards/Flashcards.tsx`, `src/modes/Flashcards/Flashcards.module.css` - -- [ ] **Step 1: Create `src/modes/Flashcards/Flashcards.tsx`** - -```tsx -import { useState } from "react"; -import type { Question } from "../../lib/types"; -import { Button } from "../../components/Button"; -import styles from "./Flashcards.module.css"; - -export interface AnswerPayload { - selfGrade: boolean; - isCorrect: boolean; -} - -interface Props { - question: Question; - onAnswer: (payload: AnswerPayload) => void; - onNext: () => void; -} - -export function Flashcards({ question, onAnswer, onNext }: Props) { - const [flipped, setFlipped] = useState(false); - const [graded, setGraded] = useState(false); - - const grade = (gotIt: boolean) => { - if (graded) return; - setGraded(true); - onAnswer({ selfGrade: gotIt, isCorrect: gotIt }); - }; - - const handleNext = () => { - setFlipped(false); - setGraded(false); - onNext(); - }; - - return ( -
- - {flipped && !graded ? ( -
- - -
- ) : null} - {graded ? ( - - ) : null} -
- ); -} -``` - -- [ ] **Step 2: Create `src/modes/Flashcards/Flashcards.module.css`** - -```css -.wrap { - display: flex; - flex-direction: column; - gap: var(--space-4); - align-items: stretch; -} - -.card { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - padding: var(--space-7) var(--space-5); - min-height: 240px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: var(--space-4); - text-align: center; - font: inherit; - color: inherit; -} - -.card:hover { - border-color: var(--color-accent); -} - -.face { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.faceLabel { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-text-muted); -} - -.faceText { - font-size: 22px; - font-weight: 600; - line-height: 1.4; -} - -.hint { - font-size: 12px; - color: var(--color-text-muted); -} - -.gradeRow { - display: flex; - gap: var(--space-3); - justify-content: center; -} -``` - -- [ ] **Step 3: Typecheck** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 4: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: implement `Flashcards` mode - -- click card to flip between `prompt` and `flashcard_back` -- `Got it` / `Missed it` buttons grade after reveal -- `Next` advances; flipped + graded state reset per question -EOF -)" -``` - ---- - -## Task 20: `Recap` mode component - -**Files:** -- Create: `src/modes/Recap/Recap.tsx`, `src/modes/Recap/Recap.module.css` - -- [ ] **Step 1: Create `src/modes/Recap/Recap.tsx`** - -```tsx -import { useState } from "react"; -import type { Question } from "../../lib/types"; -import { Button } from "../../components/Button"; -import styles from "./Recap.module.css"; - -export interface AnswerPayload { - selfGrade: boolean; - isCorrect: boolean; -} - -interface Props { - question: Question; - onAnswer: (payload: AnswerPayload) => void; - onNext: () => void; -} - -export function Recap({ question, onAnswer, onNext }: Props) { - const [revealed, setRevealed] = useState(false); - const [graded, setGraded] = useState(false); - - const grade = (gotIt: boolean) => { - if (graded) return; - setGraded(true); - onAnswer({ selfGrade: gotIt, isCorrect: gotIt }); - }; - - const handleNext = () => { - setRevealed(false); - setGraded(false); - onNext(); - }; - - return ( -
-

{question.prompt}

- - {!revealed ? ( - - ) : ( -
-
Canonical answer
-
{question.recap_answer}
-
- )} - - {revealed && !graded ? ( -
- - -
- ) : null} - - {graded ? ( - - ) : null} -
- ); -} -``` - -- [ ] **Step 2: Create `src/modes/Recap/Recap.module.css`** - -```css -.wrap { - display: flex; - flex-direction: column; - gap: var(--space-4); -} - -.prompt { - margin: 0; - font-size: 22px; - line-height: 1.4; -} - -.answer { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-left: 4px solid var(--color-accent); - border-radius: var(--radius-md); - padding: var(--space-4); -} - -.answerLabel { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-text-muted); - margin-bottom: var(--space-2); -} - -.answerText { - font-size: 16px; - line-height: 1.5; -} - -.gradeRow { - display: flex; - gap: var(--space-3); - justify-content: center; -} -``` - -- [ ] **Step 3: Typecheck** - -Run: `bun run typecheck` -Expected: no errors. - -- [ ] **Step 4: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: implement `Recap` mode - -- shows `prompt`; `Reveal answer` displays `recap_answer` -- `Got it` / `Missed it` grade after reveal; `Next` advances -EOF -)" -``` - ---- - -## Task 21: `QuizPage` — orchestrates the active session - -**Files:** -- Create: `src/routes/QuizPage/QuizPage.tsx`, `src/routes/QuizPage/QuizPage.module.css` -- Modify: `src/App.tsx` (swap placeholder) - -- [ ] **Step 1: Create `src/routes/QuizPage/QuizPage.tsx`** - -```tsx -import { useEffect } from "react"; -import { useLocation } from "wouter"; -import { useSessionStore } from "../../store/sessionStore"; -import { completeSession, recordAnswer } from "../../lib/queries"; -import { ProgressBar } from "../../components/ProgressBar"; -import { MultipleChoice } from "../../modes/MultipleChoice/MultipleChoice"; -import { Flashcards } from "../../modes/Flashcards/Flashcards"; -import { Recap } from "../../modes/Recap/Recap"; -import styles from "./QuizPage.module.css"; - -interface Params { - slug: string; -} - -export function QuizPage({ params }: { params: Params }) { - const sessionId = useSessionStore((s) => s.sessionId); - const mode = useSessionStore((s) => s.mode); - const questions = useSessionStore((s) => s.questions); - const currentIndex = useSessionStore((s) => s.currentIndex); - const answers = useSessionStore((s) => s.answers); - const record = useSessionStore((s) => s.recordAnswer); - const advance = useSessionStore((s) => s.advance); - const [, navigate] = useLocation(); - - useEffect(() => { - if (!sessionId || !mode || questions.length === 0) { - navigate(`/m/${params.slug}`, { replace: true }); - } - }, [sessionId, mode, questions.length, navigate, params.slug]); - - if (!sessionId || !mode || questions.length === 0) return null; - - const question = questions[currentIndex]; - const total = questions.length; - - const finishIfDone = async (latestAnswers: typeof answers) => { - if (latestAnswers.length !== total) return; - const score = latestAnswers.filter((a) => a.isCorrect).length; - await completeSession({ sessionId, score }); - navigate(`/m/${params.slug}/results/${sessionId}`, { replace: true }); - }; - - const handleMc = async (payload: { - choiceId: string; - isCorrect: boolean; - }) => { - record({ - questionId: question.id, - choiceId: payload.choiceId, - selfGrade: null, - isCorrect: payload.isCorrect, - }); - await recordAnswer({ - sessionId, - questionId: question.id, - choiceId: payload.choiceId, - selfGrade: null, - isCorrect: payload.isCorrect, - }); - }; - - const handleSelfGrade = async (payload: { - selfGrade: boolean; - isCorrect: boolean; - }) => { - record({ - questionId: question.id, - choiceId: null, - selfGrade: payload.selfGrade, - isCorrect: payload.isCorrect, - }); - await recordAnswer({ - sessionId, - questionId: question.id, - choiceId: null, - selfGrade: payload.selfGrade, - isCorrect: payload.isCorrect, - }); - }; - - const handleNext = async () => { - advance(); - // After advance, if we've now answered all questions, finish. - const nextAnswers = useSessionStore.getState().answers; - await finishIfDone(nextAnswers); - }; - - return ( -
-
- - Question {currentIndex + 1} of {total} - - currentIndex ? 1 : 0)} total={total} /> -
- - {mode === "multiple_choice" ? ( - - ) : mode === "flashcards" ? ( - - ) : ( - - )} -
- ); -} -``` - -- [ ] **Step 2: Create `src/routes/QuizPage/QuizPage.module.css`** - -```css -.page { - max-width: 720px; - margin: 0 auto; - padding: var(--space-5); - display: flex; - flex-direction: column; - gap: var(--space-5); -} - -.top { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.counter { - font-size: 13px; - color: var(--color-text-muted); -} -``` - -- [ ] **Step 3: Wire `QuizPage` into `App.tsx`** - -```tsx -import { QuizPage } from "./routes/QuizPage/QuizPage"; -``` - -Replace the `/m/:slug/quiz` placeholder: - -```tsx - - {(params) => ( - - - - )} - -``` - -- [ ] **Step 4: Manual test (partial — results page still placeholder)** - -Run: `bun run dev` -- Sign in. Pick G1 → Multiple Choice → Start. -- Answer all 40 questions. The page should advance one at a time, the progress bar fills. -- After the 40th answer + Next, you're redirected to `/m/g1/results/` (placeholder shows the UUID). -- In Supabase SQL Editor, run `select count(*) from session_answers where session_id = '';` → expect 40. -- Run `select score, session_size, completed_at from sessions where id = '';` → `score` set, `completed_at` set. - -Stop the dev server. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: implement `QuizPage` orchestrator - -- pulls active session from `sessionStore`; redirects to module if missing -- renders one of `MultipleChoice` / `Flashcards` / `Recap` per `mode` -- inserts `session_answers` row per pick; on final `Next` calls `completeSession` and navigates to results -EOF -)" -``` - ---- - -## Task 22: `ResultsPage` — score + per-question review - -**Files:** -- Create: `src/routes/ResultsPage/ResultsPage.tsx`, `src/routes/ResultsPage/ResultsPage.module.css` -- Modify: `src/App.tsx` (swap placeholder) - -- [ ] **Step 1: Create `src/routes/ResultsPage/ResultsPage.tsx`** - -```tsx -import { useEffect, useState } from "react"; -import { Link, useLocation } from "wouter"; -import { - fetchSessionWithAnswers, - type SessionWithAnswers, -} from "../../lib/queries"; -import { useSessionStore } from "../../store/sessionStore"; -import { Card } from "../../components/Card"; -import { Button } from "../../components/Button"; -import styles from "./ResultsPage.module.css"; - -interface Params { - slug: string; - id: string; -} - -export function ResultsPage({ params }: { params: Params }) { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const reset = useSessionStore((s) => s.reset); - const [, navigate] = useLocation(); - - useEffect(() => { - let alive = true; - (async () => { - try { - const d = await fetchSessionWithAnswers(params.id); - if (!alive) return; - setData(d); - } catch (e) { - if (!alive) return; - setError(e instanceof Error ? e.message : "Failed to load"); - } finally { - if (alive) setLoading(false); - } - })(); - return () => { - alive = false; - }; - }, [params.id]); - - if (loading) return
Loading…
; - if (error) return
Error: {error}
; - if (!data) return
Session not found.
; - - const { session, answers, questions } = data; - const questionById = new Map(questions.map((q) => [q.id, q])); - const total = answers.length; - const score = session.score ?? answers.filter((a) => a.is_correct).length; - const pct = total === 0 ? 0 : Math.round((score / total) * 100); - - const tryAgain = () => { - reset(); - navigate(`/m/${params.slug}`); - }; - - const goHome = () => { - reset(); - navigate("/"); - }; - - return ( -
- -
- {score} / {total} -
-
{pct}% correct
-
- Mode: {session.mode.replace("_", " ")} - {session.completed_at ? ( - - Finished {new Date(session.completed_at).toLocaleString()} - - ) : null} -
-
- - -
-
- -

Review

-
- {answers.map((a, i) => { - const q = questionById.get(a.question_id); - if (!q) return null; - const correctChoice = q.choices.find((c) => c.is_correct); - const pickedChoice = a.choice_id - ? q.choices.find((c) => c.id === a.choice_id) - : null; - return ( - -
- #{i + 1} - - {a.is_correct ? "Correct" : "Missed"} - -
-
{q.prompt}
- {pickedChoice ? ( -
- You picked: {pickedChoice.label} -
- ) : a.self_grade !== null ? ( -
- Self-graded:{" "} - {a.self_grade ? "Got it" : "Missed it"} -
- ) : null} - {correctChoice ? ( -
- Correct: {correctChoice.label} -
- ) : null} -
- Answer: {q.recap_answer} -
-
- ); - })} -
- - - ← Back home - -
- ); -} -``` - -- [ ] **Step 2: Create `src/routes/ResultsPage/ResultsPage.module.css`** - -```css -.page { - max-width: 720px; - margin: 0 auto; - padding: var(--space-6) var(--space-5); - display: flex; - flex-direction: column; - gap: var(--space-5); -} - -.state { - padding: var(--space-6); - text-align: center; - color: var(--color-text-muted); -} - -.summary { - display: flex; - flex-direction: column; - gap: var(--space-2); - align-items: center; - text-align: center; -} - -.scoreNum { - font-size: 56px; - font-weight: 800; - letter-spacing: -0.02em; -} - -.pct { - font-size: 16px; - color: var(--color-text-muted); -} - -.modeRow { - display: flex; - flex-wrap: wrap; - gap: var(--space-3); - color: var(--color-text-muted); - font-size: 13px; - margin-top: var(--space-2); -} - -.actions { - display: flex; - gap: var(--space-3); - margin-top: var(--space-4); -} - -.reviewHeading { - margin: 0; - font-size: 14px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-muted); -} - -.list { - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.item { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.itemHead { - display: flex; - align-items: center; - justify-content: space-between; -} - -.itemNum { - color: var(--color-text-muted); - font-size: 13px; -} - -.correct { - color: var(--color-correct); - font-weight: 700; -} - -.incorrect { - color: var(--color-incorrect); - font-weight: 700; -} - -.itemPrompt { - font-weight: 600; -} - -.line { - font-size: 14px; - color: var(--color-text); - line-height: 1.5; -} - -.line strong { - color: var(--color-text-muted); - font-weight: 600; - margin-right: var(--space-1); -} - -.back { - font-size: 14px; - color: var(--color-text-muted); -} -``` - -- [ ] **Step 3: Wire `ResultsPage` into `App.tsx`** - -```tsx -import { ResultsPage } from "./routes/ResultsPage/ResultsPage"; -``` - -Replace `/m/:slug/results/:id` placeholder: - -```tsx - - {(params) => ( - - - - )} - -``` - -- [ ] **Step 4: Manual test** - -Run: `bun run dev` -- Sign in, pick JS → Recap → size 10 → Start. Answer all 10 (reveal + self-grade each). -- You're redirected to `/m/javascript/results/`. -- The summary card shows score / 10, percent, mode, completion timestamp. -- The review list shows all 10 questions with prompt, "Self-graded" line, correct answer. -- Click "Try again" → lands on `/m/javascript` (mode picker). -- Click "Back home" → lands on `/`. The JS card now shows "Last score: X / 10". - -Stop the dev server. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: implement `ResultsPage` - -- summary card: score, percent, mode, completed_at -- per-question review: prompt, picked choice or self-grade, canonical answer -- `Try again` → module page; `Back home` → home; both reset `sessionStore` -EOF -)" -``` - ---- - -## Task 23: Acceptance walkthrough + README - -**Files:** -- Modify: `README.md` - -- [ ] **Step 1: Update `README.md`** - -```markdown -# Locked In - -Brush up on a skill or topic right before a test, so the material is fresh -in your mind. Three modules (G1 driver's test, JavaScript, TypeScript) and -three modes (multiple choice, flashcards, recap). - -## Stack - -Bun · Vite · React + TypeScript · wouter · zustand · CSS modules · -Supabase (Postgres + Auth + RLS). - -## Setup - -1. Install dependencies: - ``` - bun install - ``` -2. Create a Supabase project at . -3. Create `.env.local` at the repo root with: - ``` - VITE_SUPABASE_URL=https://xxxx.supabase.co - VITE_SUPABASE_ANON_KEY= - ``` -4. In the Supabase SQL Editor, run each migration in order: - - `supabase/migrations/001_init.sql` - - `supabase/migrations/002_seed_g1.sql` - - `supabase/migrations/003_seed_javascript.sql` - - `supabase/migrations/004_seed_typescript.sql` -5. Run the dev server: - ``` - bun run dev - ``` - -## Scripts - -- `bun run dev` — start Vite dev server -- `bun run build` — production build -- `bun run preview` — preview the production build -- `bun run typecheck` — `tsc --noEmit` -- `bun run lint` — ESLint -``` - -- [ ] **Step 2: Acceptance walkthrough — verify all spec criteria** - -Run: `bun run dev`. With a real Supabase project + completed seed migrations, walk through each acceptance criterion from the spec (`docs/superpowers/specs/2026-05-25-lockedin-design.md` § Acceptance criteria): - -1. Sign up with email + password on `/login`, see `HomePage` immediately. -2. Sign out and sign back in; the relevant module card shows "Last score: X / Y". -3. Pick any module → any mode → start a session. -4. For G1, see all 40 questions in a single session (order shuffled). -5. For JS or TS, pick session size and see exactly that many questions, drawn at random. -6. Complete a session in each mode (MC, flashcards, recap) and reach results with a correct score. -7. On `ResultsPage`, scroll through every question with the answer. -8. Hit "Try again" and start a fresh session (dynamic re-samples; static reshuffles). -9. In the browser devtools network panel, observe a query like `select=*&...` on `sessions` returns only the current user's rows even without a `where user_id =` filter. - -Tick each box as you confirm. Fix anything that fails and re-test before continuing. - -- [ ] **Step 3: Final typecheck + lint** - -Run: `bun run typecheck` -Expected: exit 0. - -Run: `bun run lint` -Expected: exit 0 (warnings OK; no errors). - -- [ ] **Step 4: Commit** - -```bash -git add -A -git commit -m "$(cat <<'EOF' -LI: document setup in `README.md` - -- stack summary -- `.env.local` instructions for `VITE_SUPABASE_URL` / `VITE_SUPABASE_ANON_KEY` -- migration order; dev/build/typecheck scripts -EOF -)" -``` - ---- - -## Done - -After Task 23, v1 is shippable: a logged-in user can pick a module, pick a mode and (for dynamic modules) a size, run a quiz, and see results. Per-user progress is in Postgres, RLS-scoped, ready for the dashboard feature later. diff --git a/docs/superpowers/plans/2026-05-27-vitest-setup.md b/docs/superpowers/plans/2026-05-27-vitest-setup.md deleted file mode 100644 index 6a4fe16..0000000 --- a/docs/superpowers/plans/2026-05-27-vitest-setup.md +++ /dev/null @@ -1,340 +0,0 @@ -# Vitest setup — implementation plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add Vitest to the `lockedin` (crammr) repo and seed it with 3 unit tests covering the pure helpers in `src/lib/`, then compose `lint` + `typecheck` + `test` into a single `check` gate via `npm-run-all`. - -**Architecture:** Vitest 2.x re-uses the existing `vite.config.ts` (the `@/*` alias and React plugin) — its `test` block is added alongside, so there is no second config file. Tests are co-located with source as `*.test.ts`. The `npm-run-all` package (already installed) provides `run-s` for sequential script composition. - -**Tech Stack:** Vitest 2.1.x, npm-run-all 4.x, TypeScript 5.x, Vite 5.x (all already installed except Vitest). - -**Spec reference:** `docs/superpowers/specs/2026-05-27-vitest-setup-design.md` - ---- - -## File Structure - -| File | Action | Responsibility | -|---|---|---| -| `package.json` | Modify | Add `vitest` devDep; add `test` / `test:watch` / `check` scripts | -| `vite.config.ts` | Modify | Add `/// ` and a `test` block | -| `src/lib/sampling.test.ts` | Create | Tests for `sampleN` | -| `src/lib/role.test.ts` | Create | Tests for `getRole` | -| `src/lib/moduleCategories.test.ts` | Create | Tests for `categoryForModule` | - -No production source is changed — tests cover existing pure helpers. - ---- - -## Task 1: Install Vitest and wire scripts + config - -**Files:** -- Modify: `package.json` (devDependencies, scripts) -- Modify: `vite.config.ts` - -- [ ] **Step 1.1: Install Vitest 2.x** - -`vitest@^2.1.9` is required (not `@latest`) because `vitest@4` requires `vite@^6` and this repo is pinned to `vite@^5.4.2`. - -Run: -```bash -npm install --save-dev vitest@^2.1.9 -``` - -Expected: an `added N packages` line; `package.json` gains `"vitest": "^2.1.9"` under `devDependencies`. - -- [ ] **Step 1.2: Add test + check scripts to `package.json`** - -Edit the `"scripts"` block. The full block after edit: - -```jsonc -"scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "lint": "eslint . --max-warnings=0", - "typecheck": "tsc --noEmit -p tsconfig.app.json", - "test": "vitest run", - "test:watch": "vitest", - "check": "run-s lint typecheck test", - "prepare": "husky" -} -``` - -Only three keys are new: `test`, `test:watch`, `check`. All others are preserved verbatim. - -- [ ] **Step 1.3: Add Vitest config to `vite.config.ts`** - -Replace the entire contents of `vite.config.ts` with: - -```ts -/// -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import { fileURLToPath, URL } from "node:url"; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - "@": fileURLToPath(new URL("./src", import.meta.url)), - }, - }, - test: { - include: ["src/**/*.test.{ts,tsx}"], - }, -}); -``` - -The `include` glob covers `.tsx` so future component tests are discovered without an additional config change. - -The triple-slash reference at the top is mandatory — without it, TypeScript does not recognise the `test` key on `defineConfig`'s argument and `npm run typecheck` will fail. Use the `vitest/config` subpath (the documented type-augmentation entry point), not the bare `vitest` package. - -- [ ] **Step 1.4: Verify `vitest` finds no test files yet** - -Run: -```bash -npm test -``` - -Expected output contains: -``` -No test files found, exiting with code 1 -``` - -Exit code 1 here is fine — it confirms vitest is wired and discovering correctly. The next tasks will add test files. (Do NOT add any `passWithNoTests` flag.) - -- [ ] **Step 1.5: Verify typecheck still passes** - -Run: -```bash -npm run typecheck -``` - -Expected: exits 0 with no output. The `/// ` directive must be respected — if this fails with "object literal may only specify known properties, and 'test' does not exist", the directive was added incorrectly. - -- [ ] **Step 1.6: Verify lint still passes** - -Run: -```bash -npm run lint -``` - -Expected: exits 0 with no output. - -- [ ] **Step 1.7: Commit** - -```bash -git add package.json package-lock.json vite.config.ts -git commit -m "$(cat <<'EOF' -LI: install `vitest` and add `test` + `check` scripts - -- pin `vitest@^2.1.9` for `vite@^5` compat -- `test`, `test:watch`, `check` scripts -- `check` composes `lint` → `typecheck` → `test` via `run-s` -- `vite.config.ts` gains `test` block with `/// ` -EOF -)" -``` - -Note: `package-lock.json` may be untracked in this repo (per `.gitignore` and recent history). If `git status` shows it as untracked, omit it from the `git add` line. If `git add package-lock.json` succeeds without complaint, include it. - ---- - -## Task 2: Add `sampling.test.ts` - -**Files:** -- Create: `src/lib/sampling.test.ts` - -**Reference:** `src/lib/sampling.ts` exports `shuffle(items)` and `sampleN(items, n)`. `sampleN` returns `shuffle(items)` when `n >= items.length`, otherwise `shuffle(items).slice(0, n)`. - -- [ ] **Step 2.1: Write the test file** - -Create `src/lib/sampling.test.ts` with this exact content: - -```ts -import { describe, expect, it } from "vitest"; -import { sampleN } from "./sampling"; - -describe("sampleN", () => { - it("returns n elements drawn from the input with no duplicates when n < items.length", () => { - const input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const; - const result = sampleN(input, 3); - - expect(result).toHaveLength(3); - for (const value of result) { - expect(input).toContain(value); - } - expect(new Set(result).size).toBe(result.length); - }); -}); -``` - -This single `it` exercises three properties of `sampleN` (length, subset, uniqueness) — sufficient for the spec's "one test per pure helper" rule. - -- [ ] **Step 2.2: Run the test** - -Run: -```bash -npm test -``` - -Expected: 1 test file, 1 passed; exit code 0. The implementation already exists, so this passes immediately. - -- [ ] **Step 2.3: Commit** - -```bash -git add src/lib/sampling.test.ts -git commit -m "$(cat <<'EOF' -LI: add `sampleN` unit test - -- asserts length, subset, and uniqueness on a 10-element input -EOF -)" -``` - ---- - -## Task 3: Add `role.test.ts` - -**Files:** -- Create: `src/lib/role.test.ts` - -**Reference:** `src/lib/role.ts` exports `getRole(user)`. Returns `"regular"` for `null`/`undefined`, returns the role string for `app_metadata.role === "admin"` or `"trial"`, otherwise falls back to `"regular"`. - -- [ ] **Step 3.1: Write the test file** - -Create `src/lib/role.test.ts` with this exact content: - -```ts -import type { User } from "@supabase/supabase-js"; -import { describe, expect, it } from "vitest"; -import { getRole } from "./role"; - -const userWithRole = (role: string): User => - ({ app_metadata: { role } }) as unknown as User; - -describe("getRole", () => { - it("returns 'regular' when the user is null", () => { - expect(getRole(null)).toBe("regular"); - }); - - it("returns the role when app_metadata.role is admin", () => { - expect(getRole(userWithRole("admin"))).toBe("admin"); - }); - - it("returns 'regular' when app_metadata.role is unknown", () => { - expect(getRole(userWithRole("hacker"))).toBe("regular"); - }); -}); -``` - -The `as unknown as User` cast is unavoidable here — the test only exercises the `app_metadata.role` field, and constructing a full Supabase `User` (id, email, aud, created_at, etc.) would be pure ceremony. ESLint does not forbid this cast. - -- [ ] **Step 3.2: Run the tests** - -Run: -```bash -npm test -``` - -Expected: 2 test files, 4 tests passed (1 from Task 2 + 3 here); exit code 0. - -- [ ] **Step 3.3: Commit** - -```bash -git add src/lib/role.test.ts -git commit -m "$(cat <<'EOF' -LI: add `getRole` unit test - -- covers `null` user, `admin` passthrough, unknown-role fallback -EOF -)" -``` - ---- - -## Task 4: Add `moduleCategories.test.ts` - -**Files:** -- Create: `src/lib/moduleCategories.test.ts` - -**Reference:** `src/lib/moduleCategories.ts` exports `MODULE_CATEGORY_SLUG`, `DEFAULT_CATEGORY_SLUG = "other"`, and `categoryForModule(slug)` which returns the mapped category or `DEFAULT_CATEGORY_SLUG`. - -- [ ] **Step 4.1: Write the test file** - -Create `src/lib/moduleCategories.test.ts` with this exact content: - -```ts -import { describe, expect, it } from "vitest"; -import { categoryForModule } from "./moduleCategories"; - -describe("categoryForModule", () => { - it("maps a known module slug to its category", () => { - expect(categoryForModule("javascript")).toBe("programming-languages"); - }); - - it("falls back to 'other' for an unknown slug", () => { - expect(categoryForModule("not-a-real-slug")).toBe("other"); - }); -}); -``` - -- [ ] **Step 4.2: Run the tests** - -Run: -```bash -npm test -``` - -Expected: 3 test files, 6 tests passed (1 + 3 + 2); exit code 0. - -- [ ] **Step 4.3: Commit** - -```bash -git add src/lib/moduleCategories.test.ts -git commit -m "$(cat <<'EOF' -LI: add `categoryForModule` unit test - -- covers known-slug mapping and `other` fallback -EOF -)" -``` - ---- - -## Task 5: Verify the full `check` gate - -**Files:** none (verification only). - -- [ ] **Step 5.1: Run `npm run check`** - -Run: -```bash -npm run check -``` - -Expected sequence: -1. `> lint` — `eslint . --max-warnings=0` exits 0 -2. `> typecheck` — `tsc --noEmit -p tsconfig.app.json` exits 0 -3. `> test` — `vitest run` reports 3 test files, 6 tests passed - -Final exit code: 0. If any step fails, do NOT proceed — diagnose and fix the failing step before claiming the plan is complete. - -- [ ] **Step 5.2: Confirm script order via `run-s`** - -If `npm run check` exits non-zero on `lint` or `typecheck`, vitest must NOT have run (sequential execution). Verify by reading the captured output — no `RUN v2.1.x` line should appear after a lint or typecheck failure. - -- [ ] **Step 5.3: No commit** - -Task 5 is verification only — there are no file changes to commit. - ---- - -## Acceptance - -- `npm test` reports 3 test files, 6 tests passed, exit 0. -- `npm run check` exits 0 with lint, typecheck, and test all green. -- `git log --oneline | head -5` shows 4 new commits (1 setup + 3 tests), each prefixed `LI:`. -- No new top-level files outside `package.json`, `vite.config.ts`, and the three `src/lib/*.test.ts` files. -- No new devDependencies beyond `vitest` (and the already-installed `npm-run-all`). diff --git a/docs/superpowers/plans/2026-07-09-module-kind.md b/docs/superpowers/plans/2026-07-09-module-kind.md deleted file mode 100644 index 826820f..0000000 --- a/docs/superpowers/plans/2026-07-09-module-kind.md +++ /dev/null @@ -1,139 +0,0 @@ -# Module Kind Property Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a `kind` property to every learning module (DB column + TypeScript type) classifying its domain, so future UI work can format kinds differently. - -**Architecture:** A new re-runnable migration `022_module_kind.sql` adds `modules.kind` with a check constraint and backfills all 38 existing modules. `src/lib/types.ts` gains a `ModuleKind` union and `Module.kind`. Metadata only — no rendering changes. - -**Tech Stack:** Supabase (Postgres) migrations applied via `bun run db:apply` (psql), TypeScript, Bun. - -## Global Constraints - -- Never use `interface`; `type` aliases only (repo `CLAUDE.md`) -- Prefer `const`; no `let`/`var` unless materially necessary (repo `CLAUDE.md`) -- Commit messages: `CRMR:` prefix, terse `-` bullets, backticked identifiers, zero AI attribution (crammr-commit-format skill) -- Kind vocabulary (spec): `coding`, `human-language`, `test-prep`, `driving`, `other` -- Column name is `kind` — `type` is already taken by `static | dynamic` -- Note on TDD: this change has no runtime TS surface (a type-only edit) and no app logic; the test cycle is the migration's SQL count verification plus `tsc` and the existing `bun test` suite. No new unit tests (YAGNI). - ---- - -### Task 1: Migration `022_module_kind.sql` - -**Files:** -- Create: `supabase/migrations/022_module_kind.sql` - -**Interfaces:** -- Consumes: existing `modules` table (`001_init.sql`), module slugs seeded by `002_seed_g1.sql`, `003`/`004`, `006_seed_more_modules.sql` -- Produces: `modules.kind text not null default 'other'` with check constraint `modules_kind_check`; Task 2's `ModuleKind` union must match the constraint's five values exactly - -- [ ] **Step 1: Write the migration** - -```sql --- 022_module_kind.sql --- Add a `kind` column to modules classifying each learning module's domain, --- so the client can format kinds differently (e.g. coding modules). --- --- Re-runnable: guarded alters + idempotent updates. - -alter table modules add column if not exists kind text not null default 'other'; - -alter table modules drop constraint if exists modules_kind_check; -alter table modules add constraint modules_kind_check - check (kind in ('coding', 'human-language', 'test-prep', 'driving', 'other')); - -update modules set kind = 'coding' where slug in ( - 'javascript-1', 'javascript-2', 'javascript-3', - 'typescript-1', 'typescript-2', 'typescript-3', - 'python-1', 'python-2', 'python-3', - 'c-1', 'c-2', 'c-3', - 'cpp-1', 'cpp-2', 'cpp-3', - 'java-1', 'java-2', 'java-3', - 'csharp-1', 'csharp-2', 'csharp-3', - 'sql-1', 'sql-2', 'sql-3', - 'go-1', 'go-2', 'go-3', - 'rust-1', 'rust-2', 'rust-3' -); - -update modules set kind = 'human-language' where slug in ( - 'english', 'spanish', 'french', 'japanese', 'italian', 'portuguese' -); - -update modules set kind = 'test-prep' where slug = 'real-estate'; - -update modules set kind = 'driving' where slug = 'g1'; -``` - -- [ ] **Step 2: Apply the migration** - -Run: `bun run db:apply supabase/migrations/022_module_kind.sql` -Expected: `-> …/022_module_kind.sql` then `Applied 1 migration.` with no psql errors. (Requires `SUPABASE_DB_URL` in `.env.local`; falls back to Supabase MCP `apply_migration` if `psql` is unavailable.) - -- [ ] **Step 3: Verify backfill counts** - -Run (with `SUPABASE_DB_URL` loaded): -```bash -psql "$SUPABASE_DB_URL" -c "select kind, count(*) from modules group by kind order by kind;" -``` -Expected output — exactly four rows, no `other`: -``` - kind | count -----------------+------- - coding | 30 - driving | 1 - human-language | 6 - test-prep | 1 -``` - -- [ ] **Step 4: Re-run the migration to prove idempotence** - -Run: `bun run db:apply supabase/migrations/022_module_kind.sql` -Expected: succeeds again with no errors; count query unchanged. - -### Task 2: `ModuleKind` in `src/lib/types.ts` + commit - -**Files:** -- Modify: `src/lib/types.ts:1-13` - -**Interfaces:** -- Consumes: `modules.kind` column from Task 1 (five values, exact strings) -- Produces: `export type ModuleKind = "coding" | "human-language" | "test-prep" | "driving" | "other";` and `Module.kind: ModuleKind` — future layout work branches on `module.kind` - -- [ ] **Step 1: Add the type** - -In `src/lib/types.ts`, below `ModuleType`, add: - -```ts -export type ModuleKind = - | "coding" - | "human-language" - | "test-prep" - | "driving" - | "other"; -``` - -and in `Module`, after `type: ModuleType;`, add: - -```ts - kind: ModuleKind; -``` - -- [ ] **Step 2: Typecheck and run existing tests** - -Run: `bunx tsc --noEmit -p tsconfig.app.json && bun test` -Expected: tsc silent; all existing tests pass (`moduleCategories`, `role`, `sampling`). - -- [ ] **Step 3: Commit** - -```bash -git add supabase/migrations/022_module_kind.sql src/lib/types.ts -git commit -m "$(cat <<'EOF' -CRMR: add `kind` property to learning modules - -- `022_module_kind.sql` adds `modules.kind` (`coding`/`human-language`/`test-prep`/`driving`/`other`) with check constraint + backfill, re-runnable -- `ModuleKind` union + `Module.kind` in `types.ts` -EOF -)" -``` -Expected: pre-commit `tsc` + `eslint` pass; commit created. diff --git a/docs/superpowers/specs/2026-05-25-lockedin-design.md b/docs/superpowers/specs/2026-05-25-lockedin-design.md deleted file mode 100644 index a1d196e..0000000 --- a/docs/superpowers/specs/2026-05-25-lockedin-design.md +++ /dev/null @@ -1,275 +0,0 @@ -# Locked In — v1 Design - -**Date:** 2026-05-25 -**Status:** Approved (pending final review) - -## Purpose - -A learning-module web app for brushing up on a skill or topic right before a -test, so the material is fresh in your mind. v1 ships three modules and three -learning modes, with user accounts so progress is captured for a future stats -dashboard. - -## Tech stack - -- **Runtime / package manager:** Bun (`bun install`, `bun run dev`). -- **Build / dev server:** Vite (React + TS template). -- **UI:** React 18 + TypeScript, CSS modules for styling, `lucide-react` for - icons. No Tailwind, no component framework. -- **Routing:** wouter. -- **Global state:** zustand (one store for auth, one for the active quiz - session). -- **Backend:** Supabase — Postgres + Auth + Row-Level Security. No custom - server. -- **Lint / typecheck:** ESLint + `tsc --noEmit`. - -## Project layout - -``` -src/ - routes/ one folder per route (LoginPage, HomePage, ModulePage, QuizPage, ResultsPage) - components/ shared UI (Button, Card, Header, ProgressBar, RequireAuth) - modes/ MultipleChoice/, Flashcards/, Recap/ (one per learning mode) - store/ zustand stores (authStore, sessionStore) - lib/ supabase client, sampling helpers, types - styles/ global.css + CSS variables (light theme for v1) -supabase/ - migrations/ schema + seed data SQL -docs/superpowers/specs/ - this design and future ones -``` - -## Modules - -Three modules ship in v1: - -| slug | name | type | total questions | -| ------------ | ------------------------ | ------- | --------------- | -| `g1` | G1 Driver's Test | static | 40 (20 signs + 20 rules) | -| `javascript` | JavaScript Test | dynamic | 100 (session samples 10/20/50/all) | -| `typescript` | TypeScript Test | dynamic | 100 (session samples 10/20/50/all) | - -**Module types:** - -- **static** — every question is asked once per session (order may be - shuffled). `session_size` is always equal to `total_questions`. -- **dynamic** — a random subset of `session_size` questions is drawn from the - full pool. User picks the size at session start; allowed values for v1 are - 10, 20, 50, and "all". - -## Learning modes - -Each module supports all three modes. Mode components live in `src/modes/`, -each receiving `{ question, onAnswer }`. - -1. **Multiple Choice** — radio-style buttons rendered from - `question_choices`. Clicking one immediately reveals correctness + - `question.explanation`, then a "Next" button advances. -2. **Flashcards** — front shows `question.prompt`; tap/click to flip; back - shows `question.flashcard_back`; "Got it" / "Missed it" buttons advance. -3. **Recap (self-graded reveal)** — shows `question.prompt`; "Reveal answer" - shows `question.recap_answer`; "Got it" / "Missed it" advance. - -All three call `onAnswer(isCorrect, payload)` so `QuizPage` never has to know -mode internals. - -## Data model - -Five Postgres tables. Anything user-owned is RLS-protected. - -``` -modules seeded, read-only to users -───────── -id uuid pk -slug text unique 'g1', 'javascript', 'typescript' -name text -description text -type text 'static' | 'dynamic' -total_questions int count in pool (cached) -created_at timestamptz - -questions seeded, read-only to users -───────── -id uuid pk -module_id uuid fk → modules -category text null e.g. 'signs' or 'rules' for G1 -prompt text the question -explanation text null shown after answer in MC mode -flashcard_back text what's revealed in flashcard mode -recap_answer text canonical answer for recap mode -order_index int stable display order for static modules -created_at timestamptz - -question_choices MC options for each question -───────── -id uuid pk -question_id uuid fk → questions -label text the choice text -is_correct bool -order_index int - -sessions one per quiz attempt (RLS: user_id = auth.uid()) -───────── -id uuid pk -user_id uuid fk → auth.users -module_id uuid fk → modules -mode text 'multiple_choice' | 'flashcards' | 'recap' -session_size int N questions in this session -started_at timestamptz -completed_at timestamptz null -score int null correct count, filled on completion - -session_answers one row per answered question (RLS: via session) -───────── -id uuid pk -session_id uuid fk → sessions -question_id uuid fk → questions -choice_id uuid fk → question_choices null for MC -self_grade bool null for flashcards/recap: true=got it, false=missed -is_correct bool normalized correctness across modes -answered_at timestamptz -``` - -**Notes:** - -- One `questions` row supports all three modes — no mode-specific tables. -- For dynamic modules, sampling is `ORDER BY random() LIMIT N` in the client - query. No spaced-repetition logic in v1. -- `is_correct` is normalized so a future dashboard can do `SUM(is_correct)` - without caring which mode it came from. -- Seed data: one migration per module - (`002_seed_g1.sql`, `003_seed_javascript.sql`, `004_seed_typescript.sql`). - -## Routes & flow - -``` -/login LoginPage public; redirects home if signed in -/ HomePage lists the 3 module cards -/m/:slug ModulePage pick mode + session size, then start -/m/:slug/quiz QuizPage in-progress session; renders mode component -/m/:slug/results/:id ResultsPage score + per-question review -``` - -Everything except `/login` is wrapped in ``, which redirects to -`/login` when `authStore.user` is null. - -**Session flow:** - -``` -LoginPage - │ email+password or magic link → Supabase Auth - ▼ -HomePage - │ shows 3 module cards (G1, JavaScript, TypeScript) - │ each card: name, type badge, # questions, last score if any - ▼ -ModulePage /m/g1 - │ pick mode: [Multiple Choice] [Flashcards] [Recap] - │ pick size: static → locked to total; dynamic → 10 / 20 / 50 / all - │ [Start] - ▼ -QuizPage - │ sessionStore holds: questions[], currentIndex, answers[], mode - │ renders one of: - │ progress bar across the top - │ each answer → push to sessionStore + insert session_answers row - │ when currentIndex === questions.length → mark session complete, - │ navigate to results - ▼ -ResultsPage - │ score, % correct, time taken - │ scrollable review: every question + what user picked + correct answer - │ [Try again] [Back home] -``` - -## State (zustand) - -- **`authStore`** — `{ user, session, status, signIn, signUp, - signInWithMagicLink, signOut }`. Thin wrapper over Supabase auth; subscribes - to `supabase.auth.onAuthStateChange` so all consumers stay in sync. -- **`sessionStore`** — `{ moduleId, mode, questions, currentIndex, answers, - startedAt, sessionId, start(), recordAnswer(), finish() }`. Owns the active - quiz; cleared on `finish()` and on logout. - -## Auth flow - -Supabase Auth, two options on the login page: - -1. Email + password (default). -2. Magic link. - -No OAuth providers for v1. - -**Wiring:** - -- `lib/supabase.ts` creates a single client with `localStorage` persistence. -- `App.tsx` calls `supabase.auth.getSession()` on mount and renders a small - loading shell while the promise is pending (avoids flashing `/login` for an - already-signed-in user). -- `authStore` subscribes to `onAuthStateChange` so login, logout, token - refresh, and tab restoration all flow through one place. -- `` wraps protected routes. -- `Header` shows email + sign-out button when authenticated. - -**Row-Level Security policies:** - -```sql --- sessions: owner can do anything with their own -create policy "own sessions" on sessions - for all using (auth.uid() = user_id) - with check (auth.uid() = user_id); - --- session_answers: owner via the parent session -create policy "own session answers" on session_answers - for all using ( - exists (select 1 from sessions s - where s.id = session_answers.session_id - and s.user_id = auth.uid()) - ); - --- shared content: read-only for any authenticated user -create policy "read modules" on modules for select using (auth.role() = 'authenticated'); -create policy "read questions" on questions for select using (auth.role() = 'authenticated'); -create policy "read choices" on question_choices for select using (auth.role() = 'authenticated'); -``` - -The future dashboard becomes a pure-read addition: `select * from sessions` -already returns only the current user's rows because RLS rewrites the query. - -## Non-goals (v1) - -Named so they don't sneak into scope: - -- **Dashboard / stats pages** — schema supports it, UI lands later. -- **Spaced repetition / smart sampling** — uniform random for v1. -- **Authoring UI** — questions added/edited via SQL migrations only. -- **Mobile-app shell** — responsive web only; no PWA, no native wrappers. -- **Offline mode** — requires an internet connection. -- **Question images** — G1 signs use Unicode/emoji or inline SVG placeholders - in v1; binary image storage lands later. -- **Social features** — no sharing, leaderboards, friends. -- **Password reset UI, email verification gating, OAuth, profile editing.** -- **Test suite** — none for v1; will add when the dashboard or any - non-trivial logic lands. - -## Acceptance criteria - -A user can: - -1. Sign up with email + password on `/login`, see `HomePage` immediately. -2. Sign out and sign back in; their previous sessions still show "last score" - on the relevant module card. -3. Pick any module → any mode → start a session. -4. For G1, see all 40 questions in a single session (order shuffled). -5. For JS or TS, pick a session size (10/20/50/all) and see exactly that many - questions, drawn at random. -6. Complete a session in each mode (MC, flashcards, recap) and reach the - results page with a correct score. -7. On `ResultsPage`, scroll through every question of the session with their - answer and the correct answer. -8. Hit "Try again" and start a fresh session of the same module/mode (for - dynamic modules, a new random sample is drawn; for static modules, the - order is reshuffled). -9. Open the browser devtools and confirm a query like `select * from sessions` - returns only rows owned by the logged-in user, even though the client has - no `where user_id = ...` clause. diff --git a/docs/superpowers/specs/2026-05-27-vitest-setup-design.md b/docs/superpowers/specs/2026-05-27-vitest-setup-design.md deleted file mode 100644 index 6240a00..0000000 --- a/docs/superpowers/specs/2026-05-27-vitest-setup-design.md +++ /dev/null @@ -1,133 +0,0 @@ -# Vitest setup — design - -## Goal - -Add a unit-test runner to the `lockedin` (crammr) repo and seed it with 3 -unit tests covering the pure helpers in `src/lib/`. Compose the existing -`lint` + `typecheck` + new `test` script into a single `check` gate via -`npm-run-all`. - -## Framework - -**Vitest.** Chosen over Jest because: - -- Vite-native — reuses the existing `vite.config.ts`, the `@/*` path alias, - and `tsconfig.app.json` with no extra config. -- ESM-clean — `package.json` is `"type": "module"`, which Jest still - handles awkwardly (ts-jest + babel + ESM shim). -- Jest-compatible API (`describe` / `it` / `expect`) — minimal learning - cost. - -## Dependencies - -Add as `devDependency`: - -- `vitest@^2.x` (peer-matches the installed `vite@^5`) - -Already installed in this brainstorm session: - -- `npm-run-all@^4.1.5` - -**Not** added (YAGNI for 3 pure-function tests): - -- `@testing-library/react` -- `jsdom` / `happy-dom` -- `@vitest/coverage-*` - -## Scripts - -In `package.json`: - -```jsonc -"test": "vitest run", // CI-style single run -"test:watch": "vitest", // local TDD -"check": "run-s lint typecheck test" // composed gate -``` - -`run-s` (sequential) over `run-p` (parallel) so failures are read -top-to-bottom and lint/typecheck output isn't interleaved with test -output. Existing `lint`, `typecheck`, `dev`, `build`, `preview`, and -`prepare` scripts are untouched. - -## Vitest config - -Add a minimal `test` block inside the existing `vite.config.ts` (no -separate `vitest.config.ts` — avoids duplicating the React plugin and -the `@/*` alias): - -```ts -test: { - include: ["src/**/*.test.{ts,tsx}"], - // node env is default; jsdom not needed for pure helpers -} -``` - -The glob covers `.tsx` even though no `.tsx` tests exist yet — Vitest's -own default covers both, and an explicit `.ts`-only override would -silently skip any future component test. - -The Vite config will need the triple-slash directive `/// ` at the top so TypeScript recognises the `test` -key. The `vitest/config` subpath is the documented type-augmentation -entry point for `defineConfig` (the bare `vitest` package re-exports -enough types to typecheck, but it's not the idiomatic form). - -## The 3 tests - -Co-located next to their source files (standard Vitest convention, -keeps tests discoverable from the unit they cover): - -### 1. `src/lib/sampling.test.ts` - -Covers `sampleN(items, n)`: - -- Returns exactly `n` elements when `n < items.length`. -- All returned elements are drawn from the input (subset assertion). -- No duplicates in the output. - -Order is not asserted — `shuffle` uses `Math.random()` and is -non-deterministic. - -### 2. `src/lib/role.test.ts` - -Covers `getRole(user)`: - -- `getRole(null)` → `"regular"` (the unauthenticated fallback). -- `getRole({ app_metadata: { role: "admin" } })` → `"admin"`. -- `getRole({ app_metadata: { role: "hacker" } })` → `"regular"` - (unknown role falls through to default). - -User objects are cast through `as unknown as User` — the test only -exercises the `app_metadata.role` field, so a full Supabase `User` stub -is unnecessary. - -### 3. `src/lib/moduleCategories.test.ts` - -Covers `categoryForModule(slug)`: - -- Known slug: `categoryForModule("javascript")` → - `"programming-languages"`. -- Unknown slug: `categoryForModule("not-a-real-slug")` → `"other"` - (`DEFAULT_CATEGORY_SLUG`). - -## ESLint - -Existing `eslint . --max-warnings=0` will pick up `*.test.ts` files -without change. Vitest globals (`describe`, `it`, `expect`) are -**imported explicitly** from `"vitest"` — no `globals` config needed -in vitest, no env config needed in eslint. - -## Out of scope - -- Coverage reporting. -- React Testing Library / component or hook tests. -- A husky pre-commit hook for `test` (husky is installed but its config - is intentionally not touched here). -- CI workflow changes. - -## Acceptance - -- `npm test` exits 0 with 3 passing tests. -- `npm run check` exits 0 (lint → typecheck → test, all green). -- `npm run typecheck` still exits 0 (the `test` key in `vite.config.ts` - must type-check via the `vitest` reference directive). diff --git a/docs/superpowers/specs/2026-07-09-module-kind-design.md b/docs/superpowers/specs/2026-07-09-module-kind-design.md deleted file mode 100644 index f7c51e2..0000000 --- a/docs/superpowers/specs/2026-07-09-module-kind-design.md +++ /dev/null @@ -1,83 +0,0 @@ -# Module kind property — design - -**Date:** 2026-07-09 -**Status:** Approved - -## Problem - -Every learning module should carry a property saying what kind of learning -module it is, so that future UI work can format different kinds differently -(e.g. coding-language modules get a code-oriented quiz layout). Today the only -module-level classification is: - -- `modules.type` (`static | dynamic`) — session-sampling behavior, not - content kind. The name `type` is therefore taken. -- `src/lib/moduleCategories.ts` — a hand-maintained client-side slug → category - map used only to group cards on the All Modules page. - -## Decision - -Add a `kind` column to the `modules` table (approach A: DB column, single -source of truth, declared at seed time). Metadata only for now — no rendering -or layout changes in this work. - -### Kind vocabulary - -One kind per domain: - -| kind | modules | -| ---------------- | -------------------------------------------------------------- | -| `coding` | the 30 programming-language modules (`javascript-1` … `rust-3`) | -| `human-language` | `english`, `spanish`, `french`, `japanese`, `italian`, `portuguese` | -| `test-prep` | `real-estate` | -| `driving` | `g1` | -| `other` | fallback default for future modules | - -## Data model - -New migration `supabase/migrations/022_module_kind.sql`: - -1. `alter table modules add column if not exists kind text not null default 'other';` -2. Drop-if-exists then re-add check constraint - `modules_kind_check`: `kind in ('coding', 'human-language', 'test-prep', 'driving', 'other')`. -3. Backfill `update` statements by slug per the table above. - -Re-runnability: - -- Standalone: `if not exists` + idempotent updates make 022 safe to re-run. -- Full chain: `001_init.sql` drops/recreates `modules` without the column, - `002`/`006` insert module rows, then `022` adds the column and backfills. -- The `'other'` default lets future seed migrations insert modules without - naming a kind; the check constraint keeps values valid. - -Applied via `bun run db:apply supabase/migrations/022_module_kind.sql` (or the -full `bun run db:apply`). - -## TypeScript - -In `src/lib/types.ts`: - -```ts -export type ModuleKind = - | "coding" - | "human-language" - | "test-prep" - | "driving" - | "other"; -``` - -`Module` gains `kind: ModuleKind`. Queries already `select("*")` on modules, so -no query changes. `moduleCategories.ts` and the All Modules grouping stay -untouched; future layout work branches on `module.kind`. - -## Out of scope - -- Any rendering/layout changes keyed off `kind`. -- Replacing or consolidating `moduleCategories.ts`. -- Changes to `request_categories` (module-request feature). - -## Verification - -- Apply the migration, then confirm per-kind counts: - 30 `coding`, 6 `human-language`, 1 `test-prep`, 1 `driving`, 0 `other`. -- `tsc` passes and existing tests (`bun test`) pass. diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 0bbf074..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,28 +0,0 @@ -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"] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ["**/*.{ts,tsx}"], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], - }, - } -); diff --git a/package.json b/package.json index c3ee03a..5d47e77 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,28 @@ { "name": "crammr", - "private": true, "version": "0.1.0", + "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", - "lint": "eslint . --max-warnings=0", - "typecheck": "tsc --noEmit -p tsconfig.app.json", + "lint:ts": "oxlint --type-aware", + "lint:ts:fix": "oxlint --type-aware --fix", + "lint:css": "gale \"src/**/*.css\" \"src/**/*.scss\"", + "lint:css:fix": "gale --fix \"src/**/*.css\" \"src/**/*.scss\"", + "typecheck": "tsgo --noEmit", "test": "vitest run", + "clean": "rm -rf dist", "test:watch": "vitest", - "check": "run-s lint typecheck test", + "test:coverage": "vitest run --coverage", + "format": "oxfmt", + "format:check": "oxfmt --check", + "format:staged": "lint-staged", "db:apply": "bun scripts/apply-migrations.ts", "db:push": "supabase db push", - "prepare": "husky" + "prepare": "husky", + "system-check": "run-s clean format:check lint:ts lint:css typecheck test build" }, "dependencies": { "@fontsource-variable/space-grotesk": "5.2.10", @@ -29,20 +37,35 @@ "zustand": "5.0.13" }, "devDependencies": { - "@eslint/js": "9.39.4", + "@lyricalstring/gale": "0.1.5", "@types/react": "18.3.29", "@types/react-dom": "18.3.7", + "@typescript/native-preview": "7.0.0-dev.20260707.2", "@vitejs/plugin-react": "4.7.0", - "eslint": "9.39.4", - "eslint-plugin-react-hooks": "5.2.0", - "eslint-plugin-react-refresh": "0.4.26", - "globals": "15.15.0", + "@vitest/coverage-v8": "2.1.9", "husky": "9.1.7", + "lint-staged": "17.3.0", "npm-run-all": "4.1.5", + "oxfmt": "0.62.0", + "oxlint": "1.77.0", + "oxlint-tsgolint": "7.0.2001", "playwright": "1.60.0", "typescript": "5.9.3", - "typescript-eslint": "8.60.0", "vite": "5.4.21", "vitest": "2.1.9" - } + }, + "lint-staged": { + "*.{js,jsx,ts,tsx,mjs,cjs}": [ + "oxlint --fix", + "oxfmt" + ], + "*.{css,scss}": [ + "gale --fix", + "oxfmt" + ], + "*.{json,jsonc,json5,md,mdx,yml,yaml,toml,html}": "oxfmt" + }, + "trustedDependencies": [ + "@lyricalstring/gale" + ] } diff --git a/scripts/capture-screenshots.ts b/scripts/capture-screenshots.ts index a8763fd..2441a18 100644 --- a/scripts/capture-screenshots.ts +++ b/scripts/capture-screenshots.ts @@ -28,8 +28,7 @@ const FAR_FUTURE = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365; const fakeJwt = () => { // Three base64url segments. Not cryptographically valid, but well-formed. - const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })) - .toString("base64url"); + const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url"); const body = Buffer.from( JSON.stringify({ sub: USER_ID, @@ -37,7 +36,7 @@ const fakeJwt = () => { role: "authenticated", aud: "authenticated", exp: FAR_FUTURE, - }) + }), ).toString("base64url"); const sig = Buffer.from("preview-sig").toString("base64url"); return `${header}.${body}.${sig}`; @@ -86,7 +85,8 @@ const MODULES = [ id: "m-javascript-1", slug: "javascript-1", name: "JavaScript — Level 1", - description: "JavaScript basics — types, operators, core syntax, and everyday array/object usage.", + description: + "JavaScript basics — types, operators, core syntax, and everyday array/object usage.", type: "dynamic", total_questions: 100, created_at: "2024-01-02T00:00:00.000Z", @@ -95,7 +95,8 @@ const MODULES = [ id: "m-typescript-1", slug: "typescript-1", name: "TypeScript — Level 1", - description: "TypeScript basics — type annotations, primitives, tuples, simple unions, and `any` vs `unknown`.", + description: + "TypeScript basics — type annotations, primitives, tuples, simple unions, and `any` vs `unknown`.", type: "dynamic", total_questions: 21, created_at: "2024-01-03T00:00:00.000Z", @@ -104,7 +105,8 @@ const MODULES = [ id: "m-python-1", slug: "python-1", name: "Python — Level 1", - description: "Python basics — built-in types, operators, control flow, and core collection usage.", + description: + "Python basics — built-in types, operators, control flow, and core collection usage.", type: "dynamic", total_questions: 34, created_at: "2024-01-04T00:00:00.000Z", @@ -113,7 +115,8 @@ const MODULES = [ id: "m-rust-1", slug: "rust-1", name: "Rust — Level 1", - description: "Rust basics — types, mutability, ownership intuition, references, and basic structs/enums.", + description: + "Rust basics — types, mutability, ownership intuition, references, and basic structs/enums.", type: "dynamic", total_questions: 24, created_at: "2024-01-05T00:00:00.000Z", @@ -122,7 +125,8 @@ const MODULES = [ id: "m-go-1", slug: "go-1", name: "Go — Level 1", - description: "Go basics — types, slices, maps, strings, structs, functions, packages, and zero values.", + description: + "Go basics — types, slices, maps, strings, structs, functions, packages, and zero values.", type: "dynamic", total_questions: 40, created_at: "2024-01-06T00:00:00.000Z", @@ -158,7 +162,8 @@ const MODULES = [ id: "m-real-estate", slug: "real-estate", name: "Real Estate License", - description: "Prep for the real estate licensing exam — agency, contracts, finance, and ethics.", + description: + "Prep for the real estate licensing exam — agency, contracts, finance, and ethics.", type: "dynamic", total_questions: 100, created_at: "2024-01-10T00:00:00.000Z", @@ -198,8 +203,10 @@ type JSQuestion = { const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "Closures", - prompt: "What does this snippet log?\n\n```js\nfor (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);\n```", - explanation: "`var` is function-scoped, so all three timeouts share the same `i`. By the time they fire, the loop is done and `i === 3`.", + prompt: + "What does this snippet log?\n\n```js\nfor (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);\n```", + explanation: + "`var` is function-scoped, so all three timeouts share the same `i`. By the time they fire, the loop is done and `i === 3`.", flashcard_back: "`3 3 3` — the `var` binding is shared across all three callbacks.", recap_answer: "`3 3 3` — `var` is function-scoped.", choices: [ @@ -212,7 +219,8 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "Equality", prompt: "Which of these comparisons returns `true`?", - explanation: "`==` performs type coercion. `'5' == 5` becomes `5 == 5`. The others use `===` which compares both type and value.", + explanation: + "`==` performs type coercion. `'5' == 5` becomes `5 == 5`. The others use `===` which compares both type and value.", flashcard_back: "`'5' == 5` — `==` coerces the string to a number before comparing.", recap_answer: "`'5' == 5` is `true` because `==` coerces.", choices: [ @@ -225,7 +233,8 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "Hoisting", prompt: "What is logged?\n\n```js\nconsole.log(typeof foo);\nvar foo = 'hello';\n```", - explanation: "`var` declarations are hoisted (initialized to `undefined`), so `typeof foo` is `'undefined'` — not a ReferenceError.", + explanation: + "`var` declarations are hoisted (initialized to `undefined`), so `typeof foo` is `'undefined'` — not a ReferenceError.", flashcard_back: "`'undefined'` — the declaration is hoisted but the assignment is not.", recap_answer: "`'undefined'` because `var foo` is hoisted.", choices: [ @@ -238,7 +247,8 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "this binding", prompt: "Which call pattern makes `this` refer to the global object (in non-strict mode)?", - explanation: "A bare function call like `fn()` binds `this` to the global object in non-strict mode (or `undefined` in strict mode).", + explanation: + "A bare function call like `fn()` binds `this` to the global object in non-strict mode (or `undefined` in strict mode).", flashcard_back: "Bare invocation `fn()` — `this` is the global object in non-strict mode.", recap_answer: "`fn()` invoked as a bare function — `this` is the global object.", choices: [ @@ -263,8 +273,10 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ }, { category: "Promises", - prompt: "What does this log?\n\n```js\nPromise.resolve(1)\n .then(v => v + 1)\n .then(v => { throw v; })\n .catch(v => console.log(v));\n```", - explanation: "The first `.then` produces `2`. The second throws `2`. `.catch` receives the thrown value.", + prompt: + "What does this log?\n\n```js\nPromise.resolve(1)\n .then(v => v + 1)\n .then(v => { throw v; })\n .catch(v => console.log(v));\n```", + explanation: + "The first `.then` produces `2`. The second throws `2`. `.catch` receives the thrown value.", flashcard_back: "`2` — the catch handler receives the value thrown by the previous `.then`.", recap_answer: "`2`.", choices: [ @@ -290,7 +302,8 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "Numbers", prompt: "Which expression equals `true`?", - explanation: "`NaN` is the only JavaScript value not equal to itself. `Number.isNaN(NaN)` is the safe check.", + explanation: + "`NaN` is the only JavaScript value not equal to itself. `Number.isNaN(NaN)` is the safe check.", flashcard_back: "`Number.isNaN(NaN)` — only `NaN !== NaN`.", recap_answer: "`Number.isNaN(NaN)` — `NaN` is the only value not equal to itself.", choices: [ @@ -302,8 +315,10 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ }, { category: "Async", - prompt: "In what order do these run?\n\n```js\nconsole.log('A');\nsetTimeout(() => console.log('B'), 0);\nPromise.resolve().then(() => console.log('C'));\nconsole.log('D');\n```", - explanation: "Synchronous code runs first (`A`, `D`). Microtasks (Promises) run before macrotasks (setTimeout) — so `C` before `B`.", + prompt: + "In what order do these run?\n\n```js\nconsole.log('A');\nsetTimeout(() => console.log('B'), 0);\nPromise.resolve().then(() => console.log('C'));\nconsole.log('D');\n```", + explanation: + "Synchronous code runs first (`A`, `D`). Microtasks (Promises) run before macrotasks (setTimeout) — so `C` before `B`.", flashcard_back: "`A D C B` — microtasks beat macrotasks.", recap_answer: "`A D C B`.", choices: [ @@ -316,7 +331,8 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "Destructuring", prompt: "What is `a` after `const { x: a = 5 } = { x: undefined }`?", - explanation: "Default values apply when the property is `undefined` — but not when it's `null`.", + explanation: + "Default values apply when the property is `undefined` — but not when it's `null`.", flashcard_back: "`5` — defaults apply for `undefined`.", recap_answer: "`5`.", choices: [ @@ -342,7 +358,8 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "Scope", prompt: "Which keyword creates a block-scoped binding that cannot be reassigned?", - explanation: "`const` is block-scoped and prevents reassignment of the binding (though objects it references are still mutable).", + explanation: + "`const` is block-scoped and prevents reassignment of the binding (though objects it references are still mutable).", flashcard_back: "`const` — block-scoped, no reassignment.", recap_answer: "`const`.", choices: [ @@ -355,7 +372,8 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "Iteration", prompt: "Which loop iterates over the values of an iterable like an Array or a Set?", - explanation: "`for...of` iterates values. `for...in` iterates enumerable keys, which is rarely what you want for arrays.", + explanation: + "`for...of` iterates values. `for...in` iterates enumerable keys, which is rarely what you want for arrays.", flashcard_back: "`for...of` — iterates values of an iterable.", recap_answer: "`for...of`.", choices: [ @@ -368,7 +386,8 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "Optional chaining", prompt: "What does `obj?.foo?.bar` evaluate to when `obj.foo` is `null`?", - explanation: "Optional chaining short-circuits to `undefined` when the value before `?.` is nullish.", + explanation: + "Optional chaining short-circuits to `undefined` when the value before `?.` is nullish.", flashcard_back: "`undefined` — optional chaining short-circuits on nullish.", recap_answer: "`undefined`.", choices: [ @@ -420,7 +439,8 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ { category: "Strings", prompt: "Which method returns a new string with leading and trailing whitespace removed?", - explanation: "`String.prototype.trim()` removes whitespace from both ends. `trimStart` / `trimEnd` only trim one side.", + explanation: + "`String.prototype.trim()` removes whitespace from both ends. `trimStart` / `trimEnd` only trim one side.", flashcard_back: "`trim()` — removes whitespace from both ends.", recap_answer: "`trim()`.", choices: [ @@ -445,8 +465,10 @@ const JS_QUESTION_SEEDS: JSQuestion[] = [ }, { category: "Objects", - prompt: "How do you check whether an object has its own property named `'foo'` (not inherited)?", - explanation: "`Object.hasOwn(obj, 'foo')` is the modern, safe form — it works even on `Object.create(null)` objects.", + prompt: + "How do you check whether an object has its own property named `'foo'` (not inherited)?", + explanation: + "`Object.hasOwn(obj, 'foo')` is the modern, safe form — it works even on `Object.create(null)` objects.", flashcard_back: "`Object.hasOwn(obj, 'foo')` — modern, prototype-safe.", recap_answer: "`Object.hasOwn(obj, 'foo')`.", choices: [ @@ -505,7 +527,7 @@ await context.addInitScript( { key: `sb-${PROJECT_REF}-auth-token`, session: FAKE_SESSION, - } + }, ); const page = await context.newPage(); diff --git a/src/App.tsx b/src/App.tsx index fd00716..0afe132 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,16 +29,10 @@ export function App() { }, [bootstrap]); const needsOnboarding = - status === "authenticated" && - !!user && - user.user_metadata?.onboarded !== true; + status === "authenticated" && !!user && user.user_metadata?.onboarded !== true; if (status === "loading") { - return ( -
- Loading… -
- ); + return
Loading…
; } return ( @@ -48,67 +42,67 @@ export function App() {
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {(params) => ( + + + + + + + + + + + + + + + + + + + + + + - + - )} - - - {(params) => ( + + - + - )} - - - {(params) => ( + + - + - )} - - -
Not found
-
-
+ + + {(params) => ( + + + + )} + + + {(params) => ( + + + + )} + + + {(params) => ( + + + + )} + + +
Not found
+
+