diff --git a/.beads/.gitignore b/.beads/.gitignore index eb82c48..09f2832 100644 --- a/.beads/.gitignore +++ b/.beads/.gitignore @@ -1,5 +1,6 @@ # Dolt database (managed by Dolt, not git) dolt/ +embeddeddolt/ # Runtime files bd.sock @@ -14,6 +15,11 @@ daemon.* # Interactions log (runtime, not versioned) interactions.jsonl +# Auto-exported issues snapshot (privacy: avoid leaking issue contents +# to public remotes/forks). Disable the export with: +# bd config set export.auto false +issues.jsonl + # Push state (runtime, per-machine) push-state.json diff --git a/.beads/config.yaml b/.beads/config.yaml index c5ac76d..876b1d7 100644 --- a/.beads/config.yaml +++ b/.beads/config.yaml @@ -1,3 +1,58 @@ -issue_prefix: aw -issue-prefix: aw- -dolt.auto-start: false +# Beads Configuration File +# This file configures default behavior for all bd commands in this repository +# All settings can also be set via environment variables (BD_* prefix) +# or overridden with command-line flags + +# Issue prefix for this repository (used by bd init) +# If not set, bd init will auto-detect from directory name +# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. +# issue-prefix: "" + +# Use no-db mode: JSONL-only, no Dolt database +# When true, bd will use .beads/issues.jsonl as the source of truth +# no-db: false + +# Enable JSON output by default +# json: false + +# Feedback title formatting for mutating commands (create/update/close/dep/edit) +# 0 = hide titles, N > 0 = truncate to N characters +# output: +# title-length: 255 + +# Default actor for audit trails (overridden by BEADS_ACTOR or --actor) +# actor: "" + +# Export events (audit trail) to .beads/events.jsonl on each flush/sync +# When enabled, new events are appended incrementally using a high-water mark. +# Use 'bd export --events' to trigger manually regardless of this setting. +# events-export: false + +# Multi-repo configuration (experimental - bd-307) +# Allows hydrating from multiple repositories and routing writes to the correct database +# repos: +# primary: "." # Primary repo (where this database lives) +# additional: # Additional repos to hydrate from (read-only) +# - ~/beads-planning # Personal planning repo +# - ~/work-planning # Work planning repo + +# JSONL backup (periodic export for off-machine recovery) +# Auto-enabled when a git remote exists. Override explicitly: +# backup: +# enabled: false # Disable auto-backup entirely +# interval: 15m # Minimum time between auto-exports +# git-push: false # Disable git push (export locally only) +# git-repo: "" # Separate git repo for backups (default: project repo) + +# Integration settings (access with 'bd config get/set') +# These are stored in the database, not in this file: +# - jira.url +# - jira.project +# - linear.url +# - linear.api-key +# - github.org +# - github.repo + +sync.remote: "git+ssh://git@github.com/bryanhirsch/acme-widgets-private.git" + +export.auto: false \ No newline at end of file diff --git a/.beads/hooks/post-checkout b/.beads/hooks/post-checkout index 67ad327..d485872 100755 --- a/.beads/hooks/post-checkout +++ b/.beads/hooks/post-checkout @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# --- BEGIN BEADS INTEGRATION v1.0.3 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.0 --- +# --- END BEADS INTEGRATION v1.0.3 --- diff --git a/.beads/hooks/post-merge b/.beads/hooks/post-merge index a731aec..5aa3315 100755 --- a/.beads/hooks/post-merge +++ b/.beads/hooks/post-merge @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# --- BEGIN BEADS INTEGRATION v1.0.3 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.0 --- +# --- END BEADS INTEGRATION v1.0.3 --- diff --git a/.beads/hooks/pre-commit b/.beads/hooks/pre-commit index 02cf2ac..d7ac3d9 100755 --- a/.beads/hooks/pre-commit +++ b/.beads/hooks/pre-commit @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# --- BEGIN BEADS INTEGRATION v1.0.3 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.0 --- +# --- END BEADS INTEGRATION v1.0.3 --- diff --git a/.beads/hooks/pre-push b/.beads/hooks/pre-push index 7918492..5af9e7b 100755 --- a/.beads/hooks/pre-push +++ b/.beads/hooks/pre-push @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# --- BEGIN BEADS INTEGRATION v1.0.3 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.0 --- +# --- END BEADS INTEGRATION v1.0.3 --- diff --git a/.beads/hooks/prepare-commit-msg b/.beads/hooks/prepare-commit-msg index c0c3ce1..f0aec3c 100755 --- a/.beads/hooks/prepare-commit-msg +++ b/.beads/hooks/prepare-commit-msg @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# --- BEGIN BEADS INTEGRATION v1.0.3 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.0 --- +# --- END BEADS INTEGRATION v1.0.3 --- diff --git a/.beads/metadata.json b/.beads/metadata.json index 8dcb987..7cf5350 100644 --- a/.beads/metadata.json +++ b/.beads/metadata.json @@ -2,8 +2,6 @@ "database": "dolt", "backend": "dolt", "dolt_mode": "server", - "dolt_database": "aw", - "project_id": "d93c817c-4c12-4c7e-9582-9110a949bfd9", - "dolt_host": "127.0.0.1", - "dolt_user": "root" -} + "dolt_database": "acme_widgets_private", + "project_id": "d93c817c-4c12-4c7e-9582-9110a949bfd9" +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a3fc3ec --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + + env: + # Hardcoded; bump in lockstep with @playwright/test in package.json. + # Cache is invalidated on version change. + PLAYWRIGHT_VERSION: 1.59.1 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.11 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bun run typecheck + + - name: Build + run: bun run build + + - name: Vitest + run: bun run test + + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ env.PLAYWRIGHT_VERSION }} + + - name: Install Playwright browsers + run: bunx playwright install --with-deps chromium + + - name: Playwright + run: bun run test:e2e + + - name: Upload Playwright report on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Upload Playwright test-results on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-test-results + path: test-results/ + retention-days: 7 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..87a7bf0 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,51 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.11 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build with Pages base path + env: + VITE_BASE: /acme-widgets/ + run: bun run build + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 58af901..ce29958 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,51 @@ # Acme Widgets -This repo is used to create an example website for Software Factory Intensive training. +Example website for Software Factory Intensive training. Live at +****. -The training guide, including guidance on setup for all the different components used in the training including this repo, can be found in the Google Doc here: -https://docs.google.com/document/d/1zZhUPXKH-U3Z0FOqGXTU4hbP6o43WA0vnIU1bA_9jWM/edit?tab=t.0 +The training guide — including setup for the components around this repo — +lives in the Google Doc: +. + +## Quick Start + +Prerequisites: [Bun](https://bun.sh) `>=1.2`. + +```bash +git clone git@github.com:bryanhirsch/acme-widgets.git +cd acme-widgets +bun install +bun run dev # http://localhost:5173 +``` + +Other useful scripts: + +```bash +bun run typecheck # tsc --noEmit +bun run build # production build to dist/ (also writes 404.html) +bun run preview # serve the built output on http://localhost:4173 +bun run test # Vitest, one shot +bun run test:watch # Vitest, watch mode +bun run test:e2e # Playwright (chromium); spins up `bun run preview` +``` + +## Stack + +TypeScript + React 19 + Vite 6 + Tailwind v4 + React Router v7, +hosted on GitHub Pages, CI on GitHub Actions. See +[`docs/current/STACK.md`](docs/current/STACK.md) for as-built versions +and conventions, and +[`docs/decision-records/0001.ADR.TECH_STACK.md`](docs/decision-records/0001.ADR.TECH_STACK.md) +for the decision record. + +## Repo layout + +- [`docs/current/`](docs/current) — what is true on `main` right now. +- [`docs/decision-records/`](docs/decision-records) — frozen ADRs. +- [`docs/future/`](docs/future) — proposed changes drafted as ADRs. +- `src/` — application source. `pages/` contains the three routes; + `components/` contains shared chrome and the 15 widget presets; + `lib/widgets.ts` is the CSV parser + cache. +- `e2e/` — Playwright specs. +- `public/data/widgets.csv` — the catalog. Edit this to change the + inventory; no rebuild required for content changes outside dev. diff --git a/bun.lock b/bun.lock index 84ec044..f885144 100644 --- a/bun.lock +++ b/bun.lock @@ -5,19 +5,41 @@ "": { "name": "acme-widgets", "dependencies": { + "@heroicons/react": "^2.2.0", + "papaparse": "^5.5.3", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-router": "^7.14.2", }, "devDependencies": { + "@playwright/test": "^1.59.1", + "@tailwindcss/vite": "^4.2.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/papaparse": "^5.5.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^29.1.0", + "tailwindcss": "^4.2.4", "typescript": "^5.7.0", "vite": "^6.0.0", + "vitest": "^4.1.5", }, }, }, "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="], + + "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], @@ -50,12 +72,28 @@ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.2.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.0", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.3", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -108,6 +146,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="], + + "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="], + "@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=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -118,6 +160,8 @@ "@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=="], + "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], @@ -170,6 +214,48 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="], + + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -178,45 +264,149 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@types/papaparse": ["@types/papaparse@5.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@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/expect": ["@vitest/expect@4.1.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.5", "", { "dependencies": { "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.5", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g=="], + + "@vitest/runner": ["@vitest/runner@4.1.5", "", { "dependencies": { "@vitest/utils": "4.1.5", "pathe": "^2.0.3" } }, "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ=="], + + "@vitest/spy": ["@vitest/spy@4.1.5", "", {}, "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ=="], + + "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.24", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "electron-to-chromium": ["electron-to-chromium@1.5.345", "", {}, "sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg=="], + "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], + + "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], + + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "jsdom": ["jsdom@29.1.0", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-YNUc7fB9QuvSSQWfrH0xF+TyABkxUwx8sswgIDaCrw4Hol8BghdZDkITtZheRJeMtzWlnTfsM3bBBusRvpO1wg=="], + "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=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -224,34 +414,130 @@ "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "papaparse": ["papaparse@5.5.3", "", {}, "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="], + + "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], + + "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], + "postcss": ["postcss@8.5.12", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA=="], + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "react-router": ["react-router@7.14.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "tldts": ["tldts@7.0.29", "", { "dependencies": { "tldts-core": "^7.0.29" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg=="], + + "tldts-core": ["tldts-core@7.0.29", "", {}, "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q=="], + + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "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=="], "vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], + "vitest": ["vitest@4.1.5", "", { "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", "@vitest/pretty-format": "4.1.5", "@vitest/runner": "4.1.5", "@vitest/snapshot": "4.1.5", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.5", "@vitest/browser-preview": "4.1.5", "@vitest/browser-webdriverio": "4.1.5", "@vitest/coverage-istanbul": "4.1.5", "@vitest/coverage-v8": "4.1.5", "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], + + "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=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], } } diff --git a/docs/current/BEADS.md b/docs/current/BEADS.md new file mode 100644 index 0000000..b614b37 --- /dev/null +++ b/docs/current/BEADS.md @@ -0,0 +1,89 @@ +# Beads setup for this repo + +Beads (`bd`) is the issue tracker used by this project. This document covers **how to set up beads on a new clone** — see `.beads/README.md` and `bd prime` for everyday usage. + +## Three setup modes + +Beads supports three storage layouts. Pick whichever fits how you work — they all read and write the same issues, and **different developers on the same repo can use different modes** (see "Per-developer override" below). + +The committed `.beads/metadata.json` records the team default. If you cloned this repo and that default works for you, just run `bd bootstrap`. If you want a different local layout, set the override in `.beads/.env` (gitignored) before running `bd bootstrap`. + +| Mode | Where data lives | Concurrency | When to pick it | +|---|---|---|---| +| **1. Embedded (default)** | `.beads/embeddeddolt/` (per project) | Single writer | One project, one developer, no coordination needed | +| **2. Stand-alone server** | `~/.beads/shared-server/dolt/` | Multi-writer | One project on this machine, but you want concurrent agents/processes (e.g. multiple Claude sessions) | +| **3. GC rig (shared server)** | `~/.beads/shared-server/dolt/` (joins existing) | Multi-writer | This is one of several beads-tracked projects on your machine — they all share one local dolt server | + +Modes 2 and 3 use the same `bd` command and the same `~/.beads/shared-server/` location — the only difference is whether the shared server already exists on your machine. `bd init --shared-server` and `bd bootstrap` both auto-detect and do the right thing. + +## Mode 1: Stand-alone with embedded dolt + +**Starting fresh (this repo doesn't have beads yet):** +```bash +bd init +``` +Creates `.beads/` with `metadata.json` showing `"dolt_mode": "embedded"`, plus the embedded database under `.beads/embeddeddolt/`. + +**Cloning a repo that already uses beads:** +```bash +bd bootstrap +``` +Reads the committed `metadata.json`, sets up your local `.beads/embeddeddolt/` from the repo's tracked state. Never destroys data — safe to run on first clone or to repair a broken setup. + +## Mode 2: Stand-alone with shared dolt server + +**Starting fresh:** +```bash +bd init --shared-server +``` +Starts (or reuses) the dolt sql-server at `~/.beads/shared-server/`, registers this project there, and writes `metadata.json` with the shared-server config. + +**Cloning a repo that already uses beads:** +```bash +bd bootstrap +``` +If the committed `metadata.json` says shared-server, `bd bootstrap` will start your local shared server (if not already running) and register this project. If the committed `metadata.json` says embedded but you want shared-server locally, see "Per-developer override" below before running `bootstrap`. + +## Mode 3: GC rig — joining an existing shared server + +This is the same setup as Mode 2 — the only thing that changes is that `~/.beads/shared-server/` already exists from another project on your machine. `bd init --shared-server` and `bd bootstrap` both detect the running server and connect to it; you don't pass any extra flags. + +**To verify your rig is healthy after adding this project:** +```bash +bd dolt status # should show the shared server is running +bd dolt show # shows host/port/database for this project +ls ~/.beads/shared-server/ +``` + +If the shared server isn't running yet, `bd dolt start` will bring it up. + +## Per-developer override (`.beads/.env`) + +`metadata.json` is committed and represents the team default. To use a different mode locally **without** changing what's committed, create `.beads/.env` (already gitignored). bd loads it on every command. + +**Switch from embedded → shared-server locally:** +```bash +# .beads/.env +BEADS_DOLT_SERVER_MODE=1 +BEADS_DOLT_SERVER_HOST=127.0.0.1 +BEADS_DOLT_SERVER_PORT=3307 # or whatever your shared server is on +BEADS_DOLT_SERVER_USER=root +# BEADS_DOLT_PASSWORD=... # only if you set one +``` + +**Override just the port** (e.g. another service is squatting on 3307): +```bash +# .beads/.env +BEADS_DOLT_SERVER_PORT=3317 +``` + +The committed `metadata.json` stays untouched — other developers see the team default; you see your override. Confirm with `bd dolt show` (server modes only; embedded mode rejects this command). + +## What's committed vs. local-only + +Already configured by `.beads/.gitignore` — you don't need to change anything: + +- **Committed:** `metadata.json`, `config.yaml`, `.beads/.gitignore`, `README.md` +- **Gitignored (per-machine):** `.beads/.env`, `embeddeddolt/`, `dolt/`, `dolt-server.port`, `dolt-server.pid`, `dolt-server.log`, `*.lock`, `push-state.json`, `last-touched`, `interactions.jsonl` + +Port numbers are never shared — bd writes the live port to `dolt-server.port` at runtime, which is gitignored. diff --git a/docs/current/DOCUMENTATION.md b/docs/current/DOCUMENTATION.md new file mode 100644 index 0000000..fb90728 --- /dev/null +++ b/docs/current/DOCUMENTATION.md @@ -0,0 +1,68 @@ +# Documentation Standard + +In a mature software factory, agents read and write the code; humans read and +write plain English. Agents do code review; humans review functionality and +the plain-English descriptions in `docs/`. This document defines how those +plain-English documents are organized in this repository. + +## Directory structure + +``` +docs/ + current/ # Authoritative docs for current state of code on the default branch (main) + future/ # Proposed changes, drafted as ADRs awaiting approval + roadmap → future # Symlink for anyone looking for "the roadmap" + decision-records/ # ADRs in flight or landed, moved here on a branch where the ADR is being implemented + human-review-logs/ # Per-PR checklists for human functional review +``` + +## Lifecycle of an architecture decision + +1. **Draft.** A proposed change is written as an ADR in `docs/future/`, + numbered sequentially (e.g. `0001.ADR.TECH_STACK.md`). +2. **Approve & branch.** After approval if/when the work gets prioritized + the ADR is moved from `docs/future/` + to `docs/decision-records/` on the branch created to implement it. +3. **Evolve during implementation.** If the architecture evolves before + merge, update the ADR in place in `docs/decision-records/` so the PR + reflects what was actually built. +4. **Merge.** After merge, the ADR in `docs/decision-records/` is a frozen + historical record — a decision made at a point in time, in a particular + context. It does not change again. New decisions get new ADRs. +5. **Reflect current state.** The same PR updates `docs/current/` to describe + the new state of the codebase. + +## What goes where + +| Directory | Stable? | Purpose | +|--------------------------|---------|-----------------------------------------------------------| +| `docs/current/` | Living | What is true on the default branch right now. | +| `docs/future/` | Living | What we are considering. | +| `docs/decision-records/` | Frozen upon merge to main | ADRs get moved here as part of implementation. | +| `docs/human-review-logs/`| Append | One file per PR that warrants human functional review. | + +## When to update `docs/current/` + +Any pull request that makes a meaningful change to technical architecture, +user experience, or information architecture must include corresponding +updates to `docs/current/`. If the docs and the code disagree, that is a bug +in either the code or the docs — never an acceptable steady state. + +## Human review logs + +Some changes deserve a human walkthrough before merge. For those, add a file +at `docs/human-review-logs/--.md` containing a checklist of +suggested functional reviews. Where a review item corresponds to an +automated test, reference the test so the reviewer has context. The goal is +to let a human verify, as efficiently as possible, that the change does what +it was intended to do. + +A review log is appropriate when: +- The change is user-visible and an automated test cannot fully cover the + intent (e.g. copy, layout, motion). +- The change crosses subsystem boundaries in a way a single PR diff is hard + to evaluate. +- The change is a milestone the team wants captured for posterity. + +Skip the review log for routine refactors, dependency bumps, and other +changes whose intent is fully captured by tests and the diff itself. diff --git a/docs/current/HOSTING.md b/docs/current/HOSTING.md new file mode 100644 index 0000000..b97a1d3 --- /dev/null +++ b/docs/current/HOSTING.md @@ -0,0 +1,42 @@ +# Hosting & Repo Architecture + +The acme-widgets training project is one local working tree synced to **two GitHub remotes** plus **one Dolt remote**. This split keeps the beads issue database private while letting the public site host on free GitHub Pages with free GitHub Actions CI. + +## Remotes + +| Name | URL | Carries | Why | +| --------- | ---------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `origin` | `git@github.com:bryanhirsch/acme-widgets-private.git` | Source code + `.beads/` config + Dolt branches (issue data) | Private repo holds the beads/Dolt data we don't want public | +| `public` | `git@github.com:bryanhirsch/acme-widgets.git` | Source code only (same `main` branch contents, no Dolt branches) | Public repo unlocks **free** GitHub Pages hosting + GitHub Actions CI | +| Dolt | `git+ssh://git@github.com/bryanhirsch/acme-widgets-private.git` (via `bd dolt remote`) | Dolt issue data on orphan branches inside the private repo | Issue tracker (beads) data is sensitive; never goes to the public repo | + +## Branch and push strategy + +There is **one** branch — `main` — and the same commits go to both git remotes. + +```bash +git push origin main # push source + .beads/ config tracked files (private) +git push public main # push source to public (Pages + CI run from here) +bd dolt push # push Dolt issue data to private remote on Dolt branches +``` + +The Dolt branches (`build1.0.0/nyc1`, `workshop1.0.0/nyc1`, etc.) live on the **private** remote only and are managed by `bd dolt push`/`pull`. They are not part of the `main` branch history that the public remote sees. + +## What ends up where + +- **Public remote (`public`):** the `main` branch only. No Dolt branches. The `.beads/` config files (`config.yaml`, hooks, `metadata.json`) are technically present in tree, but they contain no issue content — only project bootstrap metadata. The actual issue data lives in Dolt and never reaches the public remote. +- **Private remote (`origin`):** the `main` branch **plus** the Dolt branches. This is the source of truth for both code and issue tracking. +- **GitHub Pages:** deploys from the **public** remote. Free tier requires a public repo. +- **GitHub Actions CI:** runs on the **public** remote. Free tier for public repos. + +## Working with this setup + +- For **issue tracking** (`bd ready`, `bd close`, etc.): operate normally; `bd dolt push` syncs to the private Dolt remote. +- For **shipping code**: push to both remotes. The deploy/CI workflows live on `public/main`. +- For **fresh clones**: clone the **private** repo first (`git@github.com:bryanhirsch/acme-widgets-private.git`) to get the beads bootstrap config, run `bd bootstrap` to clone the Dolt issue data, then add the `public` remote so you can push code there too. + +## Why not one repo? + +- A purely **public** repo would expose the beads issue database (issues, decisions, planning notes) to anyone. +- A purely **private** repo would require a paid GitHub plan for Pages and Actions on private repos. +- Splitting code (public) from issue tracker data (private, in Dolt branches on the private remote) keeps everything free while keeping the project notes private. diff --git a/docs/current/INFORMATION_ARCHITECTURE.md b/docs/current/INFORMATION_ARCHITECTURE.md new file mode 100644 index 0000000..abeaad4 --- /dev/null +++ b/docs/current/INFORMATION_ARCHITECTURE.md @@ -0,0 +1,27 @@ +# Information Architecture + +The Acme Widgets site is intentionally small. There are three top-level routes, +no auth, and no nested navigation. Anyone landing on the home page should be +able to find a widget or learn how to order one within a single click. + +## Route map + +| Path | Purpose | +|------------|--------------------------------------------------------------------------------------------------| +| `/` | Home page advertising Acme Widgets — widgets for all purposes and occasions. | +| `/widgets` | Listing of every widget for sale, sourced from `public/data/widgets.csv`. | +| `/orders` | Placeholder explaining that online orders are coming soon; meanwhile, email `sales@acmewidgets.com`. | + +## Navigation + +A single top-level navigation surfaces all three routes from every page. There +is no breadcrumb, no sidebar, and no per-widget detail page. If a future +feature needs deeper nesting (e.g. `/widgets/:id`), capture the change in a +new ADR before adding it. + +## Routing implementation + +See [`0001.ADR.TECH_STACK.md`](../decision-records/0001.ADR.TECH_STACK.md) +for the choice of React Router v7 (data router). Routes are declared in a +single `routes.tsx` module so trainees can see the entire information +architecture in one file. diff --git a/docs/current/PERSONAS.md b/docs/current/PERSONAS.md new file mode 100644 index 0000000..0667cf3 --- /dev/null +++ b/docs/current/PERSONAS.md @@ -0,0 +1,41 @@ +# Personas + +Acme Widgets serves three personas. None of them have accounts in the current +system — the site is anonymous and read-only — but the personas still drive +how the catalog is organized and how copy is written. + +## Customers + +Everyone needs widgets. Customers range from kids buying a first widget to +elderly buyers replacing a long-loved one, and they shop both for personal +use and for business. The catalog must be browseable without a tutorial and +should communicate purpose and occasion clearly. + +**What customers do on the site:** browse `/widgets`, decide they want one, +follow the instructions on `/orders` to email sales. + +## Producers + +The people who make widgets and sell them through Acme Widgets. This includes +handcrafted artisanal makers, manufacturers of high-precision widgets, and +specialty producers for particular occasions and purposes. + +**What producers do on the site:** producers do not interact with the live +site directly. They submit catalog entries by editing `public/data/widgets.csv` +in a pull request. An employee reviews and merges. + +## Employees + +People who work for Acme Widgets, buying widgets from producers and selling to +customers. + +**What employees do on the site:** employees own the catalog. They review +producer PRs against `widgets.csv`, merge them, and respond to orders sent to +`sales@acmewidgets.com`. + +## Why this matters for the codebase + +Because there is no auth and no per-persona UI, persona differences show up in +**copy and catalog structure**, not in conditional rendering. When adding new +content, ask which persona it serves and whether the existing IA +(`/`, `/widgets`, `/orders`) is the right home for it. diff --git a/docs/current/STACK.md b/docs/current/STACK.md new file mode 100644 index 0000000..58e28d7 --- /dev/null +++ b/docs/current/STACK.md @@ -0,0 +1,96 @@ +# As-Built Stack + +This file is the ground truth for what the default branch actually ships. +It is the companion to [`../decision-records/0001.ADR.TECH_STACK.md`](../decision-records/0001.ADR.TECH_STACK.md): +the ADR captures the decision and its rationale; this file captures the +versions and conventions that won during implementation. + +If the code and this file disagree, that is a bug in the file. + +## Pinned versions + +| Layer | Choice | Notes | +| ---------------- | --------------------------------- | ---------------------------------------------------------- | +| Runtime / pm | Bun `>=1.2`, currently `1.3.11` | Pinned in CI to `1.3.11` via `oven-sh/setup-bun@v2`. | +| Bundler | Vite `^6` | `@vitejs/plugin-react`. | +| Language | TypeScript `^5.7` | Strict + `noUncheckedIndexedAccess`. | +| UI | React `^19` | | +| Routing | React Router `^7` (data router) | `createBrowserRouter` + ``. | +| Styling | **Tailwind CSS v4** (CSS-first) | `@tailwindcss/vite` plugin; no `tailwind.config.js`. | +| Icons | `@heroicons/react/24/{solid,outline}` | | +| CSV parsing | `papaparse` | Used for RFC 4180 quote handling. | +| Unit / component | Vitest `^4`, jsdom, testing-library | Config in `vite.config.ts`, setup in `src/test/setup.ts`. | +| E2E | Playwright `^1.59`, chromium-only | Config at repo root. webServer wraps `bun run preview`. | + +The `engines.bun` floor lives in `package.json`. The CI bun version is +pinned in `.github/workflows/ci.yml` (and `deploy.yml`) — bump both at +the same time when you upgrade the runtime. + +## Tailwind variant — v4, not v3 + +Tailwind v4 was chosen during implementation of aw-l0r.2 over v3, per +the bead's own NOTES which named v4 as the recommended default. The +practical implications: + +- **No `tailwind.config.js` and no `postcss.config.js`.** Both files + intentionally do not exist. v4 reads its theme from a `@theme` block + in CSS. +- **Single source of truth for design tokens** is `src/index.css`. The + brass / paper / jewel-tone palette and the Fraunces + Inter font + stacks live in that one `@theme` block. Reach into that block to add + or change tokens; do not introduce a JS config. +- **Plugin pipeline** is the `@tailwindcss/vite` plugin registered in + `vite.config.ts`. There is no PostCSS config in the repo. +- **`@apply` still works** inside the `@layer base` block in + `src/index.css`; we use it for the small set of base styles + (`html { @apply bg-paper text-ink … }`). + +If you ever switch back to v3, write a new ADR. Don't silently mix the +two — every preset in `src/components/widgetPresets.tsx` assumes v4 +arbitrary-value syntax (`text-[clamp(40px,6vw,72px)]`, +`bg-brass-600`, etc.). + +## `BASE_URL` convention + +The site has to work in two places: at `/` in dev and at +`/acme-widgets/` on GitHub Pages (the public remote — see +[`HOSTING.md`](HOSTING.md)). One vite build serves both, controlled +by `import.meta.env.BASE_URL`: + +- **Vite config:** `base: process.env.VITE_BASE ?? '/'` in + `vite.config.ts`. CI sets `VITE_BASE=/acme-widgets/` for the deploy + job; local dev/preview leaves it unset. +- **Router basename:** `createBrowserRouter(routes, { basename: import.meta.env.BASE_URL })` + in `src/routes.tsx`. **Do not** double-prefix in `` calls — + pass app-relative paths only (e.g. `to="/widgets"`). +- **Asset fetches:** prefix with `import.meta.env.BASE_URL`. Example: + `fetch(\`${import.meta.env.BASE_URL}data/widgets.csv\`)` in + `src/lib/widgets.ts`. `BASE_URL` already has a trailing slash; do + not double up. + +If you add a new fetch of a `public/` asset, follow the same pattern. + +## GitHub Pages SPA fallback (`404.html`) + +GitHub Pages serves `404.html` for any path it can't find on disk. +For an SPA, that means a hard refresh on `/widgets` would 404 unless +we hand it the SPA shell. The fix is one line in `package.json`: + +```json +"postbuild": "cp -f dist/index.html dist/404.html" +``` + +Bun runs lifecycle scripts, so `bun run build` produces both files +without any extra wiring. **Do not strip this.** If you change the +build pipeline, make sure `dist/404.html` continues to exist in +production. + +## Test runner split + +- **Vitest** (`bun run test`) — pure functions, hooks, components in + isolation. Excludes the `e2e/` directory. +- **Playwright** (`bun run test:e2e`) — flows that depend on routing, + real network fetches (yes, the live `widgets.csv`), or browser APIs. + +See [`TESTING.md`](TESTING.md) for the policy on when to reach for +each. diff --git a/docs/current/TESTING.md b/docs/current/TESTING.md new file mode 100644 index 0000000..18066fe --- /dev/null +++ b/docs/current/TESTING.md @@ -0,0 +1,47 @@ +# Testing + +Acme Widgets uses two test runners, each scoped to a different layer of the +application. Both run on every PR via GitHub Actions and must be green before +merge. + +## Unit & component tests — Vitest + +Vitest runs unit tests for pure functions and component tests for React +components in isolation. It uses the same Vite transform pipeline as the app +itself, so tests see the code exactly as the bundler sees it. + +- **Location:** co-located with source as `*.test.ts` / `*.test.tsx`. +- **Run locally:** `bun run test` +- **Watch mode:** `bun run test:watch` + +Use Vitest when: +- The unit under test is a pure function, hook, or component. +- The test does not need a real browser DOM, network, or routing. + +## End-to-end tests — Playwright + +Playwright drives a real headless browser against a built copy of the site. +E2E tests cover the user-visible flows defined in +[`INFORMATION_ARCHITECTURE.md`](INFORMATION_ARCHITECTURE.md): landing on `/`, +browsing `/widgets`, reading `/orders`. + +- **Location:** `e2e/` at the repo root. +- **Run locally:** `bun run test:e2e` +- **Headed (debug):** `bun run test:e2e:headed` + +Use Playwright when: +- The behavior depends on routing, real network fetches, or browser APIs. +- The test verifies a user-visible flow end to end. + +## Choosing between the two + +Default to Vitest. Reach for Playwright only when the value of the test +depends on real browser behavior. Every Playwright test costs CI time, so +each one should justify itself with a flow a unit test cannot cover. + +## What we do not test + +- Visual regression. Out of scope for the training example. +- Performance / load. The site is static and the catalog is small. +- Accessibility audits beyond what Playwright + axe-core can assert in a + regular E2E run. diff --git a/docs/decision-records/0001.ADR.TECH_STACK.md b/docs/decision-records/0001.ADR.TECH_STACK.md new file mode 100644 index 0000000..8c0bcff --- /dev/null +++ b/docs/decision-records/0001.ADR.TECH_STACK.md @@ -0,0 +1,120 @@ +# ADR 0001: Tech Stack & Hosting for Acme Widgets + +- **Status:** Accepted +- **Date:** 2026-04-29 +- **Deciders:** Bryan + +## Context + +Acme Widgets is the example website used in the Software Factories Intensive +training. It exists to give trainees a small, realistic codebase to practice +factory workflows on. That purpose drives three constraints: + +1. **Fast to set up.** A trainee should be able to clone, install, and run + locally in minutes, on a clean laptop, without account provisioning. +2. **Free to operate.** No paid services on the critical path. The repo should + stay green on a personal GitHub account at zero cost. +3. **Recognizable to working engineers.** The stack should reflect choices a + real product team might make in 2026, so lessons transfer. + +The site itself is small: a marketing home page, a static widget catalog, and +a placeholder orders page. There is no auth, no database, no payments — those +are intentionally out of scope so the example stays focused on factory +mechanics rather than product complexity. + +## Decision + +### Application stack + +- **Language:** TypeScript +- **UI framework:** React +- **Runtime / package manager:** Bun +- **Bundler:** Vite (with Bun as the package manager and script runner) +- **Routing:** React Router (data router, v7) +- **Styling:** Tailwind CSS + +### Data + +- **Widget catalog:** static CSV checked into the repo at + `public/data/widgets.csv`, fetched at runtime by the catalog page and parsed + client-side. Producers and employees update the catalog by editing the CSV + and opening a PR. +- **No backend, no database.** Orders are handled out-of-band by emailing + `sales@acmewidgets.com`. + +### Hosting & CI + +- **Hosting:** GitHub Pages (free for public repos), serving the static build + output. +- **CI:** GitHub Actions (free tier for public repos) for build, test, and + deploy-to-Pages on merge to the default branch. + +### Testing + +- **Unit / component tests:** Vitest (Jest-compatible API, runs natively under + Bun/Vite without the Node-compat overhead Jest requires). +- **End-to-end tests:** Playwright, headless in CI. + +> Note: the rough notes proposed Jest. Vitest is preferred because it shares +> Vite's transform pipeline and works with Bun without extra configuration. +> If a future ADR moves the project off Vite, revisit this choice. + +## Consequences + +**Positive** + +- Zero hosting cost; trainees can fork and deploy on their own GitHub account. +- Bun + Vite gives sub-second cold starts on local dev, keeping the inner loop + tight during exercises. +- Static CSV catalog means no database, no migrations, no seeding scripts — + exercises can focus on factory workflow rather than infrastructure. +- TypeScript + React + Tailwind matches what most working engineers use today, + so trainees recognize patterns immediately. + +**Negative / accepted trade-offs** + +- GitHub Pages serves static assets only. Any future feature requiring a + server (real orders, search, auth) will require a new ADR and likely a + hosting change. +- CSV-as-database does not scale past a few hundred rows or any concurrent + writers. That is acceptable for the training scope. +- Bun is younger than Node; some npm packages have rough edges under it. + Mitigated by keeping dependencies few and mainstream. + +## Alternatives considered + +- **Next.js on Vercel.** Rejected: requires a Vercel account, and SSR/ISR + features add concepts the training doesn't need. +- **Plain HTML + a static site generator (Astro, 11ty).** Rejected: trainees + expect React; SSGs add a meta-framework concept that distracts from factory + exercises. +- **Firebase Hosting + Firestore.** Rejected: introduces account setup, + billing risk, and a real database — all out of scope for the example. +- **Jest for unit tests.** Rejected in favor of Vitest, see Testing above. + +### Default branch + +`main`. Acme Widgets has no need for parallel major-version branches and +follows the GitHub default. + +## Open questions + +- ~~Should the CSV be parsed at build time (typed import) or fetched at + runtime (smaller bundle, easier to hot-swap)?~~ **Resolved during + implementation:** runtime fetch via `papaparse`, with the parsed list + cached in module scope so the home featured trio and the catalog page + share one network request. See `src/lib/widgets.ts`. + +## Related + +- As-built stack (versions, conventions): see + [`../current/STACK.md`](../current/STACK.md). +- Hosting & repo architecture (two GitHub remotes + one Dolt remote): see + [`../current/HOSTING.md`](../current/HOSTING.md). +- Information architecture (routes, page purposes): see + [`../current/INFORMATION_ARCHITECTURE.md`](../current/INFORMATION_ARCHITECTURE.md). +- Personas (customers, producers, employees): see + [`../current/PERSONAS.md`](../current/PERSONAS.md). +- Testing strategy: see [`../current/TESTING.md`](../current/TESTING.md). +- Documentation convention: see + [`../current/DOCUMENTATION.md`](../current/DOCUMENTATION.md). diff --git a/docs/roadmap b/docs/roadmap new file mode 120000 index 0000000..5a1521c --- /dev/null +++ b/docs/roadmap @@ -0,0 +1 @@ +future \ No newline at end of file diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts new file mode 100644 index 0000000..ab7e604 --- /dev/null +++ b/e2e/home.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test'; + +test('home page renders the hero h1 and links to the catalog', async ({ + page, +}) => { + await page.goto('/'); + + await expect( + page.getByRole('heading', { level: 1, name: /widgets for all purposes/i }), + ).toBeVisible(); + + // Primary CTA links to /widgets. Use the link role + name; the wordmark + // logo also links to /, but it doesn't match /catalog/i. + await expect( + page.getByRole('link', { name: /catalog/i }), + ).toHaveAttribute('href', '/widgets'); +}); diff --git a/e2e/orders.spec.ts b/e2e/orders.spec.ts new file mode 100644 index 0000000..776e386 --- /dev/null +++ b/e2e/orders.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test'; + +test('orders page renders an h1 and the sales mailto link', async ({ page }) => { + await page.goto('/orders'); + + // Asserting an h1 exists so removing it in source produces a useful + // failure ("expected to be visible" + the matched query). + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); + + // The literal email string must be visible somewhere in the DOM. + await expect(page.getByText('sales@acmewidgets.com')).toBeVisible(); + + // The mailto link points exactly at the bare address (no query params). + await expect( + page.getByRole('link', { name: /sales@acmewidgets\.com/ }), + ).toHaveAttribute('href', 'mailto:sales@acmewidgets.com'); +}); diff --git a/e2e/widgets.spec.ts b/e2e/widgets.spec.ts new file mode 100644 index 0000000..b0b4d78 --- /dev/null +++ b/e2e/widgets.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test'; + +test('widgets page renders cards from the live CSV with quoted-comma intact', async ({ + page, +}) => { + await page.goto('/widgets'); + + // Wait for at least one card; we look up a stable name string from the + // CSV rather than a row index so reordering doesn't break the spec. + const classicBrass = page.getByRole('article', { name: 'Classic Brass Widget' }); + await expect(classicBrass).toBeVisible(); + + // The CSV row's description has an embedded comma — verify it survived + // the parse intact (would truncate if a naive split-on-comma were used). + await expect( + classicBrass.getByText( + 'A timeless hand-polished brass widget, suitable for everyday use and treasured heirloom display.', + ), + ).toBeVisible(); +}); diff --git a/index.html b/index.html index 97b7382..5b90c10 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,12 @@ + + + Acme Widgets diff --git a/package.json b/package.json index ee964b2..b29f668 100644 --- a/package.json +++ b/package.json @@ -7,20 +7,37 @@ "bun": ">=1.2" }, "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "preview": "vite preview", - "typecheck": "tsc --noEmit" + "dev": "bun --bun vite", + "build": "bun --bun tsc -b && bun --bun vite build", + "postbuild": "cp -f dist/index.html dist/404.html", + "preview": "bun --bun vite preview", + "typecheck": "bun --bun tsc --noEmit", + "test": "bun --bun vitest run", + "test:watch": "bun --bun vitest --watch", + "test:e2e": "playwright test", + "test:e2e:headed": "playwright test --headed" }, "dependencies": { + "@heroicons/react": "^2.2.0", + "papaparse": "^5.5.3", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router": "^7.14.2" }, "devDependencies": { + "@playwright/test": "^1.59.1", + "@tailwindcss/vite": "^4.2.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/papaparse": "^5.5.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^29.1.0", + "tailwindcss": "^4.2.4", "typescript": "^5.7.0", - "vite": "^6.0.0" + "vite": "^6.0.0", + "vitest": "^4.1.5" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..267670c --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,22 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: process.env.CI + ? [['github'], ['html', { open: 'never' }]] + : 'list', + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:4173', + }, + webServer: { + command: 'bun run preview', + url: 'http://localhost:4173', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + projects: [{ name: 'chromium', use: devices['Desktop Chrome'] }], +}); diff --git a/public/data/widgets.csv b/public/data/widgets.csv new file mode 100644 index 0000000..8965793 --- /dev/null +++ b/public/data/widgets.csv @@ -0,0 +1,16 @@ +type,description,number_in_stock,price,icon,categories +Classic Brass Widget,"A timeless hand-polished brass widget, suitable for everyday use and treasured heirloom display.",42,19.99,brass-widget.svg,everyday;artisanal;gift +Precision Steel Widget,"Machined to 0.001mm tolerance for industrial assemblies and high-precision applications.",128,89.50,steel-widget.svg,industrial;precision;business +Pocket Widget Mini,"A travel-sized widget that fits in any pocket. Perfect first widget for kids and curious newcomers.",260,4.99,pocket-widget.svg,everyday;kids;starter +Birthday Sparkle Widget,"Festive widget with confetti finish, designed to make any birthday celebration sparkle.",75,12.50,sparkle-widget.svg,occasion;birthday;gift +Wedding Heirloom Widget,"Hand-engraved silver-plated widget presented in a velvet box, a keepsake for the happy couple.",18,149.00,heirloom-widget.svg,occasion;wedding;artisanal;gift +Holiday Evergreen Widget,"Seasonal widget finished in evergreen lacquer with gold accents for the winter holidays.",94,24.99,evergreen-widget.svg,occasion;holiday;gift +Heavy-Duty Industrial Widget,"Reinforced cast-iron widget rated for continuous operation in demanding factory environments.",36,219.00,industrial-widget.svg,industrial;business;heavy-duty +Artisan Walnut Widget,"Hand-turned walnut widget from a small Vermont workshop. Each piece is unique.",22,64.00,walnut-widget.svg,artisanal;gift;everyday +Replacement Standard Widget,"Direct drop-in replacement for the most common widget assembly. Keep one in the drawer.",410,7.99,standard-widget.svg,everyday;replacement;starter +Anniversary Gold Widget,"24k gold-leafed widget mounted on a polished marble base. Marks the milestones that matter.",9,389.00,gold-widget.svg,occasion;anniversary;artisanal;gift +Lab-Grade Calibration Widget,"NIST-traceable calibration widget for laboratory and metrology use, shipped with certificate.",54,142.75,lab-widget.svg,precision;business;laboratory +Kids' Rainbow Widget,"Brightly colored, BPA-free widget sized for small hands. A great first widget.",185,6.50,rainbow-widget.svg,kids;starter;gift +Graduation Commemorative Widget,"Engravable widget with mortarboard motif, a thoughtful gift for the new graduate.",47,34.99,graduation-widget.svg,occasion;graduation;gift +Workshop Bulk-Pack Widget,"Box of 25 general-purpose widgets at a working price for shops, makers, and tinkerers.",62,79.00,bulk-widget.svg,business;everyday;bulk +Vintage-Style Copper Widget,"Antiqued copper widget styled after the original 1952 Acme catalog design.",28,42.00,copper-widget.svg,artisanal;vintage;everyday diff --git a/public/icons/placeholder.svg b/public/icons/placeholder.svg new file mode 100644 index 0000000..279cca4 --- /dev/null +++ b/public/icons/placeholder.svg @@ -0,0 +1,4 @@ + diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index a27c48d..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function App() { - return ( -
-

Acme Widgets

-

Starter site bootstrap. Routing and styling arrive in later tasks.

-
- ); -} diff --git a/src/components/TopNav.test.tsx b/src/components/TopNav.test.tsx new file mode 100644 index 0000000..32adac8 --- /dev/null +++ b/src/components/TopNav.test.tsx @@ -0,0 +1,67 @@ +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createMemoryRouter, Outlet, RouterProvider } from 'react-router'; +import { describe, expect, it } from 'vitest'; +import TopNav from './TopNav'; + +function renderTopNavAtPath(initialPath: string) { + const router = createMemoryRouter( + [ + { + path: '/', + element: ( + <> + + + + ), + children: [ + { index: true, element:
Home
}, + { + path: 'widgets', + element:
Widgets
, + }, + { + path: 'orders', + element:
Orders
, + }, + ], + }, + ], + { initialEntries: [initialPath] }, + ); + return render(); +} + +describe('TopNav', () => { + it('renders three primary nav links', () => { + renderTopNavAtPath('/'); + const nav = screen.getByRole('navigation', { name: /primary/i }); + expect(within(nav).getByRole('link', { name: 'Home' })).toBeInTheDocument(); + expect(within(nav).getByRole('link', { name: 'Widgets' })).toBeInTheDocument(); + expect(within(nav).getByRole('link', { name: 'Orders' })).toBeInTheDocument(); + }); + + it('marks the active link with aria-current="page"', () => { + renderTopNavAtPath('/widgets'); + const nav = screen.getByRole('navigation', { name: /primary/i }); + expect(within(nav).getByRole('link', { name: 'Widgets' })).toHaveAttribute( + 'aria-current', + 'page', + ); + expect(within(nav).getByRole('link', { name: 'Home' })).not.toHaveAttribute( + 'aria-current', + ); + }); + + it('navigates to /widgets when the Widgets link is clicked', async () => { + const user = userEvent.setup(); + renderTopNavAtPath('/'); + expect(screen.getByTestId('home-page')).toBeInTheDocument(); + + const nav = screen.getByRole('navigation', { name: /primary/i }); + await user.click(within(nav).getByRole('link', { name: 'Widgets' })); + + expect(await screen.findByTestId('widgets-page')).toBeInTheDocument(); + }); +}); diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx new file mode 100644 index 0000000..a099c3d --- /dev/null +++ b/src/components/TopNav.tsx @@ -0,0 +1,44 @@ +import { Cog6ToothIcon } from '@heroicons/react/24/solid'; +import { Link, NavLink } from 'react-router'; + +const links = [ + { to: '/', label: 'Home', end: true }, + { to: '/widgets', label: 'Widgets' }, + { to: '/orders', label: 'Orders' }, +]; + +const linkClass = ({ isActive }: { isActive: boolean }) => + [ + 'font-sans font-medium text-[13px] sm:text-[15px] transition-colors', + isActive + ? 'text-ink border-b-2 border-brass-600' + : 'text-ink-soft hover:text-ink', + ].join(' '); + +export default function TopNav() { + return ( +
+
+ +
+
+ ); +} diff --git a/src/components/WidgetCard.tsx b/src/components/WidgetCard.tsx new file mode 100644 index 0000000..6124879 --- /dev/null +++ b/src/components/WidgetCard.tsx @@ -0,0 +1,56 @@ +import type { Widget } from '../lib/widgets'; +import { standardPreset, widgetPresets } from './widgetPresets'; + +const currency = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', +}); + +function stockMeta(n: number): { dot: string; label: string } { + if (n <= 0) return { dot: 'bg-burgundy', label: 'Backorder' }; + if (n < 10) return { dot: 'bg-burgundy', label: `Only ${n} left` }; + if (n < 50) return { dot: 'bg-brass-600', label: `${n} in stock` }; + return { dot: 'bg-evergreen', label: `${n} in stock` }; +} + +export default function WidgetCard({ widget }: { widget: Widget }) { + const preset = widgetPresets[widget.icon] ?? standardPreset; + const stock = stockMeta(widget.numberInStock); + const visibleCategories = widget.categories.slice(0, 2); + + return ( +
+ {preset.hero} +
+
+

+ {widget.type} +

+ + {currency.format(widget.price)} + +
+

+ {widget.description} +

+
+ + + {visibleCategories.length > 0 && ( + + {visibleCategories.join(' · ')} + + )} +
+
+
+ ); +} diff --git a/src/components/WidgetCardSkeleton.tsx b/src/components/WidgetCardSkeleton.tsx new file mode 100644 index 0000000..f3dc8ac --- /dev/null +++ b/src/components/WidgetCardSkeleton.tsx @@ -0,0 +1,15 @@ +export default function WidgetCardSkeleton() { + return ( +