From 49ffe72f6b304f3d98d75cfc8d53bfe1538f3536 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:17:55 +0000 Subject: [PATCH 01/21] feat: re-parent onto template-docker v1.0.0 and add Track UI dependencies - sync from craftainer/template-docker v1.0.0 (replace tier verbatim, merge tier hand-merged, manifest extended) - app.Dockerfile -> Dockerfile; devcontainer image now .devcontainer/Dockerfile; image.mk holds react's smoke values - nginx base 1.27.4 -> 1.30.6 (image-scan) - add storybook, playwright, axe, motion, fontsource Fraunces/Instrument Sans, TanStack Query; ADR 0003 --- .claude/README.md | 29 +- .claude/agents/plan-executor.md | 72 + .claude/skills/plan-creation/SKILL.md | 109 + .claude/skills/plan-execution/SKILL.md | 99 + .devcontainer/Dockerfile | 79 + .devcontainer/Dockerfile.dockerignore | 11 + .devcontainer/README.md | 24 +- .devcontainer/compose.yml | 10 +- .devcontainer/devcontainer-lock.json | 8 +- .devcontainer/devcontainer.json | 2 +- .dockerignore | 13 +- .github/CONTENTS.md | 18 +- .github/renovate.json | 64 +- .github/template-sync-manifest.yml | 88 +- .github/template-sync-state.json | 4 + .github/workflows/README.md | 58 +- .github/workflows/checks.yml | 1 + .github/workflows/moderate-bug-fix-apply.yml | 2 +- .github/workflows/moderate-bug-fix.yml | 2 +- .../moderate-feature-build-apply.yml | 2 +- .github/workflows/moderate-feature-build.yml | 2 +- .github/workflows/release.yml | 35 +- .pre-commit-config.yaml | 37 +- CLAUDE.md | 30 +- Dockerfile | 111 +- Makefile | 140 +- README.md | 18 +- app.Dockerfile | 67 - app.Dockerfile.dockerignore | 19 - docs/TEMPLATE.md | 222 +- docs/adrs/0002-same-origin-api-proxy.md | 2 +- .../0003-re-parent-onto-template-docker.md | 110 + docs/plans/README.md | 34 + docs/plans/template.md | 14 + image.mk | 23 + package.json | 9 + pnpm-lock.yaml | 2652 +++++++++++++++-- pnpm-workspace.yaml | 4 + scripts/README.md | 38 +- scripts/check-dockerfile-versions.sh | 52 +- scripts/develop.d/10-corepack.sh | 2 +- scripts/develop.sh | 24 +- scripts/image-scan.sh | 53 + scripts/image-smoke.sh | 113 + scripts/runner-setup.d/README.md | 24 + scripts/runner-setup.sh | 34 + scripts/skopeo.sh | 26 + 47 files changed, 4058 insertions(+), 532 deletions(-) create mode 100644 .claude/agents/plan-executor.md create mode 100644 .claude/skills/plan-creation/SKILL.md create mode 100644 .claude/skills/plan-execution/SKILL.md create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/Dockerfile.dockerignore create mode 100644 .github/template-sync-state.json delete mode 100644 app.Dockerfile delete mode 100644 app.Dockerfile.dockerignore create mode 100644 docs/adrs/0003-re-parent-onto-template-docker.md create mode 100644 image.mk create mode 100644 pnpm-workspace.yaml create mode 100755 scripts/image-scan.sh create mode 100755 scripts/image-smoke.sh create mode 100644 scripts/runner-setup.d/README.md create mode 100755 scripts/runner-setup.sh create mode 100755 scripts/skopeo.sh diff --git a/.claude/README.md b/.claude/README.md index b2323c2..5d5de4d 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -25,13 +25,30 @@ this directory). issues are caught and auto-fixed immediately instead of first at commit time. An instance's own slower, pre-push-only hooks stay pre-push-only — too slow to run after every edit. +- `agents/plan-executor.md` — the subagent that executes one work item + of a `docs/plans/` plan on a faster model (`model: sonnet`), under + the main session as orchestrator (see `../CLAUDE.md`, "Execute + multi-item work as an orchestrator"). It holds the executor's side of + the protocol: what a brief contains, the write-set and no-push rules, + the escalation triggers, and the report format. +- `skills/` — project skills Claude loads when a request matches their + `description` (or that you invoke as `/`): + - `plan-creation` — writing a `docs/plans/` plan and, when the user + has setup to do first, its pre-flight runbook. + - `plan-execution` — the orchestrator's side of executing a plan: + the rolling queue of `plan-executor` subagents, researched answers + to escalations, serialized lanes, gates and close-out. + - `owasp-scan` — a parallel OWASP Top Ten scan of `src/`. + + `../CLAUDE.md` points at the two plan skills, so they're used even when + a request doesn't match their descriptions word for word. - The `PreToolUse` hook on `Bash` runs `snip hook`, the [snip](https://github.com/edouard-claude/snip) token-filtering proxy: it rewrites supported commands (`git` and others; an instance's own language tooling once it's added) to return only their signal (failures, diffs, summaries) instead of full output. `snip` itself is - installed by `scripts/develop.sh`, pinned to `SNIP_VERSION` in the - Dockerfile, and checksum-verified against its published release + installed by `scripts/develop.sh`, pinned to `SNIP_VERSION` in + `../.devcontainer/Dockerfile`, and checksum-verified against its published release checksums — see "Versions and config" in `docs/TEMPLATE.md`. Chosen over the more popular [rtk](https://github.com/rtk-ai/rtk) because its filters are @@ -45,8 +62,8 @@ this directory). - `clear-thought` (`@waldzellai/clear-thought-onepointfive`) — a local stdio MCP server exposing structured reasoning/mental-model tools, launched via `npx`. `npx` needs Node.js, which is the only - reason `scripts/develop.sh` installs it (pinned via the Dockerfile's - `NODE_VERSION` ARG) — Claude Code itself doesn't need it. + reason `scripts/develop.sh` installs it (pinned via + `../.devcontainer/Dockerfile`'s `NODE_VERSION` ARG) — Claude Code itself doesn't need it. An instance adds its own stack-specific MCP servers here (database, object storage, browser automation, ...), pinned to an exact @@ -67,10 +84,10 @@ this directory). ## Removing a tool - **snip**: delete the `PreToolUse` hook entry in `settings.json` and the - `snip` install block (and `SNIP_VERSION` ARG in the Dockerfile) from + `snip` install block (and `SNIP_VERSION` ARG in `../.devcontainer/Dockerfile`) from `scripts/develop.sh`. - **context7** / **security-guidance**: delete the plugin's `enabledPlugins` entry in `settings.json`. - **clear-thought**: delete its entry from `../.mcp.json`; if nothing else needs Node.js, also remove its install block (and `NODE_VERSION` - ARG) from `../Dockerfile`/`scripts/develop.sh`. + ARG) from `../.devcontainer/Dockerfile`/`scripts/develop.sh`. diff --git a/.claude/agents/plan-executor.md b/.claude/agents/plan-executor.md new file mode 100644 index 0000000..1e6be0d --- /dev/null +++ b/.claude/agents/plan-executor.md @@ -0,0 +1,72 @@ +--- +name: plan-executor +description: Executes exactly one work item of a docs/plans/ plan, briefed by the orchestrating main session. Works only inside its brief's write set, in its own worktree, and hands back to the orchestrator instead of improvising when it hits an escalation trigger. Use one per independent work item; run independent items in parallel. +model: sonnet +--- + +You execute one work item from a plan under `docs/plans/`. The +orchestrator (the main session) wrote your brief. It owns every decision +outside that brief and talks to the user. Your job is to finish your +item and report it accurately. + +## Your brief contains + +1. **Goal:** the work item, quoted from the plan, plus any Decision log + entries that apply. Decision log entries are settled. Don't reopen + them. +2. **Write set:** the only files and directories you may create, change + or delete. Everything else is read-only to you. +3. **Interfaces:** names and signatures you must provide, or may rely on + from items running in parallel. Code against them exactly as stated, + even if the other side doesn't exist yet in your worktree. +4. **Done when:** the checks that must pass before you report `done`. + +## Rules + +- Before writing anything, read `CLAUDE.md`, then every `README.md` on + the path to each file in your write set, root first (`CLAUDE.md`, + "Before writing anything"). +- Before relying on a third-party library's API, look it up with + context7. Don't code from memory. +- Commit only to your own `wip/` branch in your worktree. Never + push, open a PR, create a repo, tag, publish, or delete a branch. +- Don't run integration tests, e2e tests or load tests unless your + brief says to. They share the devcontainer's services, and the + orchestrator runs them one at a time in its own lanes. Run lint, + type-check and unit tests for your write set yourself. +- Show real check output. "Looks done" is not verification. + +## Escalate: stop and hand back + +Stop working and report `STATUS: escalate` as soon as any of these +happens. Don't work around it: + +1. A required check still fails after two distinct fix attempts. +2. The fix needs a file outside your write set, or an interface that + differs from your brief. +3. Your brief contradicts the code, a `README.md`, an ADR or an NFR, or + is ambiguous enough that you'd be guessing. +4. A library or framework behaves differently from what your brief + assumes. +5. The only way to pass a check is a suppression comment, a lower + coverage threshold, or a weakened guardrail. +6. Anything under `.devcontainer/` would need to change. That needs a + rebuild only the user can trigger. +7. Any outward-facing or hard-to-reverse action would be needed. +8. A measurement contradicts the expected effect, or the run-to-run + spread is larger than the effect. + +The orchestrator researches the problem, decides, and resumes you with +a message. Keep your worktree as it is when you escalate. + +## Report: your final message, exactly this shape + +``` +STATUS: done | escalate +SUMMARY: <2-5 lines> +CHANGED: +CHECKS: verbatim output tail, one per check> +COMMIT: +ESCALATION: +``` diff --git a/.claude/skills/plan-creation/SKILL.md b/.claude/skills/plan-creation/SKILL.md new file mode 100644 index 0000000..c8747b0 --- /dev/null +++ b/.claude/skills/plan-creation/SKILL.md @@ -0,0 +1,109 @@ +--- +name: plan-creation +description: Write a plan under docs/plans/ (and, for plans that need user setup, its pre-flight runbook) for multi-step or multi-item work, laid out so an orchestrator can execute it with parallel plan-executor subagents. Use when the user asks for a plan, a roadmap, or how to approach a large change. +--- + +# Plan creation + +Produce a plan file under `docs/plans/` that someone else, usually a fresh +orchestrating session using the `plan-execution` skill, can execute +without coming back to ask. The plan file's format and lifecycle are in +`docs/plans/README.md` and `docs/plans/template.md`; this skill is how to +get to a good one. + +## 1. Explore before writing + +- Read the `README.md`s on the path to every directory the plan touches + (`CLAUDE.md`, "Before writing anything"), the ADRs and NFRs it builds + on, and any plan still in progress under `docs/plans/`. A new plan that + touches files an unfinished plan is still changing must say it starts + after that plan's close-out. +- For other repos the plan touches, look at their actual state (shallow + clones in the scratchpad), not their names: what they contain, whether + they're wired into template sync, what their latest releases are. +- Use a subagent for any sweep that would pull many files into context. +- Check the machine: free memory, CPU count, which toolchains this + devcontainer has. These set the concurrency limit and decide whether + executors need toolchain containers for other repos. + +## 2. Research the open questions + +- Libraries and frameworks: context7 first. +- Practices, standards, registry behavior: WebSearch/WebFetch, preferring + primary sources (official docs, RFCs, maintainers). Cite the URLs in the + plan. +- Anything outward-facing the plan will create (repo names, package names + on each registry): check it's free now, and say so in the plan. +- Registry and platform quirks that would stop an unattended run (a + registry with no pending trusted publishers needs a first-publish token; + a ruleset the agent can't apply; a permission rule that would prompt) + belong in the pre-flight runbook, found now rather than mid-run. + +## 3. Ask the user what isn't yours to decide + +Per `CLAUDE.md` ("Decide alone only where a common best practice +exists"): naming, public API shape, visual design, licensing and genuine +trade-offs go to the user. Ask them now, during planning, with +`AskUserQuestion` (2–4 options, trade-offs, your lean first and marked), +so the run doesn't stop for them later. Record each answer in the plan's +Decision log as "user decision". Decisions that can only be made after +research during execution become explicit gates in the plan instead. + +## 4. Write the plan + +Follow `docs/plans/template.md`. For a plan with more than one +independent work item, the Execution section needs everything +`docs/plans/README.md`'s "Execution section" lists, plus: + +- **Orchestrator and executor models**, and the **concurrency target**: + a rolling queue (start the next ready item whenever one finishes), with + limits inside it for memory-heavy work (e.g. compiled-language builds) + and zero parallel agents during measurements. +- **Worktrees:** `isolation: "worktree"` for this repo; for other repos, + one local clone at `.claude/worktrees/ext-` and one + `git worktree add` per item on `wip/`. +- **Toolchains:** where an executor runs another repo's checks (that + repo's own devcontainer image, joined to this devcontainer's services). +- **"Done when"** for every brief: the repo's own + `prek run --all-files --hook-stage manual`, run with new files staged + (`git add -A` first: some hooks skip untracked files). +- **Work items:** ID (unique across active plans), what it implements, + exact write set, dependencies. The first item in every repo adds all of + that repo's new dependencies and its lockfile, alone. Research items + are report-only and feed the Decision log. +- **Interfaces** between items running in parallel, as names and + signatures both sides code against. +- **Pre-assigned numbers** for ADRs, NFRs and migrations, per repo. +- **Serialized lanes** (verification, release, benchmarks, e2e). +- **User gates** table, and **standing approvals** (what the orchestrator + may do without asking: creating named repos, pushing branches, opening + PRs, triggering template releases). +- **Decision log** seeded with every decision made while planning. + +Keep the "why" short and link sources; put rationale that outlives the +plan into the ADRs the plan's items write. + +## 5. Write the pre-flight runbook (when the user has setup to do) + +If anything only the user can do must happen before kickoff (accounts, +registry publishers or tokens, org secrets, renaming repos, rebuilding the +devcontainer, local permission rules, machine resources), write +`docs/plans/-preflight.md` next to the plan: + +- **Checklist** at the top, one line per step. +- **Part 1, decisions:** each open question as tick boxes, recommended + option first, so the user answers everything once. +- **Part 2, setup:** per step, where to click or what to run (exact URLs, + field values, commands), and a **Check** line saying how to confirm it + worked. +- **Kickoff:** a fresh session, what to verify (`/agents`, `/mcp`, + `gh auth status`), and the exact message to send. +- A closing list of what the orchestrator will still come back for. + +The runbook belongs to the plan and is deleted with it. + +## 6. Verify and hand over + +Run the repo's checks on the new files and show the output. Close with a +commit message for the user (`CLAUDE.md`, "Report changes after +executing"); don't commit unless asked. diff --git a/.claude/skills/plan-execution/SKILL.md b/.claude/skills/plan-execution/SKILL.md new file mode 100644 index 0000000..ca29d1d --- /dev/null +++ b/.claude/skills/plan-execution/SKILL.md @@ -0,0 +1,99 @@ +--- +name: plan-execution +description: Execute a docs/plans/ plan, or any task that splits into several independent items, as the orchestrator - briefing plan-executor subagents in their own worktrees, answering their escalations with researched decisions, merging, verifying and running the user gates. Use when the user asks to execute or continue a plan. +--- + +# Plan execution (orchestrator) + +The main session orchestrates; it doesn't implement work items itself. +Executors are `plan-executor` subagents: their side of the protocol +(brief contents, write-set rules, escalation triggers, report format) is +in `.claude/agents/plan-executor.md` and isn't repeated here. Plan layout +is in `docs/plans/README.md`. + +## Before the first item + +1. Read the plan in full, its Decision log (settled; don't reopen) and + its pre-flight runbook if there is one. Confirm the kickoff gate (G0) + with the user unless the kickoff message grants it. +2. Note the plan's concurrency target, limits and standing approvals. + Anything outside the standing approvals is asked for individually. +3. Set up what items need: clones and worktrees for other repos, + toolchain images, the integration branch named in the plan. + +## The queue + +- Keep the plan's target number of executors running. Waves define + dependencies, not batches: whenever an executor reports, start the + next **ready** item (dependencies merged and verified, write set + disjoint from every running item), critical path first, then the item + that unblocks the most. +- Start each with the `Agent` tool, `subagent_type: "plan-executor"`, + the model the plan names, and `isolation: "worktree"` (this repo) or an + explicit worktree path (other repos), in the background. +- A brief holds: the goal quoted from the plan plus the Decision log + rows that apply, the exact write set, the interfaces to provide or rely + on, and "Done when". Nothing the executor would have to guess. +- Respect the plan's limits inside the target (e.g. one agent compiling + a heavy language at a time; no agents during a measurement window). + +## When an executor reports + +**`done`:** read the diff, not just the summary. Every changed file is +inside the write set; `CHECKS` shows real output with new files staged. +Merge into the integration branch, run the verification lane, update the +plan's Status, then start the next ready item. + +**`escalate`:** research before deciding: + +1. context7 for the library or framework involved; +2. WebSearch/WebFetch for the exact error text, the tool's issue tracker, + changelog and official docs (primary sources over blog posts); +3. this repo's ADRs, NFRs, READMEs and the plan's Decision log. + +Then: + +- **If the research converges** on a common best practice, or the plan, + an ADR or an NFR requires one option: decide, add a Decision log row + (decision, reason, source URLs), and **resume the same executor** with + `SendMessage`, quoting the row. Start a new executor only if the old + one's context is unusable. +- **If it doesn't** (sources disagree, a real trade-off, taste or + convention), it's the user's decision (`CLAUDE.md`). Ask with the + question in one sentence, 2–4 options with trade-offs, the sources and + why they don't settle it, and your lean labeled as a lean. Pause only + that item; keep the queue running. Record the answer as "user + decision". +- Scope changes, security or licensing trade-offs, deleting anything the + plan didn't create, and anything outside the standing approvals also go + to the user. +- If the same item escalates twice on the same approach, re-plan the item + (split it, change the approach, record why) instead of trying a third + variation. + +## Serialized lanes + +Run these yourself, one at a time, never in parallel executors: + +- **Verification:** after each merge, the repo's full suite including + integration and e2e (they share the devcontainer's services and ports). + On red: revert that merge on the integration branch, send the failing + output to the owning executor as an escalation-style message, and keep + the queue moving. +- **Releases**, in the plan's dependency order, each behind its gate. +- **Measurements**, only in quiet windows with no executors running. + +## Gates and the user + +- Batch same-kind gates in a wave (PR merges, releases) into one request. + Batch non-blocking questions too. +- Show evidence with every gate: check output, CI links, release runs. +- After each merged batch, add a dated progress bullet to the plan's + Status, so a fresh session can resume from the plan alone. + +## Close-out + +Final verification lane over every repo the plan touched; fold what's +worth keeping into ADRs and docs; ask for the final gate; then delete the +plan (and its runbook) per `docs/plans/README.md`, and give the user a +commit message. diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..3611eff --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,79 @@ +# syntax=docker/dockerfile:1.28 +# Single-stage build: develop (the devcontainer image). Built with the repo +# root as context (see compose.yml). The repo-root Dockerfile is reserved for +# an instance's own runtime image -- see docs/TEMPLATE.md's "Contents" and +# "Instance extension points". + +ARG DEBIAN_VERSION=trixie + +# renovate: datasource=github-releases depName=j178/prek +ARG PREK_VERSION=0.5.5 + +# renovate: datasource=npm depName=@anthropic-ai/claude-code +ARG CLAUDE_CODE_VERSION=2.1.303 + +# renovate: datasource=github-releases depName=edouard-claude/snip +ARG SNIP_VERSION=0.25.2 + +# renovate: datasource=github-releases depName=cli/cli +ARG GH_VERSION=2.102.0 + +# uv itself isn't a project dependency manager here -- it's only the +# mechanism scripts/develop.sh uses to install an exact, checksum-verified +# CPython build (see PYTHON_VERSION below) rather than an unpinned apt +# package. Left on PATH afterward; harmless, and an instance that adds +# Python as its own application runtime can reuse it directly. +# renovate: datasource=github-releases depName=astral-sh/uv +ARG UV_VERSION=0.13.1 + +# python3 is infrastructure tooling, not an application runtime this +# template assumes: prek needs an interpreter to build the venv for any +# "language: python" hook (.pre-commit-config.yaml's pre-commit-hooks +# repo), and .github/scripts/*.py need one directly. +# renovate: datasource=python-version depName=python +ARG PYTHON_VERSION=3.14.8 + +# Node.js is infrastructure tooling too, not an application runtime: npx +# (the clear-thought MCP server in .mcp.json) is the only thing that +# needs it. +# renovate: datasource=node-version depName=node +ARG NODE_VERSION=24.22.0 + +ARG SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ARG SSL_CERT_DIR=/etc/ssl/certs + +######################################## +# develop — interactive devcontainer image, based on Microsoft's generic +# base devcontainer image. No *application* language runtime is assumed +# here — an instance adds its own (Python via uv, Rust via rustup, Go, +# Node.js, OpenTofu/Ansible, ...) on top of this stage, following the same +# ARG/scripts/develop.sh pattern established below. The Python and Node.js +# this stage does install are infrastructure tooling only -- see the ARG +# comments above. +######################################## +FROM mcr.microsoft.com/devcontainers/base:${DEBIAN_VERSION} AS develop +ARG PREK_VERSION +ARG CLAUDE_CODE_VERSION +ARG SNIP_VERSION +ARG UV_VERSION +ARG PYTHON_VERSION +ARG NODE_VERSION +ARG GH_VERSION +ARG SSL_CERT_FILE +ARG SSL_CERT_DIR + +ENV SSL_CERT_FILE=${SSL_CERT_FILE} \ + SSL_CERT_DIR=${SSL_CERT_DIR} \ + REQUESTS_CA_BUNDLE=${SSL_CERT_FILE} \ + CURL_CA_BUNDLE=${SSL_CERT_FILE} + +COPY scripts/develop.sh /tmp/develop.sh +RUN bash /tmp/develop.sh "$PREK_VERSION" "$CLAUDE_CODE_VERSION" "$SNIP_VERSION" "$UV_VERSION" "$PYTHON_VERSION" "$NODE_VERSION" "$GH_VERSION" + +# Instance hook: scripts/develop.d/*.sh, see that directory's README.md. +COPY scripts/develop.d/ /tmp/develop.d/ +RUN for f in /tmp/develop.d/*.sh; do [ -e "$f" ] || continue; echo "==> $f"; bash "$f" || exit 1; done + +USER vscode +WORKDIR /workspace +CMD ["sleep", "infinity"] diff --git a/.devcontainer/Dockerfile.dockerignore b/.devcontainer/Dockerfile.dockerignore new file mode 100644 index 0000000..3aeee72 --- /dev/null +++ b/.devcontainer/Dockerfile.dockerignore @@ -0,0 +1,11 @@ +# Build context excludes for the devcontainer image (.devcontainer/Dockerfile). +# BuildKit reads .dockerignore next to the Dockerfile in place of +# the context root's .dockerignore, which stays free for an instance's own +# runtime image. +.git +.github +.devcontainer +.secrets +docs +README.md +CLAUDE.md diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 8108e60..0a1b9fb 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -3,8 +3,12 @@ - `devcontainer.json` — references `compose.yml` and `compose.instance.yml` as the devcontainer's `dockerComposeFile`, and configures the devcontainer itself (features, mounts, editor settings). -- `compose.yml` — the dev service (`myapp`), built from the top-level - `Dockerfile`'s `develop` stage. An instance that needs backing services +- `Dockerfile` — the devcontainer image (a single `develop` stage), built + with the repo root as context; `Dockerfile.dockerignore` holds its + build-context excludes. The top-level `Dockerfile` is reserved for an + instance's runtime image (`../docs/TEMPLATE.md`). +- `compose.yml` — the dev service (`myapp`), built from `Dockerfile`'s + `develop` stage. An instance that needs backing services (a database, a queue, ...) adds its own `stack/` directory (one subdirectory per service, each with its own compose fragment) and an `include:` list in `compose.instance.yml` pointing at them — see `stack/README.md`'s @@ -119,7 +123,7 @@ step there. Still worth doing once the container is back: - Run `prek run --all-files --hook-stage manual` — the same check-suite that gates CI (`checks.yml`) — to confirm whatever motivated the - rebuild (a `Dockerfile`/`scripts/develop.sh` change, a new tool) works + rebuild (a `.devcontainer/Dockerfile`/`scripts/develop.sh` change, a new tool) works end-to-end inside the container, not just that the image built. - If the change added, removed, or reordered a `mounts` entry, confirm it actually took effect — mounts apply only at container creation, not @@ -129,7 +133,7 @@ step there. Still worth doing once the container is back: picked up the new list. A plain "Reopen in Container" (no rebuild) is enough for changes outside -`.devcontainer/`, `Dockerfile`, and `scripts/develop.sh` — those only +`.devcontainer/` and `scripts/develop.sh` — those only need the window reconnected, not a fresh image. ## Do @@ -143,18 +147,18 @@ need the window reconnected, not a fresh image. - Commit `devcontainer-lock.json` in the same change as any feature edit in `devcontainer.json`. Regenerate it with `npx @devcontainers/cli upgrade --workspace-folder .`, or by - rebuilding the devcontainer. Renovate bumps feature versions but - never regenerates this file, so its feature-bump PRs fail the - `devcontainer-lock` check (and don't automerge) until someone - pushes the regenerated lockfile to the Renovate branch. + rebuilding the devcontainer. Feature versions are bumped by hand, + not by Renovate: Renovate can't regenerate this file, so + `../.github/renovate.json` turns its devcontainer feature updates + off rather than let them merge with a stale lockfile. ## Don't - Bind- or volume-mount a language runtime's dependency tree, or anything else you don't want a host antivirus/DLP tool scanning or syncing — it lives purely in the container's writable layer instead. (`claude-config` - is a deliberate, narrow exception — see `compose.yml`'s comment on that - volume.) + and `gh-config` are deliberate, narrow exceptions — small login and + session state; see `compose.yml`'s comments on those volumes.) - Add a `ports:` mapping or a `networks:` block anywhere here. - Pin a feature or image version as `latest` — pin the exact version so Renovate can bump it deliberately. diff --git a/.devcontainer/compose.yml b/.devcontainer/compose.yml index b7d7d32..98c1cad 100644 --- a/.devcontainer/compose.yml +++ b/.devcontainer/compose.yml @@ -1,5 +1,5 @@ -# Dev service: the devcontainer itself, built from the top-level -# Dockerfile's `develop` stage. The workspace is bind-mounted so source +# Dev service: the devcontainer itself, built from .devcontainer/Dockerfile's +# `develop` stage, with the repo root as build context. The workspace is bind-mounted so source # edits are picked up without a rebuild. An instance that adds backing # services (a database, a queue, ...) adds its own `include:` list to # compose.instance.yml instead, pointing at its own `stack/*/compose.yml` @@ -12,7 +12,7 @@ services: myapp: build: context: .. - dockerfile: Dockerfile + dockerfile: .devcontainer/Dockerfile target: develop volumes: - ..:/workspace:cached @@ -23,6 +23,7 @@ services: # README for its specific env var/path convention once an instance # adds one. - claude-config:/home/vscode/.claude + - gh-config:/home/vscode/.config/gh command: sleep infinity # No ports: mapping and no networks: block — see .devcontainer/ # README.md's "Don't". An instance that needs host access adds @@ -36,3 +37,6 @@ volumes: # runtime's dependency tree, so the DLP/malware-scanning concern above # doesn't apply the same way here. claude-config: + # Persists the GitHub CLI's login (`gh auth login`) across rebuilds, for + # the same reason. + gh-config: diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index 5dc5aa1..8515835 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -1,9 +1,9 @@ { "features": { - "ghcr.io/devcontainers/features/docker-in-docker:4.1.1": { - "version": "4.1.1", - "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:2b44bcb32e75d5a3c028479f7f957cf7d5e42d81761373025a8d501dd3748f88", - "integrity": "sha256:2b44bcb32e75d5a3c028479f7f957cf7d5e42d81761373025a8d501dd3748f88" + "ghcr.io/devcontainers/features/docker-in-docker:4.1.3": { + "version": "4.1.3", + "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:fae40f3c8be5a7c3b0db344c1d30341bc2c91ba38d1d5b14cd2ad72e6d65d084", + "integrity": "sha256:fae40f3c8be5a7c3b0db344c1d30341bc2c91ba38d1d5b14cd2ad72e6d65d084" } } } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e32e04f..ad24269 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "workspaceFolder": "/workspace", "shutdownAction": "stopCompose", "features": { - "ghcr.io/devcontainers/features/docker-in-docker:4.1.1": { + "ghcr.io/devcontainers/features/docker-in-docker:4.1.3": { "moby": false, "dockerDashComposeVersion": "v2" } diff --git a/.dockerignore b/.dockerignore index 1c107c1..7e60ec4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,19 @@ -# Build context excludes for `docker build` (see top-level Dockerfile). +# Build context excludes for the runtime image (top-level Dockerfile). The +# devcontainer image has its own: .devcontainer/Dockerfile.dockerignore. .git .github .devcontainer .secrets +.vscode +.claude docs README.md CLAUDE.md -# Node (app runtime): the devcontainer build never needs these, and -# node_modules alone would bloat its build context. app.Dockerfile's own -# build uses app.Dockerfile.dockerignore instead. +# Node (app runtime): the builder stage runs its own `pnpm install` / +# `pnpm build`; without these, `COPY . .` (after that install) would overwrite +# the container's node_modules with the host's, which may not match its +# platform/arch. node_modules +coverage dist diff --git a/.github/CONTENTS.md b/.github/CONTENTS.md index 8095b82..ae77239 100644 --- a/.github/CONTENTS.md +++ b/.github/CONTENTS.md @@ -5,14 +5,22 @@ `README.md`. - `renovate.json` — weekly update PRs for GitHub Actions, every Dockerfile/compose image tag repo-wide (scanned recursively, unlike - Dependabot), and the `# renovate:` comment-annotated version pins in the - Dockerfile. An instance's own `renovate.json` additions cover its + Dependabot), and the `# renovate:` comment-annotated version pins in + `.devcontainer/Dockerfile` (and an instance's top-level runtime + `Dockerfile`). `Makefile` and `image.mk` `*_VERSION` pins carry the same + marker; the hadolint image in `.pre-commit-config.yaml`, the Trivy + image in `scripts/image-scan.sh` and the skopeo image in the `Makefile` + (the last two by tag and digest) have their own regex managers. An instance's own `renovate.json` additions cover its language's own package manifest (e.g. `pyproject.toml`, `Cargo.toml`, `package.json`). All patch/minor updates are grouped into a single weekly PR that automerges once required checks pass; all major updates - are grouped into a separate PR for manual review. Devcontainer feature - bumps are the exception: they need a manual lockfile regeneration - first (see `../.devcontainer/README.md`). + are grouped into a separate PR for manual review. A Python minor + upgrade (e.g. 3.14 to 3.15) also gets its own PR for manual review: + it passes this repo's checks, but for an instance running Python as + its app runtime it's a runtime upgrade (its runtime `Dockerfile` must + match, and its tools must support the new version). Devcontainer + features are left out: Renovate can't regenerate their lockfile, so + they're bumped by hand (see `../.devcontainer/README.md`). - `ISSUE_TEMPLATE/` — issue forms (bug report, feature request) shown when opening a new issue. - `PULL_REQUEST_TEMPLATE.md` — prefills the description box for new PRs. diff --git a/.github/renovate.json b/.github/renovate.json index 64ef019..ec75634 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -11,13 +11,28 @@ { "matchUpdateTypes": ["major"], "groupName": "major updates" + }, + { + "description": "A Python minor (3.14 -> 3.15) is a runtime upgrade for every instance that runs Python as its app runtime: its own runtime Dockerfile must move in step, and its tools must support the new version. Keep it out of the automerged group, as its own PR for manual review.", + "matchDatasources": ["python-version"], + "matchDepNames": ["python"], + "matchUpdateTypes": ["minor"], + "groupName": "python minor upgrade", + "automerge": false + }, + { + "description": "Renovate can't regenerate .devcontainer/devcontainer-lock.json, so features are bumped by hand together with it (see .devcontainer/README.md).", + "matchManagers": ["devcontainer"], + "matchDepTypes": ["feature"], + "enabled": false } ], "customManagers": [ { "customType": "regex", "managerFilePatterns": [ - "/^(app\\.)?Dockerfile$/" + "/^Dockerfile$/", + "/^\\.devcontainer/Dockerfile$/" ], "matchStrings": [ "# renovate: datasource=(?.*?) depName=(?.*?)\\s*\\nARG \\w+_VERSION=(?.*)\\s" @@ -50,6 +65,53 @@ "matchStrings": [ "# renovate: datasource=(?.*?) depName=(?.*?)\\s*\\n\\s*(?:runs-on|runner): \\$\\{\\{ vars\\.[A-Z0-9_]+ \\|\\| '(?[^']+)' \\}\\}" ] + }, + { + "description": "Pinned tool versions in the image-release Makefile and image.mk.", + "customType": "regex", + "managerFilePatterns": [ + "/^Makefile$/", + "/^image\\.mk$/" + ], + "matchStrings": [ + "# renovate: datasource=(?.*?) depName=(?.*?)\\s*\\n[A-Z0-9_]+_VERSION\\s*:?=\\s*(?\\S+)" + ] + }, + { + "description": "The skopeo image pinned by tag and digest in the Makefile; Renovate updates both.", + "customType": "regex", + "managerFilePatterns": [ + "/^Makefile$/" + ], + "matchStrings": [ + "SKOPEO_IMAGE := quay\\.io/skopeo/stable:(?v[0-9][^@\\s]*)@(?sha256:[a-f0-9]{64})" + ], + "datasourceTemplate": "docker", + "depNameTemplate": "quay.io/skopeo/stable" + }, + { + "description": "The hadolint image pinned in the hadolint hook.", + "customType": "regex", + "managerFilePatterns": [ + "/^\\.pre-commit-config\\.yaml$/" + ], + "matchStrings": [ + "entry: hadolint/hadolint:(?\\S+) hadolint" + ], + "datasourceTemplate": "docker", + "depNameTemplate": "hadolint/hadolint" + }, + { + "description": "The Trivy image pinned by tag and digest in scripts/image-scan.sh; Renovate updates both.", + "customType": "regex", + "managerFilePatterns": [ + "/^scripts/image-scan\\.sh$/" + ], + "matchStrings": [ + "trivy=aquasec/trivy:(?[0-9][^@\\s]*)@(?sha256:[a-f0-9]{64})" + ], + "datasourceTemplate": "docker", + "depNameTemplate": "aquasec/trivy" } ] } diff --git a/.github/template-sync-manifest.yml b/.github/template-sync-manifest.yml index cf5f88f..b4d1186 100644 --- a/.github/template-sync-manifest.yml +++ b/.github/template-sync-manifest.yml @@ -10,18 +10,16 @@ # wants a template-owned path gone for good should move its pattern to # "ignore" in a template release rather than deleting the file locally. # -# This is template-react's own manifest, classifying its full tracked-file -# set for its instances (paralleling template-fastapi's for the backend). -# template-react is itself an instance of template-base, whose manifest is -# "merge" tier, so this file keeps its React-specific entries across base -# syncs and takes base's additions by hand. It is "merge" tier here too: a -# repo that's both an instance of template-react and its own template for -# further instances edits this file directly and resolves this file's -# additions by hand. +# This started as template-docker's manifest (itself template-base's, grown): +# template-react is an instance of template-docker and its own template (the +# React app layer) for further instances, so it edits this file directly, +# covering its own (larger) tracked-file set, and resolves template-docker's +# additions by hand. Paths template-docker classifies "ignore" because an +# instance owns them (Dockerfile, image.mk, the hook scripts) are classified +# here for template-react's own instances instead. # # An instance that isn't itself a template doesn't edit this file; it lists -# its own files (e.g. extra hook scripts) in -# ../template-sync-manifest.local.yml instead -- +# its own extra files in ../template-sync-manifest.local.yml instead -- # same format, "ignore" tier only, read by the completeness check but never # by sync. # @@ -35,6 +33,7 @@ replace: # them, so a sync can overwrite them safely. - .claude/hooks/self-check.sh - .claude/skills/ + - .claude/agents/ - .devcontainer/compose.yml - .devcontainer/devcontainer-lock.json - .devcontainer/resolve-ssh-agent.sh @@ -57,7 +56,8 @@ replace: - .github/PULL_REQUEST_TEMPLATE.md # Build/dependency/hook plumbing, all instance-agnostic. - - Dockerfile + - .devcontainer/Dockerfile + - .devcontainer/Dockerfile.dockerignore - .gitattributes - scripts/develop.sh - scripts/post-create.sh @@ -79,19 +79,31 @@ replace: - docs/plans/README.md - docs/plans/template.md - # template-react's own layer on top of base: the runtime image and its - # assets, the release Makefile, the dev-service compose additions, the + # template-docker's own layer on top of base: the image-release Makefile + # (identical for every image-shipping app; instances customise it only + # through variables in image.mk), the smoke test, and the runner-setup + # driver. Hook scripts are listed one by one, not as globs: an instance's + # own scripts/runner-setup.d/*.sh are ignore tier (below). + # Same precedent as template-python-package: the Makefile implements the + # release contract identically for every instance, so it's template-owned. + - Makefile + - scripts/image-smoke.sh + - scripts/runner-setup.sh + - scripts/runner-setup.d/README.md + - scripts/check-dockerfile-versions.sh + - scripts/image-scan.sh + - scripts/skopeo.sh + + # template-react's own layer on top of docker's: the runtime image (an + # nginx-served SPA) and its assets, the dev-service compose additions, the # hook scripts, and the Vite/TypeScript/lint/format plumbing -- an # instance changing these is changing the build tooling, not its app. - # Listed one by one where base's hook directories are "ignore" for plain - # base instances -- an instance's own extra hook scripts go in its + # Listed one by one where docker's hook directories are "ignore" for plain + # docker instances -- an instance's own extra hook scripts go in its # template-sync-manifest.local.yml. - - app.Dockerfile - - app.Dockerfile.dockerignore - - Makefile + - Dockerfile - .devcontainer/compose.instance.yml - scripts/docker/ - - scripts/check-dockerfile-versions.sh - scripts/develop.d/10-corepack.sh - scripts/post-create.d/10-pnpm-install.sh - .vscode/launch.json @@ -108,6 +120,19 @@ ignore: # Each instance chooses its own license and copyright holder. - LICENSE + # Derived from package.json, which is merge-tier -- a 3-way merge of a + # lock file is unreliable, so an instance regenerates it locally with + # `pnpm install` after a sync PR changes package.json. + - pnpm-lock.yaml + + # The app itself: the generic CRUD client and Heroes example are a + # starting point an instance owns from the moment it's instantiated, so a + # sync never overwrites its code or static assets. + - src/ + + # Env-var documentation for the instance's own deployment. + - .env.example + # Product/domain knowledge and working plans: instance-owned from the # moment it's instantiated. - docs/architecture.md @@ -122,18 +147,10 @@ ignore: # Instance-owned bookkeeping, written only by the sync workflow itself. - .github/template-sync-state.json - # The app itself: the generic CRUD client and Heroes example are a - # starting point an instance owns from the moment it's instantiated, so a - # sync never overwrites its code or static assets. - - src/ - - # Env-var documentation for the instance's own deployment. - - .env.example - - # Derived from package.json, which is merge-tier -- a 3-way merge of a - # lock file is unreliable, so an instance regenerates it locally with - # `pnpm install` after a sync PR changes package.json. - - pnpm-lock.yaml + # Hook files an instance drops into runner-setup.d/. (develop.d/ and + # post-create.d/ scripts are template-react's own, listed under replace; + # an instance's extra ones go in its local manifest.) + - scripts/runner-setup.d/*.sh # An instance's own extra "ignore" patterns for paths this manifest can't # know about (see docs/TEMPLATE.md's "Template sync" section). @@ -144,6 +161,14 @@ merge: # instance; everything else in these two files is template-owned. - README.md - package.json + - pnpm-workspace.yaml + + # template-react's image.mk values (smoke port/path, release-check's unit + # tests); an instance adds its own on top. + - image.mk + + # Baseline excludes the instance extends with its own entries. + - .dockerignore # Files with no include mechanism of their own (JSON configs, prek's # .pre-commit-config.yaml), and docs an instance appends to: a three-way @@ -160,7 +185,6 @@ merge: - .pre-commit-config.yaml - .mcp.json - .gitignore - - .dockerignore - scripts/README.md - docs/TEMPLATE.md diff --git a/.github/template-sync-state.json b/.github/template-sync-state.json new file mode 100644 index 0000000..ce2bf42 --- /dev/null +++ b/.github/template-sync-state.json @@ -0,0 +1,4 @@ +{ + "template_repo": "craftainer/template-docker", + "synced_tag": "v1.0.0" +} diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 5314c03..d25cc43 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -61,7 +61,7 @@ still overrides the single-runner jobs (`perf.yml`, `template-sync.yml`, and `release.yml`'s `version`/`manifest`/`release` jobs) that aren't matrixed by arch. -The devcontainer used for local development (`Dockerfile`'s `develop` +The devcontainer used for local development (`.devcontainer/Dockerfile`'s `develop` stage) is deliberately *not* pinned to a single platform — plain `docker build`/`docker compose build` already targets whichever architecture the host is on, so an arm64 contributor gets a native @@ -164,7 +164,7 @@ branch/PR may already be gone via cleanup. **Environment.** All four Claude-invoking jobs run `claude` the same way `checks.yml` runs `prek`: inside the actual devcontainer (`develop` stage) via `devcontainers/ci`, so Claude runs the exact CLI build pinned -in `Dockerfile`'s `CLAUDE_CODE_VERSION`. Each writes the +in `.devcontainer/Dockerfile`'s `CLAUDE_CODE_VERSION`. Each writes the `ANTHROPIC_API_KEY` secret to `.secrets/claude.txt` first (matching `.secrets/README.md`'s "one file per secret" convention) and removes it in an `if: always()` step; `../scripts/run_claude.sh` reads it, sets a @@ -233,20 +233,27 @@ workflow works with no registry configured at all. When it is set: ## Image labels -Both copies of the published image (the OCI tarball and, if configured, -the registry push) carry the same label set, computed once by -`docker/metadata-action`: - -- Standard `org.opencontainers.image.title` / `.description` / `.url` / - `.source` / `.revision` (`github.sha`) / `.created` / `.version`, - derived from git/GitHub context. -- `io.github..sbom` and - `io.github..coverage-report` — URLs to that - release's SBOM and `coverage.xml` assets. Namespaced under the repo - owner (not `org.opencontainers.image.*`, which is reserved for the - spec's own keys) and derived from `github.repository_owner` alone, so - it's one stable prefix per org across every repo/template instance - that org owns, rather than a per-repo namespace to look up each time. +The image is built once, with the label set computed by the Makefile's +`LABELS` (not by `docker/metadata-action`). The registry copy is that same +tar pushed with skopeo (`make publish`), not a rebuild, so the labels and +the digest are those of the tested artifact: + +- `org.opencontainers.image.title` (the repository name), `.source` and + `.url` (the repository), `.version` (`RELEASE_VERSION`), `.revision` + (`git rev-parse HEAD`), `.created` (the commit time, not the build + time), and `.licenses` (`OCI_LICENSE`, + default `GPL-3.0-or-later`). +- `org.opencontainers.image.description`, only when the instance sets + `OCI_DESCRIPTION` in `image.mk` (the Makefile has no offline source + for the repository description). +- `io.github..sbom` — the URL of that release's SPDX + SBOM asset. Namespaced under the repo owner (not + `org.opencontainers.image.*`, which is reserved for the spec's own + keys) and derived from the repository owner alone, so it's one stable + prefix per org across every repo/template instance that org owns. +- `io.github..coverage-report`, only when the instance + sets `COVERAGE_REPORT` in `image.mk` to the release asset's name. +- Whatever the instance adds through `EXTRA_LABELS`. ## Release variables and secrets @@ -261,12 +268,31 @@ cleanly when the registry isn't configured. - `CI_RUNNER_AMD64` / `CI_RUNNER_ARM64` (variables, optional) — runner labels for each arch leg, defaulting to `ubuntu-24.04` / `ubuntu-24.04-arm`. +- `PYPI_PUBLISH` (variable, optional) — `testpypi` or `pypi`; selects the + index `make publish` uploads to via trusted publishing. Unset skips it. +- `NPM_PUBLISH` / `CARGO_PUBLISH` (variables, optional) — non-empty makes + `make publish` push to npm / crates.io; unset skips it. Passed to + `build` and, like `PYPI_PUBLISH`, forwarded to `release-check` so it + can install what publish uploaded. +- `NPM_TOKEN` / `CARGO_REGISTRY_TOKEN` (secrets, optional) — bootstrap + tokens for the first release of a package, before trusted publishing + is configured. Exposed only on the `make publish` step's `env:`, never + job-wide or to any other step. An unset secret arrives as an empty + string, so the Makefile must treat empty as absent. `NPM_TOKEN` is + deliberately not mapped to `NODE_AUTH_TOKEN`: npm's OIDC trusted + publishing breaks when that variable is set, even to an empty string, + so the npm template's Makefile decides what to do with `NPM_TOKEN`. ## Do - Give `release.yml` and `template-sync.yml` `contents: write` and nothing broader (`template-sync.yml` also needs `pull-requests: write`, for nothing else); leave `checks.yml` at `contents: read`. +- Keep `id-token: write` on `release.yml`'s `build` job only (the one + running `make publish`): PyPI, npm and crates.io trusted publishing + via GitHub OIDC. It is unused by repos that publish to none of them. +- Keep `NPM_TOKEN` and `CARGO_REGISTRY_TOKEN` on the `make publish` + step's `env:` only. ## Don't diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index cd4e8ac..45770a3 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -47,6 +47,7 @@ jobs: uses: docker/build-push-action@v7.4.0 with: context: . + file: .devcontainer/Dockerfile target: develop cache-from: type=gha,scope=${{ matrix.arch }} cache-to: type=gha,mode=max,scope=${{ matrix.arch }} diff --git a/.github/workflows/moderate-bug-fix-apply.yml b/.github/workflows/moderate-bug-fix-apply.yml index 91000d5..415c7c2 100644 --- a/.github/workflows/moderate-bug-fix-apply.yml +++ b/.github/workflows/moderate-bug-fix-apply.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@v7.0.1 - name: Download moderation result - uses: actions/download-artifact@v8.0.1 + uses: actions/download-artifact@v8.0.2 with: name: moderation-result run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/moderate-bug-fix.yml b/.github/workflows/moderate-bug-fix.yml index f9a1442..10e674c 100644 --- a/.github/workflows/moderate-bug-fix.yml +++ b/.github/workflows/moderate-bug-fix.yml @@ -111,7 +111,7 @@ jobs: - name: Upload result artifact if: always() - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: moderation-result path: moderation-result diff --git a/.github/workflows/moderate-feature-build-apply.yml b/.github/workflows/moderate-feature-build-apply.yml index 6bc7e55..f4b95a0 100644 --- a/.github/workflows/moderate-feature-build-apply.yml +++ b/.github/workflows/moderate-feature-build-apply.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@v7.0.1 - name: Download moderation result - uses: actions/download-artifact@v8.0.1 + uses: actions/download-artifact@v8.0.2 with: name: moderation-result run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/moderate-feature-build.yml b/.github/workflows/moderate-feature-build.yml index a91d2b4..297579c 100644 --- a/.github/workflows/moderate-feature-build.yml +++ b/.github/workflows/moderate-feature-build.yml @@ -111,7 +111,7 @@ jobs: - name: Upload result artifact if: always() - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: moderation-result path: moderation-result diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2946d03..9c210a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,6 +68,14 @@ jobs: fail-fast: false matrix: arch: ${{ fromJSON(needs.version.outputs.arches) }} + # Job-level permissions replace the workflow-level ones, so + # `contents: write` is repeated. `id-token: write` lets `make publish` + # mint a GitHub OIDC token for PyPI, npm and crates.io trusted + # publishing; repos that publish to none of them never request one, + # so it is unused for them. + permissions: + contents: write + id-token: write # renovate: datasource=github-runners depName=ubuntu runs-on: ${{ matrix.arch == 'arm64' && (vars.CI_RUNNER_ARM64 || 'ubuntu-24.04-arm') || (vars.CI_RUNNER_AMD64 || 'ubuntu-24.04') }} env: @@ -78,6 +86,15 @@ jobs: OCI_IMAGE_NAME: ${{ vars.OCI_IMAGE_NAME }} OCI_REGISTRY_USERNAME: ${{ secrets.OCI_REGISTRY_USERNAME }} OCI_REGISTRY_PASSWORD: ${{ secrets.OCI_REGISTRY_PASSWORD }} + # Package repos only: `testpypi` or `pypi` selects the index + # `make publish` uploads to via trusted publishing; unset skips it. + PYPI_PUBLISH: ${{ vars.PYPI_PUBLISH }} + # Package repos only: any non-empty value makes `make publish` push + # to npm / crates.io; unset skips it. The bootstrap secrets + # (NPM_TOKEN, CARGO_REGISTRY_TOKEN) are NOT here -- see the Publish + # step. + NPM_PUBLISH: ${{ vars.NPM_PUBLISH }} + CARGO_PUBLISH: ${{ vars.CARGO_PUBLISH }} GITHUB_TOKEN: ${{ github.token }} steps: - name: Check out repository @@ -105,11 +122,18 @@ jobs: - name: Populate release assets run: make release-assets + # The only step that sees the registry bootstrap tokens. Unset + # secrets arrive as empty strings, which the Makefile must treat as + # absent. Never map NPM_TOKEN to NODE_AUTH_TOKEN: npm's OIDC trusted + # publishing breaks when NODE_AUTH_TOKEN is set, even if empty. - name: Publish + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: make publish - name: Upload release assets - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: release-assets-${{ matrix.arch }} path: dist/ @@ -124,7 +148,7 @@ jobs: uses: actions/checkout@v7.0.1 - name: Download release assets - uses: actions/download-artifact@v8.0.1 + uses: actions/download-artifact@v8.0.2 with: pattern: release-assets-* merge-multiple: true @@ -143,11 +167,18 @@ jobs: env: RELEASE_VERSION: ${{ needs.version.outputs.version }} RELEASE_TAG: ${{ needs.version.outputs.tag }} + # So a package's release-check can also install what publish uploaded. + PYPI_PUBLISH: ${{ vars.PYPI_PUBLISH }} + NPM_PUBLISH: ${{ vars.NPM_PUBLISH }} + CARGO_PUBLISH: ${{ vars.CARGO_PUBLISH }} with: runCmd: make release-check env: | RELEASE_VERSION RELEASE_TAG + PYPI_PUBLISH + NPM_PUBLISH + CARGO_PUBLISH # devcontainers/ci starts the full compose stack but never tears it # down -- see checks.yml's identical step. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa8d3d5..7285a4a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -110,10 +110,41 @@ repos: pass_filenames: false stages: [pre-commit, manual] + # template-docker's image-layer hooks (docs/TEMPLATE.md's "Image layer"). + - repo: local + hooks: + # Official hadolint image, pinned (a regex manager in renovate.json + # tracks the tag); docker_image runs it with the repo mounted, so no + # host install is needed. Fails on warning and above, not info: the + # base-owned devcontainer Dockerfile trips info-level DL3066. There is no repo-wide + # DL3008 (pin apt package versions) ignore: where a pin would break rebuilds, add a + # per-line `# hadolint ignore=DL3008` with a reason. + - id: hadolint + name: hadolint (Dockerfile lint) + entry: hadolint/hadolint:v2.15.1 hadolint --failure-threshold warning + language: docker_image + files: ^(\.devcontainer/)?Dockerfile$ + stages: [pre-commit, manual] + + # The toolchain version ARGs must match between the devcontainer image + # and the runtime image. An instance appends its own ARG names (e.g. + # RUST_VERSION) to this entry. - id: check-dockerfile-versions - name: check-dockerfile-versions (Dockerfile and app.Dockerfile agree) - entry: scripts/check-dockerfile-versions.sh + name: check-dockerfile-versions (ARG defaults match across Dockerfiles) + entry: bash scripts/check-dockerfile-versions.sh DEBIAN_VERSION NODE_VERSION language: system - files: ^(app\.)?Dockerfile$ + files: ^(\.devcontainer/)?Dockerfile$ pass_filenames: false stages: [pre-commit, manual] + + # Vulnerability scan of the built image tar(s) in dist/ with Trivy + # (ADR 0001; the Trivy pin and why are in scripts/image-scan.sh). + # Manual stage only: it needs `make build` to have produced dist/*.tar + # and network for the vulnerability DB. Skips cleanly with no tar. + - id: image-scan + name: image-scan (Trivy, HIGH/CRITICAL with a fix) + entry: bash scripts/image-scan.sh + language: system + pass_filenames: false + always_run: true + stages: [manual] diff --git a/CLAUDE.md b/CLAUDE.md index f5b61af..516d260 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,11 +54,11 @@ Practices below are distilled from Anthropic's own Claude Code guidance touch (see "Before writing anything" above) and write a plan before editing. Skip planning for a change you could describe as a one-sentence diff. - When asked to produce a plan, write it to `docs/plans/` per - [`docs/plans/README.md`](docs/plans/README.md) rather than only - answering inline — that file also covers what happens to the plan - document once its work is executed (folded into an ADR/docs, or - removed). + When asked to produce a plan, use the `plan-creation` skill + (`.claude/skills/plan-creation/`): it writes the plan to `docs/plans/` + per [`docs/plans/README.md`](docs/plans/README.md) rather than only + answering inline, and that file also covers what happens to the plan + once its work is executed. - **Report changes after executing.** After any turn that modified, created, or deleted files, close with a brief commit message for the user's use (a one-line summary plus a short bullet list of the most @@ -81,12 +81,20 @@ Practices below are distilled from Anthropic's own Claude Code guidance - **Course-correct early.** If the same correction has to be made twice on one approach, stop and reconsider the approach itself rather than trying a third variation. -- **Resolve test coverage gaps in parallel.** When closing multiple - test coverage gaps, first plan out which gaps are independent (touch - disjoint files/modules with no shared state or ordering dependency), - then dispatch one subagent per independent gap to write/fix its tests - concurrently rather than working through the list serially. Gaps that - share a file or depend on each other's changes stay sequential. +- **Decide alone only where a common best practice exists.** Settle a + decision yourself only when primary sources (official docs, RFCs, + maintainers) or a clear majority of reputable practice agree, or when + a plan, ADR or NFR already requires one option. Otherwise the user + decides: sources that disagree, genuine trade-offs, and matters of + taste or convention (naming, public API shape, visual design, + user-facing defaults). Ask with the options and their trade-offs, the + sources, and your own lean labeled as a lean. +- **Execute multi-item work as an orchestrator.** When executing a + `docs/plans/` plan, or any task that splits into several independent + items (closing multiple test coverage gaps, for example), the main + session orchestrates rather than implementing, through + `plan-executor` subagents (`.claude/agents/plan-executor.md`). Use the + `plan-execution` skill (`.claude/skills/plan-execution/`) for how. - **Flag devcontainer changes as needing a rebuild.** Editing anything under `.devcontainer/` (compose files, `stack/`, Dockerfiles) doesn't take effect in the running container — tell the user a rebuild is diff --git a/Dockerfile b/Dockerfile index f09ff39..a97c245 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,74 +1,67 @@ -# syntax=docker/dockerfile:1.27 -# Single-stage build: develop (devcontainer). A downstream instance adds its -# own builder/runner stages on top of this same base image and -# postCreateCommand structure -- see docs/TEMPLATE.md's "Contents" section. +# syntax=docker/dockerfile:1.28 +# Runtime image (builder → runner), the repo's top-level Dockerfile; the +# devcontainer image is `.devcontainer/Dockerfile`'s `develop` stage. +# NODE_VERSION/DEBIAN_VERSION must match .devcontainer/Dockerfile's, so +# the bundle is built with the same Node the devcontainer tests against -- +# enforced by scripts/check-dockerfile-versions.sh. +# renovate: datasource=node-version depName=node +ARG NODE_VERSION=24.22.0 ARG DEBIAN_VERSION=trixie -# renovate: datasource=github-releases depName=j178/prek -ARG PREK_VERSION=0.5.3 - -# renovate: datasource=npm depName=@anthropic-ai/claude-code -ARG CLAUDE_CODE_VERSION=2.1.278 - -# renovate: datasource=github-releases depName=edouard-claude/snip -ARG SNIP_VERSION=0.25.2 - -# uv itself isn't a project dependency manager here -- it's only the -# mechanism scripts/develop.sh uses to install an exact, checksum-verified -# CPython build (see PYTHON_VERSION below) rather than an unpinned apt -# package. Left on PATH afterward; harmless, and an instance that adds -# Python as its own application runtime can reuse it directly. -# renovate: datasource=github-releases depName=astral-sh/uv -ARG UV_VERSION=0.12.17 - -# python3 is infrastructure tooling, not an application runtime this -# template assumes: prek needs an interpreter to build the venv for any -# "language: python" hook (.pre-commit-config.yaml's pre-commit-hooks -# repo), and .github/scripts/*.py need one directly. -# renovate: datasource=python-version depName=python -ARG PYTHON_VERSION=3.14.7 - -# Node.js is infrastructure tooling too, not an application runtime: npx -# (the clear-thought MCP server in .mcp.json) is the only thing that -# needs it. -# renovate: datasource=node-version depName=node -ARG NODE_VERSION=24.21.0 +# renovate: datasource=docker depName=nginxinc/nginx-unprivileged +ARG NGINX_VERSION=1.30.6-alpine ARG SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt ARG SSL_CERT_DIR=/etc/ssl/certs ######################################## -# develop — interactive devcontainer image, based on Microsoft's generic -# base devcontainer image. No *application* language runtime is assumed -# here — an instance adds its own (Python via uv, Rust via rustup, Go, -# Node.js, OpenTofu/Ansible, ...) on top of this stage, following the same -# ARG/scripts/develop.sh pattern established below. The Python and Node.js -# this stage does install are infrastructure tooling only -- see the ARG -# comments above. +# builder — installs deps with pnpm (the pinned version comes from +# package.json's own `packageManager` field, which Corepack reads) and +# builds the static SPA bundle. ######################################## -FROM mcr.microsoft.com/devcontainers/base:${DEBIAN_VERSION} AS develop -ARG PREK_VERSION -ARG CLAUDE_CODE_VERSION -ARG SNIP_VERSION -ARG UV_VERSION -ARG PYTHON_VERSION -ARG NODE_VERSION +FROM node:${NODE_VERSION}-${DEBIAN_VERSION}-slim AS builder ARG SSL_CERT_FILE ARG SSL_CERT_DIR - ENV SSL_CERT_FILE=${SSL_CERT_FILE} \ - SSL_CERT_DIR=${SSL_CERT_DIR} \ - REQUESTS_CA_BUNDLE=${SSL_CERT_FILE} \ - CURL_CA_BUNDLE=${SSL_CERT_FILE} + SSL_CERT_DIR=${SSL_CERT_DIR} -COPY scripts/develop.sh /tmp/develop.sh -RUN bash /tmp/develop.sh "$PREK_VERSION" "$CLAUDE_CODE_VERSION" "$SNIP_VERSION" "$UV_VERSION" "$PYTHON_VERSION" "$NODE_VERSION" +RUN corepack enable +WORKDIR /workspace +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN pnpm install --frozen-lockfile +COPY . . +RUN pnpm build -# Instance hook: scripts/develop.d/*.sh, see that directory's README.md. -COPY scripts/develop.d/ /tmp/develop.d/ -RUN for f in /tmp/develop.d/*.sh; do [ -e "$f" ] || continue; echo "==> $f"; bash "$f" || exit 1; done +######################################## +# runner — nginx-unprivileged serving the built SPA and proxying /api to +# the real backend (see docs/adrs/0002-same-origin-api-proxy.md). Its own +# docker-entrypoint.d/20-envsubst-on-templates.sh already renders +# scripts/docker/nginx.conf.template -> /etc/nginx/conf.d/default.conf from +# real container env vars on every start; docker-entrypoint.d/ here adds +# one more such script for this app's own /config.js (its client-side +# runtime config), same mechanism, one level up the stack. +######################################## +FROM nginxinc/nginx-unprivileged:${NGINX_VERSION} AS runner -USER vscode -WORKDIR /workspace -CMD ["sleep", "infinity"] +# Defaults match the backend's own devcontainer (see docs/adrs/0001-...md +# and docs/adrs/0002-...md) -- correct for a local smoke-test of this +# image, not a real deployment, which must override all three. +ENV OIDC_AUTHORITY=http://localhost:8080/realms/template-fastapi \ + OIDC_CLIENT_ID=api \ + API_PROXY_TARGET=http://host.docker.internal:8000 + +COPY scripts/docker/nginx.conf.template /etc/nginx/templates/default.conf.template +COPY scripts/docker/docker-entrypoint.d/20-envsubst-app-config.sh /docker-entrypoint.d/20-envsubst-app-config.sh +# The base image already switches to its unprivileged `nginx` user +# (uid/gid 101) -- root only for this one COPY+chown, since --chown alone +# reaches the *copied files* but not the pre-existing +# /usr/share/nginx/html directory entry itself (still root-owned there), +# and this stage's own docker-entrypoint.d script needs write access on +# that directory to create config.js at container start. +USER root +COPY --from=builder --chown=nginx:nginx /workspace/dist /usr/share/nginx/html +RUN chown nginx:nginx /usr/share/nginx/html +USER nginx + +EXPOSE 8080 diff --git a/Makefile b/Makefile index 88c6e1c..c648ce3 100644 --- a/Makefile +++ b/Makefile @@ -1,69 +1,145 @@ -# template-react's implementation of template-base's release contract -# (docs/TEMPLATE.md, "Release: a Makefile contract"): the app.Dockerfile -# `runner` image, per arch, combined into one multi-arch tag by finalize. -# release.yml sets RELEASE_VERSION / RELEASE_TAG / RELEASE_ARCH / OCI_*. +# Image implementation of template-base's release contract (docs/TEMPLATE.md, +# "Release: a Makefile contract"). release.yml sets RELEASE_VERSION / +# RELEASE_TAG / RELEASE_ARCH / OCI_* -- see there. Per-instance values go in +# the optional image.mk (every variable below is `?=`); steps beyond the +# generic ones are extra prerequisites of release-check, set there too. +# Template-owned: a sync overwrites this file. +# +# What gets published is the tar `build` wrote to dist/ -- the one `sbom` +# described and `image-smoke` ran -- copied to the registry with skopeo +# rather than rebuilt, so the registry digest is the tested artifact's. +# `created` is the commit time, so rebuilding the same commit yields the same +# labels; layer contents are not guaranteed bit-for-bit reproducible. -.PHONY: release-arches build sbom release-assets publish release-check finalize +-include image.mk -ARCHES := amd64 arm64 -REPO := $(or $(GITHUB_REPOSITORY),craftainer/template-react) -IMAGE := $(notdir $(REPO)) -ASSET := dist/$(IMAGE)-$(RELEASE_VERSION)-$(RELEASE_ARCH) -OCI_NAME := $(shell printf '%s' '$(or $(OCI_IMAGE_NAME),$(REPO))' | tr '[:upper:]' '[:lower:]') +.PHONY: release-arches build sbom release-assets publish release-check finalize image-smoke check-arch +.DEFAULT_GOAL := release-arches + +DOCKERFILE ?= Dockerfile +TARGET ?= runner +ARCHES ?= amd64 arm64 +BUILD_ARGS ?= +EXTRA_LABELS ?= +OCI_DESCRIPTION ?= +OCI_LICENSE ?= GPL-3.0-or-later +COVERAGE_REPORT ?= +IMAGE_SMOKE_PORT ?= 8000 +IMAGE_SMOKE_PATH ?= /health/live +IMAGE_SMOKE_RUN_ARGS ?= +IMAGE_SMOKE_TIMEOUT ?= 60 # renovate: datasource=docker depName=anchore/syft -SYFT_VERSION := v1.18.1 +SYFT_VERSION := v1.52.0 + +# Pinned by tag and digest; Renovate updates both together. Apache-2.0. The +# `-immutable` tag, not `vX.Y.Z`: quay.io rebuilds the plain tags daily and +# garbage-collects their old digests, which would break the digest pin. +SKOPEO_IMAGE := quay.io/skopeo/stable:v1.22.3-immutable@sha256:c0ee1f4edca5c01cb8d5611124f92f3cc47196ecab68aee5d0f90834e00574d5 + +# owner/repo: GITHUB_REPOSITORY in CI; inside the devcontainer (where +# release-check runs) it is unset, so derive it from the origin remote +# (ssh, scp-like and https forms, with or without credentials and .git); +# the directory name is the last resort. +REPO := $(or $(GITHUB_REPOSITORY),$(shell git remote get-url origin 2>/dev/null | sed -E 's|^[a-z+]+://([^@/]*@)?([^/:]+):[0-9]+/|\2/|; s|^[a-z+]+://||; s|^[^@/]*@||; s|^[^/:]+[:/]||; s|/$$||; s|\.git$$||'),$(notdir $(CURDIR))) +OWNER := $(firstword $(subst /, ,$(REPO))) +IMAGE := $(notdir $(REPO)) +ASSET_PREFIX := dist/$(IMAGE)-$(RELEASE_VERSION) +ASSET := $(ASSET_PREFIX)-$(RELEASE_ARCH) +RELEASE_URL := https://github.com/$(REPO)/releases/download/$(RELEASE_TAG) +OCI_NAME = $(shell printf '%s' '$(or $(OCI_IMAGE_NAME),$(REPO))' | tr '[:upper:]' '[:lower:]') -# Only set in CI, where release.yml exposes the Actions cache runtime. +# Values that may hold arbitrary characters travel through the environment, +# never through recipe text. +export OCI_DESCRIPTION +export IMAGE_SMOKE_RUN_ARGS IMAGE_SMOKE_PORT IMAGE_SMOKE_PATH IMAGE_SMOKE_TIMEOUT RELEASE_VERSION +export IMAGE_TAR_PREFIX = $(ASSET_PREFIX) +export SKOPEO_IMAGE + +# Only set in CI, by crazy-max/ghaction-github-runtime in release.yml. ifdef ACTIONS_RUNTIME_TOKEN CACHE := --cache-from type=gha,scope=$(RELEASE_ARCH) --cache-to type=gha,mode=max,scope=$(RELEASE_ARCH) endif -# `created` is the commit time, so build and publish (separate make -# invocations) produce identical labels and therefore the same image. -LABELS := \ +# Recursive (`=`) so git runs only in recipes that build. Description and +# coverage-report are emitted only when set; the description is read from +# the environment so quotes, `$` and backticks in it are safe. +LABELS = \ --label org.opencontainers.image.title=$(IMAGE) \ --label org.opencontainers.image.source=https://github.com/$(REPO) \ + --label org.opencontainers.image.url=https://github.com/$(REPO) \ --label org.opencontainers.image.version=$(RELEASE_VERSION) \ --label org.opencontainers.image.revision=$(shell git rev-parse HEAD) \ --label org.opencontainers.image.created=$(shell git log -1 --format=%cI) \ - --label org.opencontainers.image.licenses=GPL-3.0 + --label org.opencontainers.image.licenses=$(OCI_LICENSE) \ + --label io.github.$(OWNER).sbom=$(RELEASE_URL)/$(IMAGE)-$(RELEASE_VERSION)-$(RELEASE_ARCH).spdx.json +LABELS += $${OCI_DESCRIPTION:+--label "org.opencontainers.image.description=$$OCI_DESCRIPTION"} +ifneq ($(COVERAGE_REPORT),) +LABELS += --label io.github.$(OWNER).coverage-report=$(RELEASE_URL)/$(COVERAGE_REPORT) +endif +LABELS += $(EXTRA_LABELS) -BUILD := docker buildx build -f app.Dockerfile --target runner \ - --platform linux/$(RELEASE_ARCH) $(LABELS) $(CACHE) +BUILD = docker buildx build -f $(DOCKERFILE) --target $(TARGET) \ + --platform linux/$(RELEASE_ARCH) $(BUILD_ARGS) $(LABELS) $(CACHE) + +check-arch: + @test -n "$(RELEASE_ARCH)" || { echo "RELEASE_ARCH is not set." >&2; exit 1; } release-arches: - @python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' $(ARCHES) + @test -n "$(strip $(ARCHES))" || { echo "ARCHES is empty; list at least one arch in image.mk." >&2; exit 1; } + @printf '[%s]\n' "$$(printf '"%s",' $(ARCHES) | sed 's/,$$//')" -build: +build: check-arch mkdir -p dist $(BUILD) --output type=oci,dest=$(ASSET).tar . -sbom: +sbom: check-arch docker run --rm -v "$(CURDIR)/dist:/dist" anchore/syft:$(SYFT_VERSION) \ oci-archive:/$(ASSET).tar -o spdx-json=/$(ASSET).spdx.json -release-assets: +# Each arch is smoke-tested here, natively on its own runner, before publish. +release-assets: check-arch image-smoke test -f $(ASSET).tar && test -f $(ASSET).spdx.json -# OCI_REGISTRY_PASSWORD is a PAT (write:packages for ghcr.io) -- release.yml -# grants GITHUB_TOKEN no packages scope. -publish: +# Copies the tested tar from dist/ to the registry (skopeo copy +# oci-archive:... docker://...), records the pushed manifest digest in +# dist/.digest for finalize, and logs in through a throwaway authfile +# so the password never appears in a command line. +publish: check-arch ifeq ($(OCI_REGISTRY),) @echo "OCI_REGISTRY not set -- skipping publish." else - @printf '%s' "$$OCI_REGISTRY_PASSWORD" | docker login "$(OCI_REGISTRY)" -u "$$OCI_REGISTRY_USERNAME" --password-stdin - $(BUILD) --push -t $(OCI_REGISTRY)/$(OCI_NAME):$(RELEASE_VERSION)-$(RELEASE_ARCH) . + @test -f $(ASSET).tar || { echo "$(ASSET).tar not found; run 'make build' first." >&2; exit 1; } + @auth="$$(mktemp -d)" && trap 'rm -rf "$$auth"' EXIT && \ + printf '%s' "$$OCI_REGISTRY_PASSWORD" | bash scripts/skopeo.sh -i -v "$$auth:/auth" -- \ + login --authfile /auth/auth.json -u "$$OCI_REGISTRY_USERNAME" --password-stdin "$(OCI_REGISTRY)" && \ + bash scripts/skopeo.sh -v "$$auth:/auth:ro" -- \ + copy --authfile /auth/auth.json --digestfile /$(ASSET).digest \ + oci-archive:/$(ASSET).tar docker://$(OCI_REGISTRY)/$(OCI_NAME):$(RELEASE_VERSION)-$(RELEASE_ARCH) endif -release-check: - pnpm test +# Smoke-tests the tar in dist/ for RELEASE_ARCH (default: the host's arch). +# image.mk's IMAGE_SMOKE_* values reach the script through the environment. +image-smoke: + bash scripts/image-smoke.sh + +# No recipe by design: image.mk adds steps as prerequisites, so Make never +# sees two recipes for one target. Runs the host-arch smoke test again +# inside the devcontainer, with the instance's backing services up. +release-check: image-smoke +# One manifest list over the per-arch digests publish recorded, so it +# references exactly the images that were tested and pushed. Logs in with a +# throwaway DOCKER_CONFIG so the password isn't left in ~/.docker/config.json. finalize: ifeq ($(OCI_REGISTRY),) @echo "OCI_REGISTRY not set -- no manifest list to create." else - @printf '%s' "$$OCI_REGISTRY_PASSWORD" | docker login "$(OCI_REGISTRY)" -u "$$OCI_REGISTRY_USERNAME" --password-stdin - docker buildx imagetools create --tag $(OCI_REGISTRY)/$(OCI_NAME):$(RELEASE_VERSION) \ - $(foreach a,$(ARCHES),$(OCI_REGISTRY)/$(OCI_NAME):$(RELEASE_VERSION)-$(a)) + @export DOCKER_CONFIG="$$(mktemp -d)" && trap 'rm -rf "$$DOCKER_CONFIG"' EXIT && \ + printf '%s' "$$OCI_REGISTRY_PASSWORD" | docker login "$(OCI_REGISTRY)" -u "$$OCI_REGISTRY_USERNAME" --password-stdin && \ + srcs="" && for a in $(ARCHES); do \ + d="$$(cat dist/$(IMAGE)-$(RELEASE_VERSION)-$$a.digest)" || exit 1; \ + srcs="$$srcs $(OCI_REGISTRY)/$(OCI_NAME)@$$d"; \ + done && \ + docker buildx imagetools create --tag $(OCI_REGISTRY)/$(OCI_NAME):$(RELEASE_VERSION) $$srcs endif diff --git a/README.md b/README.md index 395000a..ebf513c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ The React frontend for [`template-fastapi`](https://github.com/craftainer/templa Vite + React + TypeScript + MUI, authenticating against that backend's own Keycloak realm and talking to it exclusively through a same-origin `/api` proxy. Itself an instance of -[`template-base`](https://github.com/craftainer/template-base) — see +[`template-docker`](https://github.com/craftainer/template-docker) (the +shared image layer, itself built on +[`template-base`](https://github.com/craftainer/template-base)) — see [`docs/TEMPLATE.md`](docs/TEMPLATE.md) for everything about this repository that's identical across every `template-base` instance (devcontainer, CI, the release Makefile contract, template sync, @@ -21,6 +23,9 @@ decisions: - [`0002-same-origin-api-proxy.md`](docs/adrs/0002-same-origin-api-proxy.md) — the `/api` proxy (dev and prod), cross-repo dev networking, and the production image's runtime-configurable settings. +- [`0003-re-parent-onto-template-docker.md`](docs/adrs/0003-re-parent-onto-template-docker.md) — + the image layer from `template-docker`, the `Dockerfile` layout, `image.mk`, + and the UI dependency set. ## Getting started @@ -66,11 +71,12 @@ image" section. ## Production image and releases -The runtime image is `app.Dockerfile`'s `runner` stage -(`docker build -f app.Dockerfile --target runner .`); the base-owned -`Dockerfile` only builds the devcontainer. `release.yml` drives the -`Makefile`, which builds it natively for `amd64` and `arm64` and, when -the `OCI_REGISTRY` repository variable is set (e.g. `ghcr.io`), pushes +The runtime image is the top-level `Dockerfile`'s `runner` stage +(`docker build --target runner .`); `.devcontainer/Dockerfile` only builds +the devcontainer. `release.yml` drives the template-docker-owned +`Makefile` (tuned by this repo's `image.mk`), which builds it natively for +`amd64` and `arm64`, smoke-tests each image, and, when the `OCI_REGISTRY` +repository variable is set (e.g. `ghcr.io`), copies the tested images to per-arch tags and combines them into one multi-arch `//template-react:` tag. Registry credentials are the `OCI_REGISTRY_USERNAME` / `OCI_REGISTRY_PASSWORD` secrets — for diff --git a/app.Dockerfile b/app.Dockerfile deleted file mode 100644 index d8c405e..0000000 --- a/app.Dockerfile +++ /dev/null @@ -1,67 +0,0 @@ -# syntax=docker/dockerfile:1.27 -# Runtime image (builder → runner); the devcontainer image is the -# base-owned `Dockerfile`'s `develop` stage. - -# NODE_VERSION/DEBIAN_VERSION must match the base-owned Dockerfile's, so -# the bundle is built with the same Node the devcontainer tests against -- -# enforced by scripts/check-dockerfile-versions.sh. -# renovate: datasource=node-version depName=node -ARG NODE_VERSION=24.21.0 -ARG DEBIAN_VERSION=trixie - -# renovate: datasource=docker depName=nginxinc/nginx-unprivileged -ARG NGINX_VERSION=1.27.4-alpine - -ARG SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt -ARG SSL_CERT_DIR=/etc/ssl/certs - -######################################## -# builder — installs deps with pnpm (the pinned version comes from -# package.json's own `packageManager` field, which Corepack reads) and -# builds the static SPA bundle. -######################################## -FROM node:${NODE_VERSION}-${DEBIAN_VERSION}-slim AS builder -ARG SSL_CERT_FILE -ARG SSL_CERT_DIR -ENV SSL_CERT_FILE=${SSL_CERT_FILE} \ - SSL_CERT_DIR=${SSL_CERT_DIR} - -RUN corepack enable -WORKDIR /workspace -COPY package.json pnpm-lock.yaml ./ -RUN pnpm install --frozen-lockfile -COPY . . -RUN pnpm build - -######################################## -# runner — nginx-unprivileged serving the built SPA and proxying /api to -# the real backend (see docs/adrs/0002-same-origin-api-proxy.md). Its own -# docker-entrypoint.d/20-envsubst-on-templates.sh already renders -# scripts/docker/nginx.conf.template -> /etc/nginx/conf.d/default.conf from -# real container env vars on every start; docker-entrypoint.d/ here adds -# one more such script for this app's own /config.js (its client-side -# runtime config), same mechanism, one level up the stack. -######################################## -FROM nginxinc/nginx-unprivileged:${NGINX_VERSION} AS runner - -# Defaults match the backend's own devcontainer (see docs/adrs/0001-...md -# and docs/adrs/0002-...md) -- correct for a local smoke-test of this -# image, not a real deployment, which must override all three. -ENV OIDC_AUTHORITY=http://localhost:8080/realms/template-fastapi \ - OIDC_CLIENT_ID=api \ - API_PROXY_TARGET=http://host.docker.internal:8000 - -COPY scripts/docker/nginx.conf.template /etc/nginx/templates/default.conf.template -COPY scripts/docker/docker-entrypoint.d/20-envsubst-app-config.sh /docker-entrypoint.d/20-envsubst-app-config.sh -# The base image already switches to its unprivileged `nginx` user -# (uid/gid 101) -- root only for this one COPY+chown, since --chown alone -# reaches the *copied files* but not the pre-existing -# /usr/share/nginx/html directory entry itself (still root-owned there), -# and this stage's own docker-entrypoint.d script needs write access on -# that directory to create config.js at container start. -USER root -COPY --from=builder --chown=nginx:nginx /workspace/dist /usr/share/nginx/html -RUN chown nginx:nginx /usr/share/nginx/html -USER nginx - -EXPOSE 8080 diff --git a/app.Dockerfile.dockerignore b/app.Dockerfile.dockerignore deleted file mode 100644 index 596cf12..0000000 --- a/app.Dockerfile.dockerignore +++ /dev/null @@ -1,19 +0,0 @@ -# Build context excludes for `docker build -f app.Dockerfile` (see -# app.Dockerfile). BuildKit reads this file *instead of* .dockerignore for -# that build, so it repeats every exclusion the runtime build needs. -.git -.github -.devcontainer -.secrets -.vscode -.claude -docs -README.md -CLAUDE.md - -# The builder stage runs its own `pnpm install`/`pnpm build` -- without -# these, `COPY . .` (after that install) would overwrite the container's -# own node_modules with the host's, which may not match its platform/arch. -node_modules -dist -coverage diff --git a/docs/TEMPLATE.md b/docs/TEMPLATE.md index d75c9ad..6e3e5f4 100644 --- a/docs/TEMPLATE.md +++ b/docs/TEMPLATE.md @@ -29,16 +29,24 @@ artifact on top of the shape described here. `README.md` + `template.md` scaffolding — no numbered content, which is entirely instance-owned. - `.secrets/` — local secret files, never committed; see its `README.md`. -- `Dockerfile` — a single `develop` stage (the devcontainer image). An - instance adds its own `builder`/`runner` (or equivalent) stages on top - for its own release artifact. +- `.devcontainer/Dockerfile` — a single `develop` stage (the devcontainer + image), with its build-context excludes in + `.devcontainer/Dockerfile.dockerignore`. The repo root has no + `Dockerfile`: that one name is reserved for an instance's own runtime + image (see "Instance extension points"), so a repo never has more than + one top-level Dockerfile. - `scripts/develop.sh`, `scripts/post-create.sh` — the `develop` stage's setup script and the devcontainer's `postCreateCommand`, each running an instance hook directory (`develop.d/`, `post-create.d/`); see `scripts/README.md` and "Instance extension points" below. - `Makefile` — the release contract `release.yml` drives - (`build`/`sbom`/`release-assets`/`publish`), each a documented no-op - here — see "Release: a Makefile contract" below. + (`build`/`sbom`/`release-assets`/`publish`); in `template-docker` the + image implementation — see "Release: a Makefile contract" and "Image + layer (template-docker)" below. +- `Dockerfile`, `image.mk`, `.dockerignore`, `scripts/image-smoke.sh`, + `scripts/runner-setup.sh`, `scripts/runner-setup.d/`, + `scripts/check-dockerfile-versions.sh` — the image layer; see "Image + layer (template-docker)" below. - `.pre-commit-config.yaml` — git hooks, run by `prek` or `pre-commit`; language-agnostic hooks only. - `CLAUDE.md` — the AI-assisted coding workflow Claude Code follows in @@ -49,22 +57,36 @@ artifact on top of the shape described here. Base-owned files stay `replace` tier (a sync overwrites them) and expose hooks an instance fills with files it owns, so an instance never edits a -base-owned file directly. Don't edit `Dockerfile`, `scripts/develop.sh` +base-owned file directly. Don't edit `.devcontainer/Dockerfile`, `scripts/develop.sh` or `compose.yml` in an instance; use the hooks below. | Hook | Base side (template-owned) | Instance side (instance-owned) | |---|---|---| -| Extra devcontainer tooling | `Dockerfile` `develop` stage runs every `scripts/develop.d/*.sh` after `develop.sh` | `scripts/develop.d/NN-name.sh` | +| Extra devcontainer tooling | `.devcontainer/Dockerfile` `develop` stage runs every `scripts/develop.d/*.sh` after `develop.sh` | `scripts/develop.d/NN-name.sh` | | Extra post-create steps | `scripts/post-create.sh`, run by `devcontainer.json`'s `postCreateCommand`, runs every `scripts/post-create.d/*.sh` | `scripts/post-create.d/NN-name.sh` | | Extra compose services / dev-service settings | `devcontainer.json` `"dockerComposeFile": ["compose.yml", "compose.instance.yml"]` | `.devcontainer/compose.instance.yml` (base ships a stub, `ignore` tier) | -| Runtime image | nothing (base `Dockerfile` is `develop`-only) | `app.Dockerfile` + `app.Dockerfile.dockerignore` | +| Runtime image | nothing (the devcontainer image lives in `.devcontainer/`) | top-level `Dockerfile` + `.dockerignore`, listed in `.github/template-sync-manifest.local.yml`'s `ignore:` | Files with no include mechanism of their own (JSON configs, `.pre-commit-config.yaml` — prek has no include) are `merge` tier, so an -instance's additions survive a sync. The runtime image goes in its own -`app.Dockerfile` so it never collides with the template-owned -`Dockerfile`; BuildKit picks up `app.Dockerfile.dockerignore` -automatically for `-f app.Dockerfile`. +instance's additions survive a sync. The runtime image is the +top-level `Dockerfile` with a plain `.dockerignore`, both instance-owned: +base keeps the devcontainer image and its excludes under +`.devcontainer/`, where BuildKit picks up +`.devcontainer/Dockerfile.dockerignore` for that build automatically. + +**One-time upgrade (instances synced from before this layout).** A sync +adds `.devcontainer/Dockerfile` but can't delete the old top-level +`Dockerfile`/`.dockerignore`: from this release on, base's manifest no +longer classifies those paths, and a merge-tier file is never deleted +automatically. In the sync PR, by hand: + +- With a runtime image: rename `app.Dockerfile` → `Dockerfile` and + `app.Dockerfile.dockerignore` → `.dockerignore`, overwriting the old + base files. Update every reference (`-f app.Dockerfile` in the + `Makefile`, `dockerfile:` in compose files, CI, docs) and list both + paths under `ignore:` in `.github/template-sync-manifest.local.yml`. +- Without one: delete the old top-level `Dockerfile` and `.dockerignore`. ## Getting started @@ -117,8 +139,8 @@ unchanged across every instance), then drives these targets: | Target | Runs | Where | Env available | |---|---|---|---| | `release-arches` | once, first | host runner | none; must print a JSON array on stdout, e.g. `["amd64"]` | -| `build`, `sbom`, `release-assets`, `publish` | once **per arch**, in that order | host runner for that arch | `RELEASE_VERSION`, `RELEASE_TAG`, `RELEASE_ARCH`, `OCI_REGISTRY`, `OCI_IMAGE_NAME`, `OCI_REGISTRY_USERNAME`, `OCI_REGISTRY_PASSWORD`, all `GITHUB_*` | -| `release-check` | once, after all arches | **inside the devcontainer** (`devcontainers/ci`), stack up | `RELEASE_VERSION`, `RELEASE_TAG` | +| `build`, `sbom`, `release-assets`, `publish` | once **per arch**, in that order | host runner for that arch | `RELEASE_VERSION`, `RELEASE_TAG`, `RELEASE_ARCH`, `OCI_REGISTRY`, `OCI_IMAGE_NAME`, `OCI_REGISTRY_USERNAME`, `OCI_REGISTRY_PASSWORD`, `PYPI_PUBLISH`, `NPM_PUBLISH`, `CARGO_PUBLISH`, all `GITHUB_*`; on `publish` only, `NPM_TOKEN` and `CARGO_REGISTRY_TOKEN` (bootstrap secrets; empty when unset, so treat empty as absent) | +| `release-check` | once, after all arches | **inside the devcontainer** (`devcontainers/ci`), stack up | `RELEASE_VERSION`, `RELEASE_TAG`, `PYPI_PUBLISH`, `NPM_PUBLISH`, `CARGO_PUBLISH` | | `finalize` | once, after `release-check` | host runner | same as per-arch minus `RELEASE_ARCH` | What each target is for: `build` produces the release artifact(s) (an @@ -144,7 +166,9 @@ The arch legs run natively, not under QEMU: `arm64` on `ubuntu-24.04-arm`, `amd64` on `ubuntu-24.04`, each overridable with the `CI_RUNNER_ARM64` / `CI_RUNNER_AMD64` repository/organization variables. -This repo's own `Makefile` implements every target as a documented +`template-docker` replaces that no-op `Makefile` with the image +implementation (see "Image layer (template-docker)" below). The base +`Makefile` implements every target as a documented no-op, since `template-base` ships no artifact — it's both a working example of the contract's shape and what `release.yml` needs to run cleanly if this repo itself is ever released. An instance's `Makefile` is @@ -152,6 +176,170 @@ not template-owned (it's the whole point that it differs per artifact type), so it's tracked like any other instance-owned file — untouched by this template's own sync manifest. +## Image layer (template-docker) + +`template-docker` is a mid-layer template: an instance of `template-base` +that is itself the template for every app that ships a container image +(`template-fastapi`, `template-axum`, `template-react`). Sync runs in +three hops, base to docker to app, and `ADR 0001` records why. It adds +these files over base: + +| Path | Tier for instances | What it is | +|---|---|---| +| `Makefile` | `replace` | the image implementation of the release contract above | +| `image.mk` | `ignore` | the instance's settings for the Makefile, every variable commented | +| `Dockerfile` | `ignore` | skeleton to replace: `builder` and `runner` stages, `ARG DEBIAN_VERSION`, `ARG APP_UID`, SSL ARGs, `APP_VERSION`/`APP_COMMIT` | +| `.dockerignore` | `merge` | baseline build-context excludes; the instance appends its own | +| `scripts/image-smoke.sh` | `replace` | loads the image tar from `dist/`, runs it and polls a URL | +| `scripts/runner-setup.sh` | `replace` | creates the group-0 runtime user and its writable directories for the `runner` stage | +| `scripts/image-scan.sh` | `replace` | the Trivy scan the `image-scan` hook runs | +| `scripts/runner-setup.d/*.sh` | `ignore` | the instance's runner-stage hooks | +| `scripts/check-dockerfile-versions.sh` | `replace` | the version-ARG consistency check | + +### `image.mk` variables + +The Makefile starts with `-include image.mk` (a missing file means all +defaults) and every variable is `?=`, so set them in `image.mk`: + +| Variable | Default | Meaning | +|---|---|---| +| `DOCKERFILE`, `TARGET` | `Dockerfile`, `runner` | build file and stage | +| `ARCHES` | `amd64 arm64` | space-separated; `release-arches` prints it as a JSON array and `finalize` merges the per-arch images into one tag; must not be empty | +| `BUILD_ARGS` | empty | full flag words, e.g. `--build-arg APP_VERSION=$(RELEASE_VERSION)` | +| `EXTRA_LABELS` | empty | full flag words, e.g. `--label k=v` | +| `OCI_DESCRIPTION` | empty | `org.opencontainers.image.description`; the label is omitted while empty | +| `OCI_LICENSE` | `GPL-3.0-or-later` | SPDX id for `org.opencontainers.image.licenses` | +| `COVERAGE_REPORT` | empty | release asset name for the `io.github..coverage-report` label; omitted while empty | +| `IMAGE_SMOKE_PORT`, `IMAGE_SMOKE_PATH` | `8000`, `/health/live` | URL polled for a 2xx | +| `IMAGE_SMOKE_RUN_ARGS`, `IMAGE_SMOKE_TIMEOUT` | empty, `60` | extra `docker run` words, whitespace-separated with no embedded spaces or quotes (passed through the environment); seconds to wait | + +`SYFT_VERSION` (the SBOM tool image tag) and `SKOPEO_IMAGE` (tag and +digest) are pinned in the Makefile for Renovate; don't override them. The +image name comes from `GITHUB_REPOSITORY` (the repository name), never a +hardcoded one. Inside the devcontainer, where `release-check` runs and +`GITHUB_REPOSITORY` is unset, it is derived from the `origin` remote (ssh +or https form), and from the directory name as a last resort; +`image-smoke` also accepts the single `dist/*--.tar` when +the expected name is absent. `OCI_IMAGE_NAME` overrides the registry path. +`build`, `sbom`, `release-assets` and `publish` need `RELEASE_ARCH` and +fail through the `check-arch` prerequisite when it is empty; +`release-arches`, `release-check`/`image-smoke` (host arch when unset) and +`finalize` don't. `publish` copies the tar from `dist/` that was SBOM'd +and smoke-tested to the registry with skopeo (`oci-archive:` to +`docker://`, Apache-2.0, pinned image; `crane push` only reads +docker-format tarballs, and `build` writes an OCI layout), so the pushed +digest is the tested one. It records the digest in +`dist/--.digest`, and `finalize` builds the manifest +list from those digests. Registry login uses a throwaway authfile fed the +password on stdin. The labels are listed in `.github/workflows/README.md`'s "Image +labels". Registry auth stays the opt-in `OCI_REGISTRY*` secrets contract. + +### Adding `release-check` steps + +`release-check` has no recipe of its own: it depends on `image-smoke`. +An instance adds steps as prerequisites in `image.mk`, so Make never +sees two recipes for one target: + +```make +release-check: instance-tests +instance-tests: + uv run pytest --cov-report=xml:dist/coverage.xml +``` + +`release-assets` depends on `image-smoke`, so each arch is booted +natively on its own runner in the build job before `publish`; the +`release-check` run inside the devcontainer smoke-tests the host arch +again with the backing services up. `image-smoke` converts the arch's +image in the OCI-layout tar with the pinned skopeo (through +`scripts/skopeo.sh`, which `publish` uses too; `--override-arch`, to a +docker-archive under `dist/`) and `docker load`s that, because +`docker load` can't read a buildx OCI tar on the classic image store, +with or without attestations. It reads `IMAGE_TAR_PREFIX` and +`SKOPEO_IMAGE` (set by the Makefile), the arch +from `RELEASE_ARCH` (else the host's; if `RELEASE_ARCH` is unset and +there is no tar for the host arch, it prints a notice and exits 0, since +the per-arch run already covered that arch), maps a random host port to +`IMAGE_SMOKE_PORT`, prints the container logs on failure and always +removes the container and the private `image-smoke-*` tag it created +(never an image that existed before). + +### Runner setup hook + +`scripts/runner-setup.sh` creates the user (UID `APP_UID`, group 0), makes +its home and `/app/data` group-writable, and leaves `/app` root-owned, so +the image also runs under an arbitrary UID (OpenShift's restricted SCC). +Run it **before** copying app code, then copy the code after it with +`COPY --chown=:0 --chmod=u=rX,g=rX,o=` (0550 directories, 0440 +files): only data and cache directories are writable, the runtime user +can't modify what it runs, and no recursive `chgrp`/`chmod` duplicates +the files into another layer. It then runs every +`scripts/runner-setup.d/*.sh` in glob order as `bash NN-name.sh APP_UID`, +still as root and in the same `RUN` layer. The instance puts extra +packages and writable directories there instead of editing the script. + +### Dockerfile version check + +The `check-dockerfile-versions` hook runs +`scripts/check-dockerfile-versions.sh DEBIAN_VERSION` whenever either +Dockerfile changes: each named ARG must have the same default in +`.devcontainer/Dockerfile` and `Dockerfile` (the runtime must match the +toolchain the devcontainer tests against). An instance appends the ARG +names it keeps in both files (e.g. `RUST_VERSION`) to the hook's `entry:` +in its `.pre-commit-config.yaml`; ARGs not listed aren't checked. + +### Image hooks + +- `hadolint` lints both Dockerfiles, run from the pinned official image + (so it needs Docker). It fails on warnings and above, not on info-level + findings, and has no repo-wide ignores. An instance that must + install an unpinned apt package adds a per-line + `# hadolint ignore=DL3008` with a reason (as the skeleton `Dockerfile` + does: Debian drops superseded versions, so pins break rebuilds). +- `image-scan` runs only at the `manual` stage, because it needs + `dist/*.tar` from `make build` and network access for the vulnerability + database. With no tar it skips. Otherwise `scripts/image-scan.sh` runs a + pinned Trivy image on each tar and fails on HIGH or CRITICAL + findings that have a fix (`--severity HIGH,CRITICAL --ignore-unfixed + --exit-code 1`). Trivy can't read the OCI-layout tar `make build` + writes, so the script unpacks it and scans the directory, selecting + `linux/` from the tar's name so a buildx attestation manifest is + never scanned instead of the image. The database is cached in the `trivy-cache` Docker + volume. Trivy is Apache-2.0, so invoking it as a tool is compatible + with this repo's GPL-3.0 licence. The scanner is the maintainers' + choice, made over Grype. Trivy's releases and Docker Hub images were + compromised in March 2026 (GHSA-69fq-xp46-6x23), so the image is pinned + by digest as well as tag (`aquasec/trivy:@sha256:…`); the + advisory lists images referenced by digest as unaffected. Renovate + updates tag and digest together. When bumping by hand, take the digest + from the registry and verify it with cosign as the advisory describes: + `cosign verify --certificate-identity-regexp + 'https://github\.com/aquasecurity/' --certificate-oidc-issuer + 'https://token.actions.githubusercontent.com' + ghcr.io/aquasecurity/trivy@sha256:…` (the Docker Hub, GHCR and ECR + digests of a release are identical). + +### Re-parenting an existing app onto template-docker + +One-time steps for an app that is today an instance of `template-base` +(or of a sibling template) and moves under `template-docker`: + +1. Make the tracked files equal `template-docker`'s tag: take the + template's `Makefile`, `scripts/` files, `.dockerignore` and + `.pre-commit-config.yaml` hooks, move the app's Makefile settings into + `image.mk` and its runtime user setup into `scripts/runner-setup.d/`, + and list its version ARGs on the `check-dockerfile-versions` hook. +2. For `template-react` and `template-axum`, which were not synced from + base, migrate to base's layout first (devcontainer, workflows, docs, + manifest) and let a base sync PR settle before this step. +3. Bootstrap the sync state: run `template-sync.yml` manually with + `template_repo=craftainer/template-docker` and `initial_sync_tag` set + to the tag the files now equal. That writes + `.github/template-sync-state.json` without changing files. From then on + syncs track `template-docker`, which in turn tracks base. +4. Resolve base's additions to the manifest by hand when + `template-docker` is the one syncing from base, as the header of + `.github/template-sync-manifest.yml` says. + ## Template sync Once instantiated, a repo created from this template can pull in later @@ -186,7 +374,7 @@ hand) — see that repo's own docs for the two-hop chain this produces. Every version and config value is defined in exactly one place; nothing duplicates or re-pins it elsewhere. Everything pinned here (base image, -Actions, hook revisions, `prek`/Claude Code CLI/`snip`/`uv`/Python/ +Actions, hook revisions, `prek`/Claude Code CLI/`snip`/`gh`/`uv`/Python/ Node.js versions) is pinned once, at its single point of use, to an exact patch version — never a floating range or `latest` — so Renovate can bump them one at a time and the diff shows exactly what changed. An @@ -194,7 +382,7 @@ instance's own language runtime/package versions follow the same rule in its own Dockerfile/manifest. `python3` and Node.js, both installed by `scripts/develop.sh` and pinned -via the Dockerfile's `PYTHON_VERSION`/`NODE_VERSION` ARGs, are +via `.devcontainer/Dockerfile`'s `PYTHON_VERSION`/`NODE_VERSION` ARGs, are infrastructure tooling only — not an application runtime this template assumes. `python3` exists because `prek` needs an interpreter to build the venv for any `language: python` hook (`.pre-commit-config.yaml`'s diff --git a/docs/adrs/0002-same-origin-api-proxy.md b/docs/adrs/0002-same-origin-api-proxy.md index 922b5b8..f263d31 100644 --- a/docs/adrs/0002-same-origin-api-proxy.md +++ b/docs/adrs/0002-same-origin-api-proxy.md @@ -27,7 +27,7 @@ configuring CORS on the backend: stripping the `/api` prefix. `src/api/client.ts`'s `apiFetch` always fetches `/api${path}`, so the app code never knows or cares where the backend actually is. -- **Prod**: `app.Dockerfile`'s `runner` stage's nginx (`scripts/docker/nginx.conf.template`) +- **Prod**: the top-level `Dockerfile`'s `runner` stage's nginx (`scripts/docker/nginx.conf.template`) proxies `/api/` the same way, to `API_PROXY_TARGET`, alongside serving the built SPA with a history-mode fallback. diff --git a/docs/adrs/0003-re-parent-onto-template-docker.md b/docs/adrs/0003-re-parent-onto-template-docker.md new file mode 100644 index 0000000..5b5b761 --- /dev/null +++ b/docs/adrs/0003-re-parent-onto-template-docker.md @@ -0,0 +1,110 @@ +# 0003. Re-parent onto template-docker, move the app image to `Dockerfile`, and add the UI dependency set + +## Status + +Accepted + +## Context + +template-react was an instance of `template-base` that carried its own +release `Makefile`, `scripts/check-dockerfile-versions.sh` and a separate +`app.Dockerfile` (+ `app.Dockerfile.dockerignore`) for the runtime image, +because base's top-level `Dockerfile` was the devcontainer image. Every +image-shipping template (fastapi, axum, react) repeated that image plumbing. +`template-docker` v1.0.0 is the shared image layer on top of base: the +devcontainer image lives in `.devcontainer/Dockerfile`, the top-level +`Dockerfile` is the instance's runtime image, and a template-owned `Makefile` +implements the release contract (OCI build, SBOM, labels, smoke test, publish +of the tested tar with skopeo) tuned by an optional instance-owned `image.mk` +(its ADR 0001). Track UI of `docs/plans/2026-10-templates-sdks-ui.md` also needs +Storybook, Playwright, axe, Motion, self-hosted fonts and TanStack Query, and +wants them, with the lockfile, in one reviewable change before the items that +use them (UI1 onward) run in parallel. + +## Decision + +We will sync from `template-docker` (`.github/template-sync-state.json`: +`craftainer/template-docker`, `v1.0.0`) and stay a template ourselves, editing +`.github/template-sync-manifest.yml` directly (it started as docker's and adds +react's own paths, so every tracked path is classified). + +**Layout upgrade (one time).** `app.Dockerfile` becomes the top-level +`Dockerfile`; `app.Dockerfile.dockerignore` is deleted and its excludes +(`node_modules`, `coverage`) join the merge-tier `.dockerignore`. The +devcontainer image is now `.devcontainer/Dockerfile` (docker/base's, replace +tier). `NODE_VERSION` in `Dockerfile` follows `.devcontainer/Dockerfile`'s +(24.22.0); `scripts/check-dockerfile-versions.sh DEBIAN_VERSION NODE_VERSION` +keeps them equal. The builder also copies `pnpm-workspace.yaml` (see below). +The runner stays nginx-unprivileged (it does not use `runner-setup.sh`), now on +`1.30.6-alpine` (was `1.27.4-alpine`): the manual-stage `image-scan` hook +(Trivy, HIGH/CRITICAL with a fix) rejected the old base's libraries. + +**`image.mk`** (merge tier, so instances can add values) sets only what differs +from docker's defaults: + +| Variable | Value | Why | +| --------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `IMAGE_SMOKE_PORT` | `8080` | nginx-unprivileged | +| `IMAGE_SMOKE_PATH` | `/` | the SPA's `index.html` | +| `IMAGE_SMOKE_RUN_ARGS` | `-e API_PROXY_TARGET=http://127.0.0.1:8000` | nginx exits on an unresolvable upstream, and the image's default `host.docker.internal` does not resolve in a plain `docker run` on Linux | +| `release-check: unit-tests` | `unit-tests:` runs `pnpm test` | the old `release-check` recipe, now a prerequisite beside docker's `image-smoke` | + +`DOCKERFILE`, `TARGET`, `ARCHES` (`amd64 arm64`) and `OCI_LICENSE` +(`GPL-3.0-or-later`) keep docker's defaults. No `BUILD_ARGS` (the bundle +embeds no version or commit) and no `COVERAGE_REPORT` (no coverage asset is +released), so the coverage label is omitted. + +**Moved or deleted.** Deleted: `app.Dockerfile`, `app.Dockerfile.dockerignore` +and the repo's own release `Makefile` recipes (docker's `Makefile` replaces +them; labels now include `url`, `sbom` and the SPDX `GPL-3.0-or-later` id, and +publish copies the tested tar instead of rebuilding). Old +`check-dockerfile-versions.sh` (which took no arguments) is replaced by docker's. +New, replace-tier from docker: `scripts/image-smoke.sh`, `runner-setup.sh`, +`runner-setup.d/README.md`, `skopeo.sh`, `image-scan.sh`, `.devcontainer/Dockerfile` +and `.devcontainer/Dockerfile.dockerignore`, plus base's newer files (agents, +skills, workflows, `develop.sh`, compose, lock). In the manifest, react's +`Dockerfile`, `.devcontainer/compose.instance.yml` and its two hook scripts are +`replace` for react's instances; `image.mk`, `package.json` and +`pnpm-workspace.yaml` are `merge`. + +**Dependencies** (exact versions, as the rest of `package.json`; installed with +pnpm 12.3.4 and locked; no config, stories, theme or tests yet, those are UI1 +and later): + +| Package | Version | Kind | Purpose | +| ----------------------------------------------------------------------- | ------- | ---- | --------------------------------------------------------------------------------------------------------------------- | +| `storybook`, `@storybook/react-vite`, `@storybook/addon-a11y` | 10.6.3 | dev | component workshop on the existing Vite 8 (the builder supports `^8`) and React 19; the a11y addon runs axe per story | +| `@playwright/test` | 1.64.1 | dev | e2e and visual tests | +| `@axe-core/playwright` | 4.14.0 | dev | accessibility assertions inside Playwright | +| `motion` | 14.4.0 | prod | animation. 14.5.0 is current but was under a day old; see below | +| `@fontsource-variable/fraunces`, `@fontsource-variable/instrument-sans` | 5.3.0 | prod | self-hosted "Field Ledger" fonts (variable builds exist), no third-party font requests | +| `@tanstack/react-query` | 5.104.5 | prod | server-state cache for the CRUD UI | + +We stay on MUI (plan decision P-D4). `@fontsource/roboto` stays until UI1 swaps +the theme's fonts. + +`pnpm-workspace.yaml` (new, merge tier) sets `allowBuilds: { esbuild: false }`. +pnpm 12 fails an install on an unreviewed dependency build script, and +Storybook pulls in esbuild, whose postinstall is only a fast path (its binary +comes from per-platform optional packages). `pnpm-lock.yaml` is run through +Prettier after `pnpm add`, as it was before, because `prettier --check .` +covers it. + +## Consequences + +- Release behaviour is preserved (static nginx image, per-arch build, SBOM, + labels, multi-arch tag) and gains an image smoke test in `release-check`, + per-arch smoke before publish, and publishing of the tested artifact. + Publishing needs `skopeo`, run from a pinned image by docker's script. +- Future template-docker releases arrive through template sync; the + `Makefile` and image scripts are no longer edited here. Per-repo values + go in `image.mk`. +- Anything referencing `app.Dockerfile` must use `Dockerfile` now; the other + Track UI templates' instances inherit the same layout. +- The dependency set adds roughly 100 packages. `motion` is pinned one minor + behind the newest release because pnpm's `minimumReleaseAge` supply-chain + gate rejects packages published under a day ago; do not add an + exclusion for it, let Renovate bump it. `pnpm install` in the builder stage + now needs `pnpm-workspace.yaml` copied beside the lockfile. +- The three unused-until-UI1 groups (Storybook, Playwright, axe) are installed + but unconfigured, so nothing runs them yet and the build is unchanged. diff --git a/docs/plans/README.md b/docs/plans/README.md index 80befde..e2311a1 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -8,6 +8,40 @@ made. A plan can reference an ADR it depends on or that it prompts. - Filename: `YYYY-MM-short-title.md` (the month the plan was started), e.g. `2026-01-add-billing.md`. +## Execution section + +A plan with more than one independent work item is executed by an +orchestrating session through `plan-executor` subagents (see +`../../CLAUDE.md`, "Execute multi-item work as an orchestrator", and the +`plan-execution` skill; plans themselves are written with the +`plan-creation` skill, both in `../../.claude/skills/`). It +needs an `## Execution` section laid out so the orchestrator can turn +each item straight into a brief: + +- **Work items:** each has an ID, the plan text it implements, a + **write set** (exact files and directories), and its dependencies. +- **Waves:** items grouped so that every item in a wave has a write set + disjoint from the others in that wave. A file nearly everything + touches (a rename, dependency additions to `pyproject.toml` and the + lockfile) goes in its own serial wave first, so later items never + fight over it. +- **Interfaces:** where items in the same wave depend on each other, + name the function or module contract between them, so both sides can + be built in parallel against it. +- **Pre-assigned numbers:** sequentially numbered files (ADRs, NFRs, + migrations) that parallel items create get their numbers fixed in the + plan, so two items never pick the same one. +- **Serialized lanes:** anything sharing devcontainer services + (integration and e2e suites, load tests and benchmarks) runs one at a + time, outside the parallel waves. Measurements also need a quiet + window, with no parallel agents competing for CPU. +- **User gates:** a table of every outward-facing or hard-to-reverse + step (creating repos, pushing, opening PRs, publishing, tagging) that + waits for the user's explicit yes. +- **Decision log:** a dated table of settled decisions. Executors treat + it as final, and the orchestrator appends to it as it resolves + escalations. + ## Do - Update a plan's own Status as work progresses, in place — a plan is a diff --git a/docs/plans/template.md b/docs/plans/template.md index 1fb2faa..a7f6429 100644 --- a/docs/plans/template.md +++ b/docs/plans/template.md @@ -15,6 +15,20 @@ The steps, in order, at the level of detail someone else could pick this up from. Link to relevant ADRs (`../adrs/NNNN-....md`) instead of repeating their reasoning here. +## Execution + +Only for plans with more than one independent work item: waves of work +items (ID, write set, dependencies), interfaces between parallel items, +pre-assigned numbers, serialized lanes, and user gates. See +`README.md`'s "Execution section". Remove this section for a +single-item plan. + +## Decision log + +| Date | Decision | +| --- | --- | +| YYYY-MM-DD | A settled decision executors must not reopen. | + ## Open questions Anything genuinely undecided that the plan depends on. Remove this diff --git a/image.mk b/image.mk new file mode 100644 index 0000000..178a467 --- /dev/null +++ b/image.mk @@ -0,0 +1,23 @@ +# template-react's values for template-docker's image release Makefile +# (`-include image.mk`); see docs/TEMPLATE.md's "Image layer". Merge tier: an +# instance edits this file for its own values and a sync keeps them. + +# DOCKERFILE and TARGET keep template-docker's defaults (`Dockerfile`, +# `runner`); ARCHES, OCI_LICENSE too. No APP_VERSION/APP_COMMIT build args: +# the SPA bundle embeds neither. No COVERAGE_REPORT: no coverage asset is +# released. + +# The runner is nginx-unprivileged on 8080 serving the SPA. Its env defaults +# point API_PROXY_TARGET at host.docker.internal, which a plain `docker run` +# on Linux can't resolve, and nginx refuses to start on an unresolvable +# upstream -- so the smoke run points it at loopback instead (the smoke test +# never calls /api). +IMAGE_SMOKE_PORT := 8080 +IMAGE_SMOKE_PATH := / +IMAGE_SMOKE_RUN_ARGS := -e API_PROXY_TARGET=http://127.0.0.1:8000 + +# The unit tests that were release-check's whole recipe before the image +# smoke test joined it as a prerequisite. +release-check: unit-tests +unit-tests: + pnpm test diff --git a/package.json b/package.json index 51fc785..20e8191 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,23 @@ "dependencies": { "@emotion/react": "11.14.0", "@emotion/styled": "11.14.1", + "@fontsource-variable/fraunces": "5.3.0", + "@fontsource-variable/instrument-sans": "5.3.0", "@fontsource/roboto": "5.3.0", "@mui/icons-material": "9.4.0", "@mui/material": "9.4.0", + "@tanstack/react-query": "5.104.5", + "motion": "14.4.0", "oidc-client-ts": "3.5.0", "react": "19.2.8", "react-dom": "19.2.8", "react-oidc-context": "3.3.1" }, "devDependencies": { + "@axe-core/playwright": "4.14.0", + "@playwright/test": "1.64.1", + "@storybook/addon-a11y": "10.6.3", + "@storybook/react-vite": "10.6.3", "@testing-library/jest-dom": "7.0.1", "@testing-library/react": "16.3.3", "@testing-library/user-event": "14.6.7", @@ -36,6 +44,7 @@ "jsdom": "30.0.1", "oxlint": "1.81.0", "prettier": "3.9.6", + "storybook": "10.6.3", "typescript": "6.0.3", "vite": "8.2.2", "vitest": "5.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4f2ecd..a45a336 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,12 @@ importers: "@emotion/styled": specifier: 11.14.1 version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) + "@fontsource-variable/fraunces": + specifier: 5.3.0 + version: 5.3.0 + "@fontsource-variable/instrument-sans": + specifier: 5.3.0 + version: 5.3.0 "@fontsource/roboto": specifier: 5.3.0 version: 5.3.0 @@ -147,6 +153,12 @@ importers: "@mui/material": specifier: 9.4.0 version: 9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@tanstack/react-query": + specifier: 5.104.5 + version: 5.104.5(@types/react@19.2.18)(react@19.2.8) + motion: + specifier: 14.4.0 + version: 14.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) oidc-client-ts: specifier: 3.5.0 version: 3.5.0 @@ -160,9 +172,21 @@ importers: specifier: 3.3.1 version: 3.3.1(oidc-client-ts@3.5.0)(react@19.2.8) devDependencies: + "@axe-core/playwright": + specifier: 4.14.0 + version: 4.14.0(playwright-core@1.64.1) + "@playwright/test": + specifier: 1.64.1 + version: 1.64.1 + "@storybook/addon-a11y": + specifier: 10.6.3 + version: 10.6.3(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8)) + "@storybook/react-vite": + specifier: 10.6.3 + version: 10.6.3(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8))(typescript@6.0.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)) "@testing-library/jest-dom": specifier: 7.0.1 - version: 7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3))) + version: 7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2))) "@testing-library/react": specifier: 16.3.3 version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -180,7 +204,7 @@ importers: version: 19.2.7(@types/react@19.2.18) "@vitejs/plugin-react": specifier: 6.1.1 - version: 6.1.1(vite@8.2.2(@types/node@24.13.3)) + version: 6.1.1(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)) jsdom: specifier: 30.0.1 version: 30.0.1 @@ -190,15 +214,18 @@ importers: prettier: specifier: 3.9.6 version: 3.9.6 + storybook: + specifier: 10.6.3 + version: 10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8) typescript: specifier: 6.0.3 version: 6.0.3 vite: specifier: 8.2.2 - version: 8.2.2(@types/node@24.13.3) + version: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2) vitest: specifier: 5.0.0 - version: 5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)) + version: 5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)) packages: "@adobe/css-tools@4.5.0": @@ -221,6 +248,15 @@ packages: } engines: { node: ^22.13.0 || >=24.0.0 } + "@axe-core/playwright@4.14.0": + resolution: + { + integrity: sha512-SGUhG5PIHd2eMdpgBF2PyQLOhX+qyDR2PT4cmoS4Qk2ak+jX/rrSvst9l0PGv5WTHolKpUMvVmoHpr4cZgf1RQ==, + } + engines: { node: ">=22.12.0" } + peerDependencies: + playwright-core: ">= 1.0.0" + "@babel/code-frame@7.29.7": resolution: { @@ -228,6 +264,20 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/compat-data@7.29.7": + resolution: + { + integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==, + } + engines: { node: ">=6.9.0" } + + "@babel/core@7.29.7": + resolution: + { + integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==, + } + engines: { node: ">=6.9.0" } + "@babel/generator@7.29.8": resolution: { @@ -235,6 +285,13 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-compilation-targets@7.29.7": + resolution: + { + integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==, + } + engines: { node: ">=6.9.0" } + "@babel/helper-globals@7.29.7": resolution: { @@ -249,6 +306,15 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-module-transforms@7.29.7": + resolution: + { + integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0 + "@babel/helper-string-parser@7.29.7": resolution: { @@ -263,6 +329,20 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-validator-option@7.29.7": + resolution: + { + integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helpers@7.29.10": + resolution: + { + integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==, + } + engines: { node: ">=6.9.0" } + "@babel/parser@7.29.8": resolution: { @@ -360,6 +440,42 @@ packages: } engines: { node: ">=20.19.0" } + "@emnapi/core@1.11.0": + resolution: + { + integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==, + } + + "@emnapi/core@1.9.2": + resolution: + { + integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==, + } + + "@emnapi/runtime@1.11.0": + resolution: + { + integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==, + } + + "@emnapi/runtime@1.9.2": + resolution: + { + integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==, + } + + "@emnapi/wasi-threads@1.2.1": + resolution: + { + integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==, + } + + "@emnapi/wasi-threads@1.2.2": + resolution: + { + integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==, + } + "@emotion/babel-plugin@11.13.5": resolution: { @@ -453,233 +569,860 @@ packages: integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==, } - "@exodus/bytes@1.15.1": + "@esbuild/aix-ppc64@0.28.2": resolution: { - integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==, + integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==, } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - peerDependencies: - "@noble/hashes": ^1.8.0 || ^2.0.0 - peerDependenciesMeta: - "@noble/hashes": - optional: true + engines: { node: ">=18" } + cpu: [ppc64] + os: [aix] - "@fontsource/roboto@5.3.0": + "@esbuild/android-arm64@0.28.2": resolution: { - integrity: sha512-BapRJOWYP+LZ21zp+wBQjfpPYKRoxc4LspJ/RLuI+HSMBD5u/X4O+ESDrSvEqDSy0rAl7GwBJ+09mdc16cVQ1Q==, + integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==, } + engines: { node: ">=18" } + cpu: [arm64] + os: [android] - "@jridgewell/gen-mapping@0.3.13": + "@esbuild/android-arm@0.28.2": resolution: { - integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, + integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==, } + engines: { node: ">=18" } + cpu: [arm] + os: [android] - "@jridgewell/resolve-uri@3.1.2": + "@esbuild/android-x64@0.28.2": resolution: { - integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==, } - engines: { node: ">=6.0.0" } + engines: { node: ">=18" } + cpu: [x64] + os: [android] - "@jridgewell/sourcemap-codec@1.6.0": + "@esbuild/darwin-arm64@0.28.2": resolution: { - integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==, + integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==, } + engines: { node: ">=18" } + cpu: [arm64] + os: [darwin] - "@jridgewell/trace-mapping@0.3.31": + "@esbuild/darwin-x64@0.28.2": resolution: { - integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, + integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==, } + engines: { node: ">=18" } + cpu: [x64] + os: [darwin] - "@mui/core-downloads-tracker@9.4.0": + "@esbuild/freebsd-arm64@0.28.2": resolution: { - integrity: sha512-d+gIYM1raJP87yrVQcVg+Q3vycVMArMkgu/TjJLi2vTvO9FHKWNRR1xs9nnN4eLpM9nGxxm02UMvc9BrmcUKvQ==, + integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==, } + engines: { node: ">=18" } + cpu: [arm64] + os: [freebsd] - "@mui/icons-material@9.4.0": + "@esbuild/freebsd-x64@0.28.2": resolution: { - integrity: sha512-5PVgBYtLOXTk6u0YUAjoV9GoTUQDbeZ8g+pus2h0GphLT/rpmftRnB5D/Kw6QCAovB7EyBX7UxKdZqPBBAHUKw==, + integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==, } - engines: { node: ">=14.0.0" } - peerDependencies: - "@mui/material": ^9.4.0 - "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@types/react": - optional: true + engines: { node: ">=18" } + cpu: [x64] + os: [freebsd] - "@mui/material@9.4.0": + "@esbuild/linux-arm64@0.28.2": resolution: { - integrity: sha512-6KKUIvkQ2r/s48s9VZV9JEZ1W2dRa7RndVsF+Ipx4CYxDOQeozeRsS5r9NiheFf8A5hpbqJJIGoX7hjg5X4XUw==, + integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==, } - engines: { node: ">=14.0.0" } - peerDependencies: - "@emotion/react": ^11.5.0 - "@emotion/styled": ^11.3.0 - "@mui/material-pigment-css": ^9.4.0 - "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@emotion/react": - optional: true - "@emotion/styled": - optional: true - "@mui/material-pigment-css": - optional: true - "@types/react": - optional: true + engines: { node: ">=18" } + cpu: [arm64] + os: [linux] - "@mui/private-theming@9.4.0": + "@esbuild/linux-arm@0.28.2": resolution: { - integrity: sha512-qQpX/2XPGDz5F2OIWzo4kkorURlDeXGKytRQudXCdLS5TuwKsMKaZyx77LZEVSB73c7o3UrLWwQ/goRuHBdtjw==, + integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==, } - engines: { node: ">=14.0.0" } - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@types/react": - optional: true + engines: { node: ">=18" } + cpu: [arm] + os: [linux] - "@mui/styled-engine@9.4.0": + "@esbuild/linux-ia32@0.28.2": resolution: { - integrity: sha512-SlqVEYqnyEgXMFW4JuIDMelyhKzoWBJ85v2mNCQd0XtU0uk7U9E1zaJZ06XqodhA8oJrb+4Q+3E/JlwBe4jWkw==, + integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==, } - engines: { node: ">=14.0.0" } - peerDependencies: - "@emotion/react": ^11.4.1 - "@emotion/styled": ^11.3.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@emotion/react": - optional: true - "@emotion/styled": - optional: true + engines: { node: ">=18" } + cpu: [ia32] + os: [linux] - "@mui/system@9.4.0": + "@esbuild/linux-loong64@0.28.2": resolution: { - integrity: sha512-AZz9z13oGS1J0BYeAFHTbV7PegCtPiE/uMEIi7TJ1kTx3Vkt+jUpLyXF/2uV8B6Y79iTh98kVeOH9fX3x3zXXg==, + integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==, } - engines: { node: ">=14.0.0" } - peerDependencies: - "@emotion/react": ^11.5.0 - "@emotion/styled": ^11.3.0 - "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@emotion/react": - optional: true - "@emotion/styled": - optional: true - "@types/react": - optional: true + engines: { node: ">=18" } + cpu: [loong64] + os: [linux] - "@mui/types@9.4.0": + "@esbuild/linux-mips64el@0.28.2": resolution: { - integrity: sha512-13DH0Oniua4WmGqeYbQAZqmiN7r4nfd0zwIoGJ5QeJwyIHgEmf+LkRw/jV1HZb6AiUoHX8Oxf4xgalq3vYHcIw==, + integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==, } - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@types/react": - optional: true + engines: { node: ">=18" } + cpu: [mips64el] + os: [linux] - "@mui/utils@9.4.0": + "@esbuild/linux-ppc64@0.28.2": resolution: { - integrity: sha512-WkzY8uYtqUDyGKPShrRQRBomfMQHddlsyKu/gRRESYO24tDPD2z0xRE8CBoUsWqpBa+YrFs9KixiS7kQRjsPAQ==, + integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==, } - engines: { node: ">=14.0.0" } - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@types/react": - optional: true + engines: { node: ">=18" } + cpu: [ppc64] + os: [linux] - "@oxc-project/types@0.148.0": + "@esbuild/linux-riscv64@0.28.2": resolution: { - integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==, + integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==, } + engines: { node: ">=18" } + cpu: [riscv64] + os: [linux] - "@oxlint/binding-android-arm-eabi@1.81.0": + "@esbuild/linux-s390x@0.28.2": resolution: { - integrity: sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==, + integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==, } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm] - os: [android] + engines: { node: ">=18" } + cpu: [s390x] + os: [linux] - "@oxlint/binding-android-arm64@1.81.0": + "@esbuild/linux-x64@0.28.2": resolution: { - integrity: sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==, + integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==, } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm64] - os: [android] + engines: { node: ">=18" } + cpu: [x64] + os: [linux] - "@oxlint/binding-darwin-arm64@1.81.0": + "@esbuild/netbsd-arm64@0.28.2": resolution: { - integrity: sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==, + integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==, } - engines: { node: ^20.19.0 || >=22.12.0 } + engines: { node: ">=18" } cpu: [arm64] - os: [darwin] + os: [netbsd] - "@oxlint/binding-darwin-x64@1.81.0": + "@esbuild/netbsd-x64@0.28.2": resolution: { - integrity: sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==, + integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==, } - engines: { node: ^20.19.0 || >=22.12.0 } + engines: { node: ">=18" } cpu: [x64] - os: [darwin] + os: [netbsd] - "@oxlint/binding-freebsd-x64@1.81.0": + "@esbuild/openbsd-arm64@0.28.2": resolution: { - integrity: sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==, + integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==, } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [x64] - os: [freebsd] + engines: { node: ">=18" } + cpu: [arm64] + os: [openbsd] - "@oxlint/binding-linux-arm-gnueabihf@1.81.0": + "@esbuild/openbsd-x64@0.28.2": resolution: { - integrity: sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==, + integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==, } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm] - os: [linux] + engines: { node: ">=18" } + cpu: [x64] + os: [openbsd] - "@oxlint/binding-linux-arm-musleabihf@1.81.0": + "@esbuild/openharmony-arm64@0.28.2": resolution: { - integrity: sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==, + integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==, } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm] - os: [linux] - + engines: { node: ">=18" } + cpu: [arm64] + os: [openharmony] + + "@esbuild/sunos-x64@0.28.2": + resolution: + { + integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [sunos] + + "@esbuild/win32-arm64@0.28.2": + resolution: + { + integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [win32] + + "@esbuild/win32-ia32@0.28.2": + resolution: + { + integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [win32] + + "@esbuild/win32-x64@0.28.2": + resolution: + { + integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [win32] + + "@exodus/bytes@1.15.1": + resolution: + { + integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + peerDependencies: + "@noble/hashes": ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + "@noble/hashes": + optional: true + + "@fontsource-variable/fraunces@5.3.0": + resolution: + { + integrity: sha512-9BYGySn4AHEJdgp9Z28tQ3X+laJMEOITXkQarZXeloWQZDq5oOvXJ3kDA8c7MGIfpogIaZfjrQBqmda8POOCKA==, + } + + "@fontsource-variable/instrument-sans@5.3.0": + resolution: + { + integrity: sha512-u4gKbDBTNFGkg997tfQn3eHOhHuquWUFTRT/rwzuKtrxX5P2ekfs2x+LgBPP4P32+cC+vUwF1Cr+IdRoPQbrGw==, + } + + "@fontsource/roboto@5.3.0": + resolution: + { + integrity: sha512-BapRJOWYP+LZ21zp+wBQjfpPYKRoxc4LspJ/RLuI+HSMBD5u/X4O+ESDrSvEqDSy0rAl7GwBJ+09mdc16cVQ1Q==, + } + + "@joshwooding/vite-plugin-react-docgen-typescript@0.7.0": + resolution: + { + integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==, + } + peerDependencies: + typescript: ">= 4.3.x" + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + + "@jridgewell/gen-mapping@0.3.13": + resolution: + { + integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, + } + + "@jridgewell/remapping@2.3.5": + resolution: + { + integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, + } + + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } + + "@jridgewell/sourcemap-codec@1.6.0": + resolution: + { + integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==, + } + + "@jridgewell/trace-mapping@0.3.31": + resolution: + { + integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, + } + + "@mui/core-downloads-tracker@9.4.0": + resolution: + { + integrity: sha512-d+gIYM1raJP87yrVQcVg+Q3vycVMArMkgu/TjJLi2vTvO9FHKWNRR1xs9nnN4eLpM9nGxxm02UMvc9BrmcUKvQ==, + } + + "@mui/icons-material@9.4.0": + resolution: + { + integrity: sha512-5PVgBYtLOXTk6u0YUAjoV9GoTUQDbeZ8g+pus2h0GphLT/rpmftRnB5D/Kw6QCAovB7EyBX7UxKdZqPBBAHUKw==, + } + engines: { node: ">=14.0.0" } + peerDependencies: + "@mui/material": ^9.4.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + + "@mui/material@9.4.0": + resolution: + { + integrity: sha512-6KKUIvkQ2r/s48s9VZV9JEZ1W2dRa7RndVsF+Ipx4CYxDOQeozeRsS5r9NiheFf8A5hpbqJJIGoX7hjg5X4XUw==, + } + engines: { node: ">=14.0.0" } + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@mui/material-pigment-css": ^9.4.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@mui/material-pigment-css": + optional: true + "@types/react": + optional: true + + "@mui/private-theming@9.4.0": + resolution: + { + integrity: sha512-qQpX/2XPGDz5F2OIWzo4kkorURlDeXGKytRQudXCdLS5TuwKsMKaZyx77LZEVSB73c7o3UrLWwQ/goRuHBdtjw==, + } + engines: { node: ">=14.0.0" } + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + + "@mui/styled-engine@9.4.0": + resolution: + { + integrity: sha512-SlqVEYqnyEgXMFW4JuIDMelyhKzoWBJ85v2mNCQd0XtU0uk7U9E1zaJZ06XqodhA8oJrb+4Q+3E/JlwBe4jWkw==, + } + engines: { node: ">=14.0.0" } + peerDependencies: + "@emotion/react": ^11.4.1 + "@emotion/styled": ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + + "@mui/system@9.4.0": + resolution: + { + integrity: sha512-AZz9z13oGS1J0BYeAFHTbV7PegCtPiE/uMEIi7TJ1kTx3Vkt+jUpLyXF/2uV8B6Y79iTh98kVeOH9fX3x3zXXg==, + } + engines: { node: ">=14.0.0" } + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + + "@mui/types@9.4.0": + resolution: + { + integrity: sha512-13DH0Oniua4WmGqeYbQAZqmiN7r4nfd0zwIoGJ5QeJwyIHgEmf+LkRw/jV1HZb6AiUoHX8Oxf4xgalq3vYHcIw==, + } + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + + "@mui/utils@9.4.0": + resolution: + { + integrity: sha512-WkzY8uYtqUDyGKPShrRQRBomfMQHddlsyKu/gRRESYO24tDPD2z0xRE8CBoUsWqpBa+YrFs9KixiS7kQRjsPAQ==, + } + engines: { node: ">=14.0.0" } + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + + "@napi-rs/wasm-runtime@1.2.5": + resolution: + { + integrity: sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=23.5.0 } + peerDependencies: + "@emnapi/core": ^1.7.1 || ^2.0.0-alpha.4 + "@emnapi/runtime": ^1.7.1 || ^2.0.0-alpha.4 + + "@oxc-parser/binding-android-arm-eabi@0.127.0": + resolution: + { + integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [android] + + "@oxc-parser/binding-android-arm64@0.127.0": + resolution: + { + integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [android] + + "@oxc-parser/binding-darwin-arm64@0.127.0": + resolution: + { + integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [darwin] + + "@oxc-parser/binding-darwin-x64@0.127.0": + resolution: + { + integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [darwin] + + "@oxc-parser/binding-freebsd-x64@0.127.0": + resolution: + { + integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [freebsd] + + "@oxc-parser/binding-linux-arm-gnueabihf@0.127.0": + resolution: + { + integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [linux] + + "@oxc-parser/binding-linux-arm-musleabihf@0.127.0": + resolution: + { + integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [linux] + + "@oxc-parser/binding-linux-arm64-gnu@0.127.0": + resolution: + { + integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [linux] + libc: [glibc] + + "@oxc-parser/binding-linux-arm64-musl@0.127.0": + resolution: + { + integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [linux] + libc: [musl] + + "@oxc-parser/binding-linux-ppc64-gnu@0.127.0": + resolution: + { + integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [ppc64] + os: [linux] + libc: [glibc] + + "@oxc-parser/binding-linux-riscv64-gnu@0.127.0": + resolution: + { + integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [riscv64] + os: [linux] + libc: [glibc] + + "@oxc-parser/binding-linux-riscv64-musl@0.127.0": + resolution: + { + integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [riscv64] + os: [linux] + libc: [musl] + + "@oxc-parser/binding-linux-s390x-gnu@0.127.0": + resolution: + { + integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [s390x] + os: [linux] + libc: [glibc] + + "@oxc-parser/binding-linux-x64-gnu@0.127.0": + resolution: + { + integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@oxc-parser/binding-linux-x64-musl@0.127.0": + resolution: + { + integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [linux] + libc: [musl] + + "@oxc-parser/binding-openharmony-arm64@0.127.0": + resolution: + { + integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [openharmony] + + "@oxc-parser/binding-wasm32-wasi@0.127.0": + resolution: + { + integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [wasm32] + + "@oxc-parser/binding-win32-arm64-msvc@0.127.0": + resolution: + { + integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [win32] + + "@oxc-parser/binding-win32-ia32-msvc@0.127.0": + resolution: + { + integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [ia32] + os: [win32] + + "@oxc-parser/binding-win32-x64-msvc@0.127.0": + resolution: + { + integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [win32] + + "@oxc-project/types@0.127.0": + resolution: + { + integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==, + } + + "@oxc-project/types@0.148.0": + resolution: + { + integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==, + } + + "@oxc-resolver/binding-android-arm-eabi@11.21.2": + resolution: + { + integrity: sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==, + } + cpu: [arm] + os: [android] + + "@oxc-resolver/binding-android-arm64@11.21.2": + resolution: + { + integrity: sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==, + } + cpu: [arm64] + os: [android] + + "@oxc-resolver/binding-darwin-arm64@11.21.2": + resolution: + { + integrity: sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==, + } + cpu: [arm64] + os: [darwin] + + "@oxc-resolver/binding-darwin-x64@11.21.2": + resolution: + { + integrity: sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==, + } + cpu: [x64] + os: [darwin] + + "@oxc-resolver/binding-freebsd-x64@11.21.2": + resolution: + { + integrity: sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==, + } + cpu: [x64] + os: [freebsd] + + "@oxc-resolver/binding-linux-arm-gnueabihf@11.21.2": + resolution: + { + integrity: sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==, + } + cpu: [arm] + os: [linux] + + "@oxc-resolver/binding-linux-arm-musleabihf@11.21.2": + resolution: + { + integrity: sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==, + } + cpu: [arm] + os: [linux] + + "@oxc-resolver/binding-linux-arm64-gnu@11.21.2": + resolution: + { + integrity: sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==, + } + cpu: [arm64] + os: [linux] + libc: [glibc] + + "@oxc-resolver/binding-linux-arm64-musl@11.21.2": + resolution: + { + integrity: sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==, + } + cpu: [arm64] + os: [linux] + libc: [musl] + + "@oxc-resolver/binding-linux-ppc64-gnu@11.21.2": + resolution: + { + integrity: sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==, + } + cpu: [ppc64] + os: [linux] + libc: [glibc] + + "@oxc-resolver/binding-linux-riscv64-gnu@11.21.2": + resolution: + { + integrity: sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==, + } + cpu: [riscv64] + os: [linux] + libc: [glibc] + + "@oxc-resolver/binding-linux-riscv64-musl@11.21.2": + resolution: + { + integrity: sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==, + } + cpu: [riscv64] + os: [linux] + libc: [musl] + + "@oxc-resolver/binding-linux-s390x-gnu@11.21.2": + resolution: + { + integrity: sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==, + } + cpu: [s390x] + os: [linux] + libc: [glibc] + + "@oxc-resolver/binding-linux-x64-gnu@11.21.2": + resolution: + { + integrity: sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==, + } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@oxc-resolver/binding-linux-x64-musl@11.21.2": + resolution: + { + integrity: sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==, + } + cpu: [x64] + os: [linux] + libc: [musl] + + "@oxc-resolver/binding-openharmony-arm64@11.21.2": + resolution: + { + integrity: sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==, + } + cpu: [arm64] + os: [openharmony] + + "@oxc-resolver/binding-wasm32-wasi@11.21.2": + resolution: + { + integrity: sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==, + } + engines: { node: ">=14.0.0" } + cpu: [wasm32] + + "@oxc-resolver/binding-win32-arm64-msvc@11.21.2": + resolution: + { + integrity: sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==, + } + cpu: [arm64] + os: [win32] + + "@oxc-resolver/binding-win32-x64-msvc@11.21.2": + resolution: + { + integrity: sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==, + } + cpu: [x64] + os: [win32] + + "@oxlint/binding-android-arm-eabi@1.81.0": + resolution: + { + integrity: sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [android] + + "@oxlint/binding-android-arm64@1.81.0": + resolution: + { + integrity: sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [android] + + "@oxlint/binding-darwin-arm64@1.81.0": + resolution: + { + integrity: sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [darwin] + + "@oxlint/binding-darwin-x64@1.81.0": + resolution: + { + integrity: sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [darwin] + + "@oxlint/binding-freebsd-x64@1.81.0": + resolution: + { + integrity: sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [freebsd] + + "@oxlint/binding-linux-arm-gnueabihf@1.81.0": + resolution: + { + integrity: sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [linux] + + "@oxlint/binding-linux-arm-musleabihf@1.81.0": + resolution: + { + integrity: sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [linux] + "@oxlint/binding-linux-arm64-gnu@1.81.0": resolution: { @@ -796,6 +1539,14 @@ packages: cpu: [x64] os: [win32] + "@playwright/test@1.64.1": + resolution: + { + integrity: sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==, + } + engines: { node: ">=20" } + hasBin: true + "@popperjs/core@2.11.8": resolution: { @@ -949,6 +1700,119 @@ packages: integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==, } + "@rollup/pluginutils@5.4.0": + resolution: + { + integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==, + } + engines: { node: ">=14.0.0" } + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + "@storybook/addon-a11y@10.6.3": + resolution: + { + integrity: sha512-nqvjpmu5g+PPtEKwgE0qAi4wfuQ9yngOb/1Gm24pBBZOuRsd3UMEBjKORSuAGrIAz9X3LCFfQi7sCcBFXvFbwg==, + } + peerDependencies: + storybook: ^10.6.3 + + "@storybook/builder-vite@10.6.3": + resolution: + { + integrity: sha512-1+WCZZg/fPxxyHDB5sFKniFt9/4RFUIzoNqMujv+BLRROM+gQeyUHvdvoyZOZ51+P8ee+/PgnNKHjsTupV+nOA==, + } + peerDependencies: + storybook: ^10.6.3 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + "@storybook/global@5.0.0": + resolution: + { + integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==, + } + + "@storybook/icons@2.2.0": + resolution: + { + integrity: sha512-FpUB/xizIrV1ILhCIYFo+TtQNN9YEoLjfcnAW0JtOXL+BcM2aisSysI0B54JMWDnljriXySBHgN6ps4s9lb75A==, + } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + "@storybook/react-dom-shim@10.6.3": + resolution: + { + integrity: sha512-cJUuPrieiTl06kzlgaU89iG+CtLAgUTt0Xdvm8qFT9yR2qwafSaswhwYWlZCNX4nWuOu7eK7uzWTwzKqyDBOWQ==, + } + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + "@types/react-dom": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.6.3 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@storybook/react-vite@10.6.3": + resolution: + { + integrity: sha512-geWwLi9JnaRG2t9bYBfz+94skARzzXNT1Toaf0lk7Oog13rtNK7ay2pWNm9SP6/L2S7mEjoWu9+zHLpwxXvfIw==, + } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.6.3 + typescript: ">= 4.9.x" + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + + "@storybook/react@10.6.3": + resolution: + { + integrity: sha512-3tfAkZNdUyx3I4IjM8dgbNWg4k0gj89Hi1MPiawfTybRRWhvnX2YqV9Ut4mrud7YzRv0v+U7+ZYEvj+maUPaow==, + } + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + "@types/react-dom": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.6.3 + typescript: ">= 4.9.x" + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + typescript: + optional: true + + "@tanstack/query-core@5.104.5": + resolution: + { + integrity: sha512-Q94gI8Opd+CbdVNOdj8ZCyV8WqLYitrDq1t96Xc8WZT6lY0r5iRnJLXYDxMex7EEf2v+mXvn4FcjaBKSxXZQ9A==, + } + + "@tanstack/react-query@5.104.5": + resolution: + { + integrity: sha512-RORgFe052U+93weaNuqg7qHq7vNVNzXN/VrXqD4bbAuQCWUqOl2lV5Ui9flWk5CVvMyL/Bh27QWhzwH0aaOx9g==, + } + peerDependencies: + "@types/react": ^18 || ^19 + react: ^18 || ^19 + peerDependenciesMeta: + "@types/react": + optional: true + "@testing-library/dom@10.4.1": resolution: { @@ -956,6 +1820,13 @@ packages: } engines: { node: ">=18" } + "@testing-library/jest-dom@6.9.1": + resolution: + { + integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==, + } + engines: { node: ">=14", npm: ">=6", yarn: ">=1" } + "@testing-library/jest-dom@7.0.1": resolution: { @@ -996,12 +1867,42 @@ packages: peerDependencies: "@testing-library/dom": ">=7.21.4" + "@tybys/wasm-util@0.10.4": + resolution: + { + integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==, + } + "@types/aria-query@5.0.4": resolution: { integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==, } + "@types/babel__core@7.20.5": + resolution: + { + integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, + } + + "@types/babel__generator@7.27.0": + resolution: + { + integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==, + } + + "@types/babel__template@7.4.4": + resolution: + { + integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, + } + + "@types/babel__traverse@7.28.0": + resolution: + { + integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==, + } + "@types/chai@5.2.3": resolution: { @@ -1014,6 +1915,12 @@ packages: integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==, } + "@types/doctrine@0.0.9": + resolution: + { + integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==, + } + "@types/estree@1.0.9": resolution: { @@ -1060,6 +1967,12 @@ packages: integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==, } + "@types/resolve@1.20.6": + resolution: + { + integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==, + } + "@vitejs/plugin-react@6.1.1": resolution: { @@ -1079,6 +1992,12 @@ packages: oxc-transform-react: optional: true + "@vitest/expect@3.2.4": + resolution: + { + integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==, + } + "@vitest/mocker@5.0.0": resolution: { @@ -1093,12 +2012,36 @@ packages: vite: optional: true + "@vitest/pretty-format@3.2.4": + resolution: + { + integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==, + } + + "@vitest/spy@3.2.4": + resolution: + { + integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==, + } + "@vitest/spy@5.0.0": resolution: { integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==, } + "@vitest/utils@3.2.4": + resolution: + { + integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==, + } + + "@webcontainer/env@1.1.1": + resolution: + { + integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==, + } + ansi-regex@5.0.1: resolution: { @@ -1133,6 +2076,20 @@ packages: } engines: { node: ">=12" } + ast-types@0.16.3: + resolution: + { + integrity: sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==, + } + engines: { node: ">=4" } + + axe-core@4.14.0: + resolution: + { + integrity: sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ==, + } + engines: { node: ">=4" } + babel-plugin-macros@3.1.0: resolution: { @@ -1140,12 +2097,49 @@ packages: } engines: { node: ">=10", npm: ">=6" } + balanced-match@4.0.4: + resolution: + { + integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, + } + engines: { node: 18 || 20 || >=22 } + + baseline-browser-mapping@2.11.30: + resolution: + { + integrity: sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA==, + } + engines: { node: ">=6.0.0" } + hasBin: true + bidi-js@1.0.3: resolution: { integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==, } + brace-expansion@5.0.12: + resolution: + { + integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==, + } + engines: { node: 20 || >=22 } + + browserslist@4.29.4: + resolution: + { + integrity: sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + hasBin: true + + bundle-name@4.1.1: + resolution: + { + integrity: sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==, + } + engines: { node: ">=18" } + callsites@3.1.0: resolution: { @@ -1153,6 +2147,19 @@ packages: } engines: { node: ">=6" } + caniuse-lite@1.0.30001817: + resolution: + { + integrity: sha512-uZ1JG80BwhtMalIAGPxPfmNhHyd/B/nTdr3hITWlRamSyAdUStP1Gop01e7SD5q6J90ixJvWDbvmiFVfDgn4bw==, + } + + chai@5.3.3: + resolution: + { + integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==, + } + engines: { node: ">=18" } + chai@6.2.2: resolution: { @@ -1160,6 +2167,13 @@ packages: } engines: { node: ">=18" } + check-error@2.1.3: + resolution: + { + integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==, + } + engines: { node: ">= 16" } + clsx@2.1.1: resolution: { @@ -1173,6 +2187,12 @@ packages: integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==, } + convert-source-map@2.0.0: + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } + cosmiconfig@7.1.0: resolution: { @@ -1224,6 +2244,34 @@ packages: integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==, } + deep-eql@5.0.2: + resolution: + { + integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==, + } + engines: { node: ">=6" } + + default-browser-id@5.0.1: + resolution: + { + integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==, + } + engines: { node: ">=18" } + + default-browser@5.5.1: + resolution: + { + integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==, + } + engines: { node: ">=18" } + + define-lazy-prop@3.0.0: + resolution: + { + integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==, + } + engines: { node: ">=12" } + dequal@2.0.3: resolution: { @@ -1238,6 +2286,13 @@ packages: } engines: { node: ">=8" } + doctrine@3.0.0: + resolution: + { + integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, + } + engines: { node: ">=6.0.0" } + dom-accessibility-api@0.5.16: resolution: { @@ -1256,6 +2311,19 @@ packages: integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==, } + electron-to-chromium@1.5.462: + resolution: + { + integrity: sha512-9qpCBlXPy5v7n1Q7WTO5rW2StI4WNQhl96HpgXRZLPlNoXAD0LbsQUflovQYAO44Yia073ISusqohncvGSMbGA==, + } + + empathic@2.1.0: + resolution: + { + integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==, + } + engines: { node: ">=14" } + entities@8.0.0: resolution: { @@ -1282,6 +2350,21 @@ packages: integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==, } + esbuild@0.28.2: + resolution: + { + integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==, + } + engines: { node: ">=18" } + hasBin: true + + escalade@3.2.0: + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: ">=6" } + escape-string-regexp@4.0.0: resolution: { @@ -1289,12 +2372,33 @@ packages: } engines: { node: ">=10" } + esprima@4.0.1: + resolution: + { + integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, + } + engines: { node: ">=4" } + hasBin: true + + estree-walker@2.0.2: + resolution: + { + integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, + } + estree-walker@3.0.3: resolution: { integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, } + esutils@2.0.3: + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: ">=0.10.0" } + expect-type@1.4.0: resolution: { @@ -1320,6 +2424,20 @@ packages: integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==, } + framer-motion@14.4.0: + resolution: + { + integrity: sha512-HtO1fxord2rnXYRyr1laG4uY7+OtgwDc9Szcg1wlMq/LF7BO3/S7a+KEo691U7pSi+i0ueRuRqaD1UaLEmZHuQ==, + } + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: { @@ -1334,6 +2452,20 @@ packages: integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, } + gensync@1.0.0-beta.2: + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: ">=6.9.0" } + + glob@13.0.6: + resolution: + { + integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==, + } + engines: { node: 18 || 20 || >=22 } + hasown@2.0.4: resolution: { @@ -1381,12 +2513,35 @@ packages: } engines: { node: ">= 0.4" } + is-docker@3.0.0: + resolution: + { + integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==, + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + hasBin: true + + is-inside-container@1.0.0: + resolution: + { + integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==, + } + engines: { node: ">=14.16" } + hasBin: true + is-potential-custom-element-name@1.0.1: resolution: { integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==, } + is-wsl@3.1.1: + resolution: + { + integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==, + } + engines: { node: ">=16" } + js-tokens@4.0.0: resolution: { @@ -1419,6 +2574,20 @@ packages: integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==, } + json5@2.2.3: + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: ">=6" } + hasBin: true + + jsonc-parser@3.3.1: + resolution: + { + integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==, + } + jwt-decode@4.0.0: resolution: { @@ -1549,6 +2718,12 @@ packages: } hasBin: true + loupe@3.2.1: + resolution: + { + integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==, + } + lru-cache@11.5.2: resolution: { @@ -1556,6 +2731,12 @@ packages: } engines: { node: 20 || >=22 } + lru-cache@5.1.1: + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } + lz-string@1.5.0: resolution: { @@ -1582,6 +2763,52 @@ packages: } engines: { node: ">=4" } + minimatch@10.2.6: + resolution: + { + integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==, + } + engines: { node: 18 || 20 || >=22 } + + minimist@1.2.8: + resolution: + { + integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, + } + + minipass@7.1.3: + resolution: + { + integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==, + } + engines: { node: ">=16 || 14 >=14.17" } + + motion-dom@14.4.0: + resolution: + { + integrity: sha512-1q8RyHSdr5COpzrv3JgJ9XRjOO88ZN9CwflpkGgyO/SR/SNRP/dD6EU8SEo092kcKY34IzHC5diJPKrQRMA8lQ==, + } + + motion-utils@14.1.1: + resolution: + { + integrity: sha512-3VvN5Uvf8B6qG+OfOui6Nht5EWp+vLuU3IqcAj7ItZGKqtmTjCebl5sGZF7/08gEHRnEl1AP/fIwbcEyVrcQGQ==, + } + + motion@14.4.0: + resolution: + { + integrity: sha512-JJTIdeNjtF2zi/py0ndRKxQF+8z2pcDUgJYXul7zqbhMvyTLRsPFR9ifE/j4cFCpCMlehQVJkTwd7BZFiPKgQg==, + } + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: { @@ -1596,6 +2823,13 @@ packages: engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true + node-releases@2.0.59: + resolution: + { + integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==, + } + engines: { node: ">=18" } + object-assign@4.1.1: resolution: { @@ -1617,6 +2851,26 @@ packages: } engines: { node: ">=18" } + open@10.2.0: + resolution: + { + integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==, + } + engines: { node: ">=18" } + + oxc-parser@0.127.0: + resolution: + { + integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + + oxc-resolver@11.21.2: + resolution: + { + integrity: sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==, + } + oxlint@1.81.0: resolution: { @@ -1659,6 +2913,13 @@ packages: integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, } + path-scurry@2.0.2: + resolution: + { + integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==, + } + engines: { node: 18 || 20 || >=22 } + path-type@4.0.0: resolution: { @@ -1666,6 +2927,13 @@ packages: } engines: { node: ">=8" } + pathval@2.0.1: + resolution: + { + integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==, + } + engines: { node: ">= 14.16" } + picocolors@1.1.1: resolution: { @@ -1679,6 +2947,22 @@ packages: } engines: { node: ">=12" } + playwright-core@1.64.1: + resolution: + { + integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==, + } + engines: { node: ">=20" } + hasBin: true + + playwright@1.64.1: + resolution: + { + integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==, + } + engines: { node: ">=20" } + hasBin: true + postcss@8.5.28: resolution: { @@ -1714,6 +2998,21 @@ packages: } engines: { node: ">=6" } + react-docgen-typescript@2.4.0: + resolution: + { + integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==, + } + peerDependencies: + typescript: ">= 4.3.x" + + react-docgen@8.0.4: + resolution: + { + integrity: sha512-gIYjtVTU4NCWZnb/LXVSWfClZXnjzX3+xxi69Tkfu4YsmYuqiOk8giLkKt/WwxWqh+U66CTh8Kqbha1uPv8/qQ==, + } + engines: { node: ^20.9.0 || >=22 } + react-dom@19.2.8: resolution: { @@ -1766,6 +3065,13 @@ packages: } engines: { node: ">=0.10.0" } + recast@0.23.21: + resolution: + { + integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==, + } + engines: { node: ">= 4" } + redent@3.0.0: resolution: { @@ -1803,6 +3109,13 @@ packages: engines: { node: ^20.19.0 || >=22.12.0 } hasBin: true + run-applescript@7.1.0: + resolution: + { + integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==, + } + engines: { node: ">=18" } + saxes@6.0.0: resolution: { @@ -1816,6 +3129,21 @@ packages: integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==, } + semver@6.3.1: + resolution: + { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, + } + hasBin: true + + semver@7.8.5: + resolution: + { + integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==, + } + engines: { node: ">=10" } + hasBin: true + siginfo@2.0.0: resolution: { @@ -1836,6 +3164,13 @@ packages: } engines: { node: ">=0.10.0" } + source-map@0.6.1: + resolution: + { + integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, + } + engines: { node: ">=0.10.0" } + stackback@0.0.2: resolution: { @@ -1848,6 +3183,31 @@ packages: integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==, } + storybook@10.6.3: + resolution: + { + integrity: sha512-lNBvcZFG44C98KvQHYzfq1O46m+sl94zY1+na5/9E3ihtNMbvpOgYyMFMIcWYUvOMo3OM4q9jzvB73KU20bU8w==, + } + hasBin: true + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + prettier: ^2 || ^3 + vite-plus: ^0.1.15 || ^0.2.0 + peerDependenciesMeta: + "@types/react": + optional: true + prettier: + optional: true + vite-plus: + optional: true + + strip-bom@3.0.0: + resolution: + { + integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, + } + engines: { node: ">=4" } + strip-indent@3.0.0: resolution: { @@ -1855,6 +3215,13 @@ packages: } engines: { node: ">=8" } + strip-indent@4.1.1: + resolution: + { + integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==, + } + engines: { node: ">=12" } + stylis@4.2.0: resolution: { @@ -1874,6 +3241,12 @@ packages: integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, } + tiny-invariant@1.3.3: + resolution: + { + integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==, + } + tinybench@6.1.4: resolution: { @@ -1895,6 +3268,20 @@ packages: } engines: { node: ">=12.0.0" } + tinyrainbow@2.0.0: + resolution: + { + integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==, + } + engines: { node: ">=14.0.0" } + + tinyspy@4.0.6: + resolution: + { + integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==, + } + engines: { node: ">=14.0.0" } + tldts-core@7.4.11: resolution: { @@ -1922,6 +3309,26 @@ packages: } engines: { node: ">=20" } + ts-dedent@2.3.0: + resolution: + { + integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==, + } + engines: { node: ">=6.10" } + + tsconfig-paths@4.2.0: + resolution: + { + integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==, + } + engines: { node: ">=6" } + + tslib@2.8.1: + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } + typescript@6.0.3: resolution: { @@ -1941,7 +3348,24 @@ packages: { integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==, } - engines: { node: ">=22.19.0" } + engines: { node: ">=22.19.0" } + + update-browserslist-db@1.3.4: + resolution: + { + integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==, + } + hasBin: true + peerDependencies: + browserslist: ">= 4.21.0" + + use-sync-external-store@1.7.0: + resolution: + { + integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==, + } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 vite@8.2.2: resolution: @@ -2076,6 +3500,28 @@ packages: engines: { node: ">=8" } hasBin: true + ws@8.22.0: + resolution: + { + integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==, + } + engines: { node: ">=10.0.0" } + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: + { + integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==, + } + engines: { node: ">=18" } + xml-name-validator@5.0.0: resolution: { @@ -2089,6 +3535,12 @@ packages: integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==, } + yallist@3.1.1: + resolution: + { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, + } + yaml@1.10.3: resolution: { @@ -2114,12 +3566,39 @@ snapshots: is-potential-custom-element-name: 1.0.1 lru-cache: 11.5.2 + "@axe-core/playwright@4.14.0(playwright-core@1.64.1)": + dependencies: + axe-core: 4.14.0 + playwright-core: 1.64.1 + "@babel/code-frame@7.29.7": dependencies: "@babel/helper-validator-identifier": 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 + "@babel/compat-data@7.29.7": {} + + "@babel/core@7.29.7": + dependencies: + "@babel/code-frame": 7.29.7 + "@babel/generator": 7.29.8 + "@babel/helper-compilation-targets": 7.29.7 + "@babel/helper-module-transforms": 7.29.7(@babel/core@7.29.7) + "@babel/helpers": 7.29.10 + "@babel/parser": 7.29.8 + "@babel/template": 7.29.7 + "@babel/traverse": 7.29.8 + "@babel/types": 7.29.8 + "@jridgewell/remapping": 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + "@babel/generator@7.29.8": dependencies: "@babel/parser": 7.29.8 @@ -2128,6 +3607,14 @@ snapshots: "@jridgewell/trace-mapping": 0.3.31 jsesc: 3.1.0 + "@babel/helper-compilation-targets@7.29.7": + dependencies: + "@babel/compat-data": 7.29.7 + "@babel/helper-validator-option": 7.29.7 + browserslist: 4.29.4 + lru-cache: 5.1.1 + semver: 6.3.1 + "@babel/helper-globals@7.29.7": {} "@babel/helper-module-imports@7.29.7": @@ -2137,10 +3624,26 @@ snapshots: transitivePeerDependencies: - supports-color + "@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)": + dependencies: + "@babel/core": 7.29.7 + "@babel/helper-module-imports": 7.29.7 + "@babel/helper-validator-identifier": 7.29.7 + "@babel/traverse": 7.29.8 + transitivePeerDependencies: + - supports-color + "@babel/helper-string-parser@7.29.7": {} "@babel/helper-validator-identifier@7.29.7": {} + "@babel/helper-validator-option@7.29.7": {} + + "@babel/helpers@7.29.10": + dependencies: + "@babel/template": 7.29.7 + "@babel/types": 7.29.8 + "@babel/parser@7.29.8": dependencies: "@babel/types": 7.29.8 @@ -2198,6 +3701,38 @@ snapshots: "@csstools/css-tokenizer@4.0.0": {} + "@emnapi/core@1.11.0": + dependencies: + "@emnapi/wasi-threads": 1.2.2 + tslib: 2.8.1 + optional: true + + "@emnapi/core@1.9.2": + dependencies: + "@emnapi/wasi-threads": 1.2.1 + tslib: 2.8.1 + optional: true + + "@emnapi/runtime@1.11.0": + dependencies: + tslib: 2.8.1 + optional: true + + "@emnapi/runtime@1.9.2": + dependencies: + tslib: 2.8.1 + optional: true + + "@emnapi/wasi-threads@1.2.1": + dependencies: + tslib: 2.8.1 + optional: true + + "@emnapi/wasi-threads@1.2.2": + dependencies: + tslib: 2.8.1 + optional: true + "@emotion/babel-plugin@11.13.5": dependencies: "@babel/helper-module-imports": 7.29.7 @@ -2281,15 +3816,110 @@ snapshots: "@emotion/weak-memoize@0.4.0": {} + "@esbuild/aix-ppc64@0.28.2": + optional: true + + "@esbuild/android-arm64@0.28.2": + optional: true + + "@esbuild/android-arm@0.28.2": + optional: true + + "@esbuild/android-x64@0.28.2": + optional: true + + "@esbuild/darwin-arm64@0.28.2": + optional: true + + "@esbuild/darwin-x64@0.28.2": + optional: true + + "@esbuild/freebsd-arm64@0.28.2": + optional: true + + "@esbuild/freebsd-x64@0.28.2": + optional: true + + "@esbuild/linux-arm64@0.28.2": + optional: true + + "@esbuild/linux-arm@0.28.2": + optional: true + + "@esbuild/linux-ia32@0.28.2": + optional: true + + "@esbuild/linux-loong64@0.28.2": + optional: true + + "@esbuild/linux-mips64el@0.28.2": + optional: true + + "@esbuild/linux-ppc64@0.28.2": + optional: true + + "@esbuild/linux-riscv64@0.28.2": + optional: true + + "@esbuild/linux-s390x@0.28.2": + optional: true + + "@esbuild/linux-x64@0.28.2": + optional: true + + "@esbuild/netbsd-arm64@0.28.2": + optional: true + + "@esbuild/netbsd-x64@0.28.2": + optional: true + + "@esbuild/openbsd-arm64@0.28.2": + optional: true + + "@esbuild/openbsd-x64@0.28.2": + optional: true + + "@esbuild/openharmony-arm64@0.28.2": + optional: true + + "@esbuild/sunos-x64@0.28.2": + optional: true + + "@esbuild/win32-arm64@0.28.2": + optional: true + + "@esbuild/win32-ia32@0.28.2": + optional: true + + "@esbuild/win32-x64@0.28.2": + optional: true + "@exodus/bytes@1.15.1": {} + "@fontsource-variable/fraunces@5.3.0": {} + + "@fontsource-variable/instrument-sans@5.3.0": {} + "@fontsource/roboto@5.3.0": {} + "@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2))": + dependencies: + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@6.0.3) + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2) + optionalDependencies: + typescript: 6.0.3 + "@jridgewell/gen-mapping@0.3.13": dependencies: "@jridgewell/sourcemap-codec": 1.6.0 "@jridgewell/trace-mapping": 0.3.31 + "@jridgewell/remapping@2.3.5": + dependencies: + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 + "@jridgewell/resolve-uri@3.1.2": {} "@jridgewell/sourcemap-codec@1.6.0": {} @@ -2330,63 +3960,204 @@ snapshots: "@emotion/styled": 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) "@types/react": 19.2.18 - "@mui/private-theming@9.4.0(@types/react@19.2.18)(react@19.2.8)": - dependencies: - "@babel/runtime": 7.29.7 - "@mui/utils": 9.4.0(@types/react@19.2.18)(react@19.2.8) - prop-types: 15.8.1 - react: 19.2.8 - optionalDependencies: - "@types/react": 19.2.18 + "@mui/private-theming@9.4.0(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@babel/runtime": 7.29.7 + "@mui/utils": 9.4.0(@types/react@19.2.18)(react@19.2.8) + prop-types: 15.8.1 + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@mui/styled-engine@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(react@19.2.8)": + dependencies: + "@babel/runtime": 7.29.7 + "@emotion/cache": 11.14.0 + "@emotion/serialize": 1.3.3 + "@emotion/sheet": 1.4.0 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 19.2.8 + optionalDependencies: + "@emotion/react": 11.14.0(@types/react@19.2.18)(react@19.2.8) + "@emotion/styled": 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) + + "@mui/system@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@babel/runtime": 7.29.7 + "@mui/private-theming": 9.4.0(@types/react@19.2.18)(react@19.2.8) + "@mui/styled-engine": 9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) + "@mui/types": 9.4.0(@types/react@19.2.18) + "@mui/utils": 9.4.0(@types/react@19.2.18)(react@19.2.8) + clsx: 2.1.1 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 19.2.8 + optionalDependencies: + "@emotion/react": 11.14.0(@types/react@19.2.18)(react@19.2.8) + "@emotion/styled": 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) + "@types/react": 19.2.18 + + "@mui/types@9.4.0(@types/react@19.2.18)": + dependencies: + "@babel/runtime": 7.29.7 + optionalDependencies: + "@types/react": 19.2.18 + + "@mui/utils@9.4.0(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@babel/runtime": 7.29.7 + "@mui/types": 9.4.0(@types/react@19.2.18) + "@types/prop-types": 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.2.8 + react-is: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@napi-rs/wasm-runtime@1.2.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)": + dependencies: + "@emnapi/core": 1.11.0 + "@emnapi/runtime": 1.11.0 + "@tybys/wasm-util": 0.10.4 + optional: true + + "@napi-rs/wasm-runtime@1.2.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)": + dependencies: + "@emnapi/core": 1.9.2 + "@emnapi/runtime": 1.9.2 + "@tybys/wasm-util": 0.10.4 + optional: true + + "@oxc-parser/binding-android-arm-eabi@0.127.0": + optional: true + + "@oxc-parser/binding-android-arm64@0.127.0": + optional: true + + "@oxc-parser/binding-darwin-arm64@0.127.0": + optional: true + + "@oxc-parser/binding-darwin-x64@0.127.0": + optional: true + + "@oxc-parser/binding-freebsd-x64@0.127.0": + optional: true + + "@oxc-parser/binding-linux-arm-gnueabihf@0.127.0": + optional: true + + "@oxc-parser/binding-linux-arm-musleabihf@0.127.0": + optional: true + + "@oxc-parser/binding-linux-arm64-gnu@0.127.0": + optional: true + + "@oxc-parser/binding-linux-arm64-musl@0.127.0": + optional: true + + "@oxc-parser/binding-linux-ppc64-gnu@0.127.0": + optional: true + + "@oxc-parser/binding-linux-riscv64-gnu@0.127.0": + optional: true + + "@oxc-parser/binding-linux-riscv64-musl@0.127.0": + optional: true + + "@oxc-parser/binding-linux-s390x-gnu@0.127.0": + optional: true + + "@oxc-parser/binding-linux-x64-gnu@0.127.0": + optional: true + + "@oxc-parser/binding-linux-x64-musl@0.127.0": + optional: true + + "@oxc-parser/binding-openharmony-arm64@0.127.0": + optional: true + + "@oxc-parser/binding-wasm32-wasi@0.127.0": + dependencies: + "@emnapi/core": 1.9.2 + "@emnapi/runtime": 1.9.2 + "@napi-rs/wasm-runtime": 1.2.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + "@oxc-parser/binding-win32-arm64-msvc@0.127.0": + optional: true + + "@oxc-parser/binding-win32-ia32-msvc@0.127.0": + optional: true + + "@oxc-parser/binding-win32-x64-msvc@0.127.0": + optional: true + + "@oxc-project/types@0.127.0": {} + + "@oxc-project/types@0.148.0": {} + + "@oxc-resolver/binding-android-arm-eabi@11.21.2": + optional: true + + "@oxc-resolver/binding-android-arm64@11.21.2": + optional: true + + "@oxc-resolver/binding-darwin-arm64@11.21.2": + optional: true + + "@oxc-resolver/binding-darwin-x64@11.21.2": + optional: true + + "@oxc-resolver/binding-freebsd-x64@11.21.2": + optional: true + + "@oxc-resolver/binding-linux-arm-gnueabihf@11.21.2": + optional: true + + "@oxc-resolver/binding-linux-arm-musleabihf@11.21.2": + optional: true + + "@oxc-resolver/binding-linux-arm64-gnu@11.21.2": + optional: true + + "@oxc-resolver/binding-linux-arm64-musl@11.21.2": + optional: true + + "@oxc-resolver/binding-linux-ppc64-gnu@11.21.2": + optional: true + + "@oxc-resolver/binding-linux-riscv64-gnu@11.21.2": + optional: true + + "@oxc-resolver/binding-linux-riscv64-musl@11.21.2": + optional: true + + "@oxc-resolver/binding-linux-s390x-gnu@11.21.2": + optional: true - "@mui/styled-engine@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(react@19.2.8)": - dependencies: - "@babel/runtime": 7.29.7 - "@emotion/cache": 11.14.0 - "@emotion/serialize": 1.3.3 - "@emotion/sheet": 1.4.0 - csstype: 3.2.3 - prop-types: 15.8.1 - react: 19.2.8 - optionalDependencies: - "@emotion/react": 11.14.0(@types/react@19.2.18)(react@19.2.8) - "@emotion/styled": 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) + "@oxc-resolver/binding-linux-x64-gnu@11.21.2": + optional: true - "@mui/system@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8)": - dependencies: - "@babel/runtime": 7.29.7 - "@mui/private-theming": 9.4.0(@types/react@19.2.18)(react@19.2.8) - "@mui/styled-engine": 9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) - "@mui/types": 9.4.0(@types/react@19.2.18) - "@mui/utils": 9.4.0(@types/react@19.2.18)(react@19.2.8) - clsx: 2.1.1 - csstype: 3.2.3 - prop-types: 15.8.1 - react: 19.2.8 - optionalDependencies: - "@emotion/react": 11.14.0(@types/react@19.2.18)(react@19.2.8) - "@emotion/styled": 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) - "@types/react": 19.2.18 + "@oxc-resolver/binding-linux-x64-musl@11.21.2": + optional: true - "@mui/types@9.4.0(@types/react@19.2.18)": - dependencies: - "@babel/runtime": 7.29.7 - optionalDependencies: - "@types/react": 19.2.18 + "@oxc-resolver/binding-openharmony-arm64@11.21.2": + optional: true - "@mui/utils@9.4.0(@types/react@19.2.18)(react@19.2.8)": + "@oxc-resolver/binding-wasm32-wasi@11.21.2": dependencies: - "@babel/runtime": 7.29.7 - "@mui/types": 9.4.0(@types/react@19.2.18) - "@types/prop-types": 15.7.15 - clsx: 2.1.1 - prop-types: 15.8.1 - react: 19.2.8 - react-is: 19.2.8 - optionalDependencies: - "@types/react": 19.2.18 + "@emnapi/core": 1.11.0 + "@emnapi/runtime": 1.11.0 + "@napi-rs/wasm-runtime": 1.2.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) + optional: true - "@oxc-project/types@0.148.0": {} + "@oxc-resolver/binding-win32-arm64-msvc@11.21.2": + optional: true + + "@oxc-resolver/binding-win32-x64-msvc@11.21.2": + optional: true "@oxlint/binding-android-arm-eabi@1.81.0": optional: true @@ -2445,6 +4216,10 @@ snapshots: "@oxlint/binding-win32-x64-msvc@1.81.0": optional: true + "@playwright/test@1.64.1": + dependencies: + playwright: 1.64.1 + "@popperjs/core@2.11.8": {} "@rolldown/binding-android-arm-eabi@1.2.7": @@ -2494,6 +4269,87 @@ snapshots: "@rolldown/pluginutils@1.0.1": {} + "@rollup/pluginutils@5.4.0": + dependencies: + "@types/estree": 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.7 + + "@storybook/addon-a11y@10.6.3(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8))": + dependencies: + "@storybook/global": 5.0.0 + axe-core: 4.14.0 + storybook: 10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8) + + "@storybook/builder-vite@10.6.3(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2))": + dependencies: + storybook: 10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8) + ts-dedent: 2.3.0 + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2) + + "@storybook/global@5.0.0": {} + + "@storybook/icons@2.2.0(react@19.2.8)": + dependencies: + react: 19.2.8 + + "@storybook/react-dom-shim@10.6.3(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8))": + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + storybook: 10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.7(@types/react@19.2.18) + + "@storybook/react-vite@10.6.3(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8))(typescript@6.0.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2))": + dependencies: + "@joshwooding/vite-plugin-react-docgen-typescript": 0.7.0(typescript@6.0.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)) + "@rollup/pluginutils": 5.4.0 + "@storybook/builder-vite": 10.6.3(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)) + "@storybook/react": 10.6.3(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8))(typescript@6.0.3) + empathic: 2.1.0 + magic-string: 1.2.3 + react: 19.2.8 + react-docgen: 8.0.4 + react-dom: 19.2.8(react@19.2.8) + resolve: 1.22.12 + storybook: 10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8) + tsconfig-paths: 4.2.0 + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2) + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - "@types/react" + - "@types/react-dom" + - rollup + - supports-color + + "@storybook/react@10.6.3(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8))(typescript@6.0.3)": + dependencies: + "@storybook/global": 5.0.0 + "@storybook/react-dom-shim": 10.6.3(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8)) + react: 19.2.8 + react-docgen: 8.0.4 + react-docgen-typescript: 2.4.0(typescript@6.0.3) + react-dom: 19.2.8(react@19.2.8) + storybook: 10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.7(@types/react@19.2.18) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + "@tanstack/query-core@5.104.5": {} + + "@tanstack/react-query@5.104.5(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@tanstack/query-core": 5.104.5 + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + "@testing-library/dom@10.4.1": dependencies: "@babel/code-frame": 7.29.7 @@ -2505,7 +4361,16 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - "@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)))": + "@testing-library/jest-dom@6.9.1": + dependencies: + "@adobe/css-tools": 4.5.0 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + "@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)))": dependencies: "@adobe/css-tools": 4.5.0 "@testing-library/dom": 10.4.1 @@ -2515,7 +4380,7 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 optionalDependencies: - vitest: 5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)) + vitest: 5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)) "@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": dependencies: @@ -2531,8 +4396,34 @@ snapshots: dependencies: "@testing-library/dom": 10.4.1 + "@tybys/wasm-util@0.10.4": + dependencies: + tslib: 2.8.1 + optional: true + "@types/aria-query@5.0.4": {} + "@types/babel__core@7.20.5": + dependencies: + "@babel/parser": 7.29.8 + "@babel/types": 7.29.8 + "@types/babel__generator": 7.27.0 + "@types/babel__template": 7.4.4 + "@types/babel__traverse": 7.28.0 + + "@types/babel__generator@7.27.0": + dependencies: + "@babel/types": 7.29.8 + + "@types/babel__template@7.4.4": + dependencies: + "@babel/parser": 7.29.8 + "@babel/types": 7.29.8 + + "@types/babel__traverse@7.28.0": + dependencies: + "@babel/types": 7.29.8 + "@types/chai@5.2.3": dependencies: "@types/deep-eql": 4.0.2 @@ -2540,6 +4431,8 @@ snapshots: "@types/deep-eql@4.0.2": {} + "@types/doctrine@0.0.9": {} + "@types/estree@1.0.9": {} "@types/node@24.13.3": @@ -2562,22 +4455,48 @@ snapshots: dependencies: csstype: 3.2.3 - "@vitejs/plugin-react@6.1.1(vite@8.2.2(@types/node@24.13.3))": + "@types/resolve@1.20.6": {} + + "@vitejs/plugin-react@6.1.1(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2))": dependencies: "@rolldown/pluginutils": 1.0.1 - vite: 8.2.2(@types/node@24.13.3) + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2) + + "@vitest/expect@3.2.4": + dependencies: + "@types/chai": 5.2.3 + "@vitest/spy": 3.2.4 + "@vitest/utils": 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 - "@vitest/mocker@5.0.0(vite@8.2.2(@types/node@24.13.3))": + "@vitest/mocker@5.0.0(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2))": dependencies: "@jridgewell/trace-mapping": 0.3.31 "@vitest/spy": 5.0.0 estree-walker: 3.0.3 magic-string: 1.2.3 optionalDependencies: - vite: 8.2.2(@types/node@24.13.3) + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2) + + "@vitest/pretty-format@3.2.4": + dependencies: + tinyrainbow: 2.0.0 + + "@vitest/spy@3.2.4": + dependencies: + tinyspy: 4.0.6 "@vitest/spy@5.0.0": {} + "@vitest/utils@3.2.4": + dependencies: + "@vitest/pretty-format": 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + "@webcontainer/env@1.1.1": {} + ansi-regex@5.0.1: {} ansi-styles@5.2.0: {} @@ -2590,24 +4509,64 @@ snapshots: assertion-error@2.0.1: {} + ast-types@0.16.3: + dependencies: + tslib: 2.8.1 + + axe-core@4.14.0: {} + babel-plugin-macros@3.1.0: dependencies: "@babel/runtime": 7.29.7 cosmiconfig: 7.1.0 resolve: 1.22.12 + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.30: {} + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.29.4: + dependencies: + baseline-browser-mapping: 2.11.30 + caniuse-lite: 1.0.30001817 + electron-to-chromium: 1.5.462 + node-releases: 2.0.59 + update-browserslist-db: 1.3.4(browserslist@4.29.4) + + bundle-name@4.1.1: + dependencies: + run-applescript: 7.1.0 + callsites@3.1.0: {} + caniuse-lite@1.0.30001817: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chai@6.2.2: {} + check-error@2.1.3: {} + clsx@2.1.1: {} convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} + cosmiconfig@7.1.0: dependencies: "@types/parse-json": 4.0.2 @@ -2638,10 +4597,25 @@ snapshots: decimal.js@10.6.0: {} + deep-eql@5.0.2: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.1: + dependencies: + bundle-name: 4.1.1 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -2651,6 +4625,10 @@ snapshots: "@babel/runtime": 7.29.7 csstype: 3.2.3 + electron-to-chromium@1.5.462: {} + + empathic@2.1.0: {} + entities@8.0.0: {} error-ex@1.3.4: @@ -2661,12 +4639,49 @@ snapshots: es-module-lexer@2.3.2: {} + esbuild@0.28.2: + optionalDependencies: + "@esbuild/aix-ppc64": 0.28.2 + "@esbuild/android-arm": 0.28.2 + "@esbuild/android-arm64": 0.28.2 + "@esbuild/android-x64": 0.28.2 + "@esbuild/darwin-arm64": 0.28.2 + "@esbuild/darwin-x64": 0.28.2 + "@esbuild/freebsd-arm64": 0.28.2 + "@esbuild/freebsd-x64": 0.28.2 + "@esbuild/linux-arm": 0.28.2 + "@esbuild/linux-arm64": 0.28.2 + "@esbuild/linux-ia32": 0.28.2 + "@esbuild/linux-loong64": 0.28.2 + "@esbuild/linux-mips64el": 0.28.2 + "@esbuild/linux-ppc64": 0.28.2 + "@esbuild/linux-riscv64": 0.28.2 + "@esbuild/linux-s390x": 0.28.2 + "@esbuild/linux-x64": 0.28.2 + "@esbuild/netbsd-arm64": 0.28.2 + "@esbuild/netbsd-x64": 0.28.2 + "@esbuild/openbsd-arm64": 0.28.2 + "@esbuild/openbsd-x64": 0.28.2 + "@esbuild/openharmony-arm64": 0.28.2 + "@esbuild/sunos-x64": 0.28.2 + "@esbuild/win32-arm64": 0.28.2 + "@esbuild/win32-ia32": 0.28.2 + "@esbuild/win32-x64": 0.28.2 + + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: "@types/estree": 1.0.9 + esutils@2.0.3: {} + expect-type@1.4.0: {} fdir@6.5.0(picomatch@4.0.7): @@ -2675,11 +4690,28 @@ snapshots: find-root@1.1.0: {} + framer-motion@14.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + motion-dom: 14.4.0 + motion-utils: 14.1.1 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -2707,8 +4739,18 @@ snapshots: dependencies: hasown: 2.0.4 + is-docker@3.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-potential-custom-element-name@1.0.1: {} + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + js-tokens@4.0.0: {} jsdom@30.0.1: @@ -2741,6 +4783,10 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + jwt-decode@4.0.0: {} lightningcss-android-arm64@1.33.0: @@ -2798,8 +4844,14 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lz-string@1.5.0: {} magic-string@1.2.3: @@ -2810,10 +4862,34 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + motion-dom@14.4.0: + dependencies: + motion-utils: 14.1.1 + + motion-utils@14.1.1: {} + + motion@14.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + framer-motion: 14.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + ms@2.1.3: {} nanoid@3.3.18: {} + node-releases@2.0.59: {} + object-assign@4.1.1: {} obug@2.1.4: {} @@ -2822,6 +4898,60 @@ snapshots: dependencies: jwt-decode: 4.0.0 + open@10.2.0: + dependencies: + default-browser: 5.5.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + oxc-parser@0.127.0: + dependencies: + "@oxc-project/types": 0.127.0 + optionalDependencies: + "@oxc-parser/binding-android-arm-eabi": 0.127.0 + "@oxc-parser/binding-android-arm64": 0.127.0 + "@oxc-parser/binding-darwin-arm64": 0.127.0 + "@oxc-parser/binding-darwin-x64": 0.127.0 + "@oxc-parser/binding-freebsd-x64": 0.127.0 + "@oxc-parser/binding-linux-arm-gnueabihf": 0.127.0 + "@oxc-parser/binding-linux-arm-musleabihf": 0.127.0 + "@oxc-parser/binding-linux-arm64-gnu": 0.127.0 + "@oxc-parser/binding-linux-arm64-musl": 0.127.0 + "@oxc-parser/binding-linux-ppc64-gnu": 0.127.0 + "@oxc-parser/binding-linux-riscv64-gnu": 0.127.0 + "@oxc-parser/binding-linux-riscv64-musl": 0.127.0 + "@oxc-parser/binding-linux-s390x-gnu": 0.127.0 + "@oxc-parser/binding-linux-x64-gnu": 0.127.0 + "@oxc-parser/binding-linux-x64-musl": 0.127.0 + "@oxc-parser/binding-openharmony-arm64": 0.127.0 + "@oxc-parser/binding-wasm32-wasi": 0.127.0 + "@oxc-parser/binding-win32-arm64-msvc": 0.127.0 + "@oxc-parser/binding-win32-ia32-msvc": 0.127.0 + "@oxc-parser/binding-win32-x64-msvc": 0.127.0 + + oxc-resolver@11.21.2: + optionalDependencies: + "@oxc-resolver/binding-android-arm-eabi": 11.21.2 + "@oxc-resolver/binding-android-arm64": 11.21.2 + "@oxc-resolver/binding-darwin-arm64": 11.21.2 + "@oxc-resolver/binding-darwin-x64": 11.21.2 + "@oxc-resolver/binding-freebsd-x64": 11.21.2 + "@oxc-resolver/binding-linux-arm-gnueabihf": 11.21.2 + "@oxc-resolver/binding-linux-arm-musleabihf": 11.21.2 + "@oxc-resolver/binding-linux-arm64-gnu": 11.21.2 + "@oxc-resolver/binding-linux-arm64-musl": 11.21.2 + "@oxc-resolver/binding-linux-ppc64-gnu": 11.21.2 + "@oxc-resolver/binding-linux-riscv64-gnu": 11.21.2 + "@oxc-resolver/binding-linux-riscv64-musl": 11.21.2 + "@oxc-resolver/binding-linux-s390x-gnu": 11.21.2 + "@oxc-resolver/binding-linux-x64-gnu": 11.21.2 + "@oxc-resolver/binding-linux-x64-musl": 11.21.2 + "@oxc-resolver/binding-openharmony-arm64": 11.21.2 + "@oxc-resolver/binding-wasm32-wasi": 11.21.2 + "@oxc-resolver/binding-win32-arm64-msvc": 11.21.2 + "@oxc-resolver/binding-win32-x64-msvc": 11.21.2 + oxlint@1.81.0: optionalDependencies: "@oxlint/binding-android-arm-eabi": 1.81.0 @@ -2861,12 +4991,25 @@ snapshots: path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + path-type@4.0.0: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@4.0.7: {} + playwright-core@1.64.1: {} + + playwright@1.64.1: + dependencies: + playwright-core: 1.64.1 + postcss@8.5.28: dependencies: nanoid: 3.3.18 @@ -2889,6 +5032,25 @@ snapshots: punycode@2.3.1: {} + react-docgen-typescript@2.4.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + react-docgen@8.0.4: + dependencies: + "@babel/core": 7.29.7 + "@babel/traverse": 7.29.8 + "@babel/types": 7.29.8 + "@types/babel__core": 7.20.5 + "@types/babel__traverse": 7.28.0 + "@types/doctrine": 0.0.9 + "@types/resolve": 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.12 + strip-indent: 4.1.1 + transitivePeerDependencies: + - supports-color + react-dom@19.2.8(react@19.2.8): dependencies: react: 19.2.8 @@ -2916,6 +5078,14 @@ snapshots: react@19.2.8: {} + recast@0.23.21: + dependencies: + ast-types: 0.16.3 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -2953,32 +5123,73 @@ snapshots: "@rolldown/binding-win32-arm64-msvc": 1.2.7 "@rolldown/binding-win32-x64-msvc": 1.2.7 + run-applescript@7.1.0: {} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 scheduler@0.27.0: {} + semver@6.3.1: {} + + semver@7.8.5: {} + siginfo@2.0.0: {} source-map-js@1.2.1: {} source-map@0.5.7: {} + source-map@0.6.1: {} + stackback@0.0.2: {} std-env@4.2.0: {} + storybook@10.6.3(@types/react@19.2.18)(prettier@3.9.6)(react@19.2.8): + dependencies: + "@storybook/global": 5.0.0 + "@storybook/icons": 2.2.0(react@19.2.8) + "@testing-library/dom": 10.4.1 + "@testing-library/jest-dom": 6.9.1 + "@testing-library/user-event": 14.6.7(@testing-library/dom@10.4.1) + "@vitest/expect": 3.2.4 + "@vitest/spy": 3.2.4 + "@webcontainer/env": 1.1.1 + esbuild: 0.28.2 + jsonc-parser: 3.3.1 + open: 10.2.0 + oxc-parser: 0.127.0 + oxc-resolver: 11.21.2 + recast: 0.23.21 + semver: 7.8.5 + use-sync-external-store: 1.7.0(react@19.2.8) + ws: 8.22.0 + optionalDependencies: + "@types/react": 19.2.18 + prettier: 3.9.6 + transitivePeerDependencies: + - bufferutil + - react + - utf-8-validate + + strip-bom@3.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 + strip-indent@4.1.1: {} + stylis@4.2.0: {} supports-preserve-symlinks-flag@1.0.0: {} symbol-tree@3.2.4: {} + tiny-invariant@1.3.3: {} + tinybench@6.1.4: {} tinyexec@1.3.0: {} @@ -2988,6 +5199,10 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + tldts-core@7.4.11: {} tldts@7.4.11: @@ -3002,13 +5217,33 @@ snapshots: dependencies: punycode: 2.3.1 + ts-dedent@2.3.0: {} + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + typescript@6.0.3: {} undici-types@7.18.2: {} undici@8.10.2: {} - vite@8.2.2(@types/node@24.13.3): + update-browserslist-db@1.3.4(browserslist@4.29.4): + dependencies: + browserslist: 4.29.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.7.0(react@19.2.8): + dependencies: + react: 19.2.8 + + vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 @@ -3017,12 +5252,13 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: "@types/node": 24.13.3 + esbuild: 0.28.2 fsevents: 2.3.3 - vitest@5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)): + vitest@5.0.0(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)): dependencies: "@types/chai": 5.2.3 - "@vitest/mocker": 5.0.0(vite@8.2.2(@types/node@24.13.3)) + "@vitest/mocker": 5.0.0(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)) chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -3033,7 +5269,7 @@ snapshots: tinybench: 6.1.4 tinyexec: 1.3.0 tinyglobby: 0.2.17 - vite: 8.2.2(@types/node@24.13.3) + vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2) why-is-node-running: 2.3.0 optionalDependencies: "@types/node": 24.13.3 @@ -3070,8 +5306,16 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + ws@8.22.0: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} + yallist@3.1.1: {} + yaml@1.10.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..36b6af9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +allowBuilds: + # esbuild (pulled in by Storybook) ships its binary via per-platform optional + # packages; its postinstall is only a fast-path optimisation. + esbuild: false diff --git a/scripts/README.md b/scripts/README.md index 55f23a1..5c5530f 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,31 +1,43 @@ # scripts/ -- `develop.sh` — the Dockerfile's `develop`-stage setup: installs `prek`, - the Claude Code CLI, and `snip`, the only tooling this template requires +- `develop.sh` — `../.devcontainer/Dockerfile`'s `develop`-stage setup: installs `prek`, + the Claude Code CLI, `snip` and the GitHub CLI (`gh`), the only tooling this template requires regardless of instance language. An instance adds its own language runtime/tooling install on top, either by extending this script or - copying its shape into a second one invoked from the instance's own - `Dockerfile` layer. Prefer the hook below over either. + copying its shape into a second script. Prefer the hook below over + either. - `post-create.sh` — the devcontainer's `postCreateCommand`: git `safe.directory`, `prek install`, then every `post-create.d/*.sh`. -- `develop.d/`, `post-create.d/` — instance hook directories, empty apart - from their own `README.md`; see `docs/TEMPLATE.md`'s "Instance extension - points". +- `develop.d/`, `post-create.d/`, `runner-setup.d/` — instance hook + directories, empty apart from their own `README.md`; see + `docs/TEMPLATE.md`'s "Instance extension points". +- `runner-setup.sh` — the top-level `Dockerfile`'s `runner` stage setup: + group-0 runtime user for arbitrary-UID platforms (run before copying + app code, which is then copied read-only), then every + `runner-setup.d/*.sh`. +- `image-scan.sh` — the manual-stage `image-scan` hook: Trivy over the + tars in `dist/`. +- `image-smoke.sh` — `make image-smoke` (a `release-check` prerequisite): + converts the arch's image in the release OCI tar from `dist/` to a + docker-archive with the pinned skopeo (`docker load` can't read a buildx + OCI tar on the classic image store), loads and runs it, and polls + `IMAGE_SMOKE_PATH` until 2xx. Needs `SKOPEO_IMAGE`, which the Makefile + exports; usage in the script's header. +- `skopeo.sh` — runs the pinned skopeo image as the caller's user with + `dist/` mounted at `/dist`; used by `make publish` and `image-smoke.sh`. +- `check-dockerfile-versions.sh ARG...` — fails when a named `ARG` default + differs between `.devcontainer/Dockerfile` and the top-level `Dockerfile`. - `develop.d/10-corepack.sh` — enables pnpm via Corepack in the devcontainer image; `post-create.d/10-pnpm-install.sh` — installs the app's dependencies. Both template-react's own, run by the hooks above. -- `check-dockerfile-versions.sh` — prek hook failing when `NODE_VERSION`/ - `DEBIAN_VERSION` differ between the base-owned `Dockerfile` and - `../app.Dockerfile`, so the bundle is built with the Node the - devcontainer tests against. - `docker/` — the runtime image's own assets: `nginx.conf.template` and `docker-entrypoint.d/20-envsubst-app-config.sh`, both `COPY`'d into - `../app.Dockerfile`'s `runner` stage. + the top-level `Dockerfile`'s `runner` stage. ## Do - Keep this script's own tools pinned to an exact version (a `# renovate:` - ARG in the Dockerfile) and checksum-verified where the upstream project + ARG in `../.devcontainer/Dockerfile`) and checksum-verified where the upstream project publishes checksums, matching the pattern each install step here already follows. diff --git a/scripts/check-dockerfile-versions.sh b/scripts/check-dockerfile-versions.sh index 722c886..1ff9139 100755 --- a/scripts/check-dockerfile-versions.sh +++ b/scripts/check-dockerfile-versions.sh @@ -1,17 +1,49 @@ #!/usr/bin/env bash -# NODE_VERSION and DEBIAN_VERSION are declared in both the base-owned -# Dockerfile (develop stage) and app.Dockerfile (builder stage); the -# runtime bundle must be built with the Node the devcontainer tests -# against. Exits 1 if either differs. +# Usage: check-dockerfile-versions.sh ARG [ARG...] +# +# Each named ARG must be declared (`ARG NAME=value`) with the same default +# in both the base-owned .devcontainer/Dockerfile (develop stage) and the +# top-level Dockerfile (builder/runner stages): the runtime image must match +# the toolchain the devcontainer tests against. Values are compared after stripping trailing comments, whitespace and +# surrounding quotes. Exits 1 if one differs, is missing from either file or +# is declared without a default. ARGs not named are not checked. set -euo pipefail +if [[ $# -eq 0 ]]; then + echo "usage: $0 ARG [ARG...]" >&2 + exit 2 +fi + +# Prints the normalized default of `ARG name=...` in file $2, or exits: +# 0 with the value, 3 when the ARG is not declared, 4 when it is declared +# without a default. Normalizing strips a trailing comment, surrounding +# whitespace and one pair of surrounding quotes. +default_of() { + local line + line="$(grep -m1 -E "^ARG[[:space:]]+$1([=[:space:]]|\$)" "$2")" || return 3 + [[ "$line" == *=* ]] || return 4 + line="${line#*=}" + line="$(sed -E 's/[[:space:]]+#.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/' <<<"$line")" + printf '%s\n' "$line" +} + status=0 -for arg in NODE_VERSION DEBIAN_VERSION; do - dev="$(grep -m1 "^ARG ${arg}=" Dockerfile | cut -d= -f2-)" - app="$(grep -m1 "^ARG ${arg}=" app.Dockerfile | cut -d= -f2-)" - if [[ "$dev" != "$app" ]]; then - echo "${arg} mismatch: Dockerfile has '${dev}', app.Dockerfile has '${app}'" >&2 - status=1 +for arg in "$@"; do + ok=1 + values=() + for f in .devcontainer/Dockerfile Dockerfile; do + rc=0 + v="$(default_of "$arg" "$f")" || rc=$? + case $rc in + 0) values+=("$v") ;; + 3) echo "${arg} missing: not declared in $f" >&2; ok=0 ;; + *) echo "${arg} declared without a default in $f" >&2; ok=0 ;; + esac + done + if [[ $ok -eq 1 && "${values[0]}" != "${values[1]}" ]]; then + echo "${arg} mismatch: .devcontainer/Dockerfile has '${values[0]}', Dockerfile has '${values[1]}'" >&2 + ok=0 fi + [[ $ok -eq 1 ]] || status=1 done exit "$status" diff --git a/scripts/develop.d/10-corepack.sh b/scripts/develop.d/10-corepack.sh index 9e3ba62..874949f 100755 --- a/scripts/develop.d/10-corepack.sh +++ b/scripts/develop.d/10-corepack.sh @@ -2,7 +2,7 @@ # Enables pnpm via Corepack (bundled with the Node.js develop.sh installs): # the prek hooks and CI invoke `pnpm` directly, and Corepack reads the # pinned version from package.json's `packageManager` field -- matching -# app.Dockerfile's builder stage. +# the top-level Dockerfile's builder stage. set -euo pipefail node_bin_dir="$(dirname "$(readlink -f "$(command -v node)")")" diff --git a/scripts/develop.sh b/scripts/develop.sh index 265e7d0..ad93c3b 100644 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -13,6 +13,7 @@ snip_version=$3 uv_version=$4 python_version=$5 node_version=$6 +gh_version=$7 # xz-utils: Node's own release tarballs are .tar.xz. apt-get update @@ -44,7 +45,7 @@ ln -s "$python_bin" /usr/local/bin/python # directly from nodejs.org's own release tarballs (checksum-verified # against that release's published SHASUMS256.txt) rather than a # devcontainer feature, so every tool this stage installs is pinned the -# same way, in the same place -- see the Dockerfile's NODE_VERSION ARG. +# same way, in the same place -- see .devcontainer/Dockerfile's NODE_VERSION ARG. node_dpkg_arch="$(dpkg --print-architecture)" case "$node_dpkg_arch" in amd64) node_arch=x64 ;; @@ -97,3 +98,24 @@ tar -xzf "${snip_tmpdir}/${snip_asset}" -C "$snip_tmpdir" snip sudo -u vscode install -Dm755 "${snip_tmpdir}/snip" /home/vscode/.local/bin/snip rm -rf "$snip_tmpdir" ln -s /home/vscode/.local/bin/snip /usr/local/bin/snip + +# GitHub CLI: CLAUDE.md's workflow drives GitHub (PRs, workflow runs, +# releases) through `gh`, so it's part of the base toolset rather than a +# per-instance add-on. Fetched as a release tarball and checksum-verified +# against the release's own checksums file, like snip above. Its login +# state lives in the gh-config volume (.devcontainer/compose.yml), so a +# rebuild doesn't require `gh auth login` again. +gh_arch="$(dpkg --print-architecture)" +gh_asset="gh_${gh_version}_linux_${gh_arch}" +gh_tmpdir="$(mktemp -d)" +curl -LsSf -o "${gh_tmpdir}/${gh_asset}.tar.gz" \ + "https://github.com/cli/cli/releases/download/v${gh_version}/${gh_asset}.tar.gz" +curl -LsSf -o "${gh_tmpdir}/checksums.txt" \ + "https://github.com/cli/cli/releases/download/v${gh_version}/gh_${gh_version}_checksums.txt" +(cd "$gh_tmpdir" && grep " ${gh_asset}.tar.gz\$" checksums.txt | sha256sum -c -) +tar -xzf "${gh_tmpdir}/${gh_asset}.tar.gz" -C "$gh_tmpdir" "${gh_asset}/bin/gh" +install -Dm755 "${gh_tmpdir}/${gh_asset}/bin/gh" /usr/local/bin/gh +rm -rf "$gh_tmpdir" +# Pre-create the gh-config volume's mount point owned by vscode, so a new +# named volume is initialized with that ownership instead of root's. +sudo -u vscode mkdir -p /home/vscode/.config/gh diff --git a/scripts/image-scan.sh b/scripts/image-scan.sh new file mode 100755 index 0000000..6353ded --- /dev/null +++ b/scripts/image-scan.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Scans every image tar `make build` left in dist/ for HIGH/CRITICAL +# vulnerabilities that have a fix, with Trivy. Skips when dist/ has no tar. +# Run by the manual-stage `image-scan` pre-commit hook; needs Docker and +# network access (the vulnerability DB is cached in the trivy-cache volume). +# +# Trivy reads a `docker save` tar or an OCI layout *directory*, not the OCI +# layout tar buildx writes, so those tars are unpacked first. The arch in its +# name (-.tar) selects the image manifest, so a buildx +# provenance/SBOM attestation manifest in the same tar is never scanned +# instead of the image. +# +# The Trivy image is pinned by digest as well as tag, as the mitigation for +# the March 2026 compromise of Trivy's releases and images +# (GHSA-69fq-xp46-6x23). Renovate bumps both together. +set -euo pipefail + +trivy=aquasec/trivy:0.76.0@sha256:2416d1f74045b57274df1a340eee6b510cacd73c66a6ccb8da092b117eb7d222 + +shopt -s nullglob +tars=(dist/*.tar) +if [[ ${#tars[@]} -eq 0 ]]; then + echo "image-scan: no image tar in dist/, skipping" + exit 0 +fi + +# Under dist/ (gitignored), not /tmp, so the path also exists for a Docker +# daemon that shares the workspace but not the caller's /tmp. +work="$(mktemp -d dist/.image-scan.XXXXXX)" +trap 'rm -rf "$work"' EXIT + +status=0 +for t in "${tars[@]}"; do + name="$(basename "$t" .tar)" + arch="${name##*-}" + echo "image-scan: $t (linux/$arch)" + if tar -tf "$t" manifest.json >/dev/null 2>&1; then + # A `docker save` tar: Trivy reads it as is. + mount=("$PWD/$t:/image.tar:ro") + input=/image.tar + else + layout="$PWD/$work/$name" + mkdir -p "$layout" + tar -xf "$t" -C "$layout" + chmod -R a+rX "$layout" + mount=("$layout:/image:ro") + input=/image + fi + docker run --rm -v trivy-cache:/root/.cache -v "${mount[0]}" "$trivy" \ + image --input "$input" --platform "linux/$arch" \ + --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 || status=1 +done +exit "$status" diff --git a/scripts/image-smoke.sh b/scripts/image-smoke.sh new file mode 100755 index 0000000..8b4d837 --- /dev/null +++ b/scripts/image-smoke.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Loads the release image tar that `make build` left in dist/, runs it and +# polls a URL until it answers 2xx. Always removes the container; prints its +# logs on failure. +# +# Usage: image-smoke.sh (configured by environment; `make image-smoke` +# sets these) +# SKOPEO_IMAGE required (the Makefile sets it); converts the tar for docker load +# IMAGE_TAR_PREFIX required; tar is "$IMAGE_TAR_PREFIX-.tar" +# (e.g. dist/template-docker-1.2.3) +# RELEASE_VERSION if that tar is missing (the image name may differ +# when the repository name can't be derived), the one +# dist/*-$RELEASE_VERSION-.tar is used instead +# RELEASE_ARCH arch to test; default: the host's. Unset and no tar +# for that arch: print a notice and exit 0 (the +# per-arch run in the build job already covered it) +# IMAGE_SMOKE_PORT container port (default 8000) +# IMAGE_SMOKE_PATH URL path (default /health/live) +# IMAGE_SMOKE_RUN_ARGS extra `docker run` words, whitespace-separated and +# without embedded spaces or quotes, e.g. "-e FOO=bar" +# (read from the environment, never from recipe text) +# IMAGE_SMOKE_TIMEOUT seconds to wait (default 60) +set -euo pipefail + +prefix="${IMAGE_TAR_PREFIX:?IMAGE_TAR_PREFIX is required}" +port="${IMAGE_SMOKE_PORT:-8000}" +path="${IMAGE_SMOKE_PATH:-/health/live}" +timeout="${IMAGE_SMOKE_TIMEOUT:-60}" +read -r -a run_args <<<"${IMAGE_SMOKE_RUN_ARGS:-}" + +case "$(uname -m)" in + aarch64 | arm64) host_arch=arm64 ;; + *) host_arch=amd64 ;; +esac +arch="${RELEASE_ARCH:-$host_arch}" +tar="$prefix-$arch.tar" +if [[ ! -f "$tar" && -n "${RELEASE_VERSION:-}" ]]; then + shopt -s nullglob + candidates=("$(dirname "$prefix")"/*-"$RELEASE_VERSION-$arch".tar) + if [[ ${#candidates[@]} -eq 1 ]]; then + tar="${candidates[0]}" + fi +fi +if [[ ! -f "$tar" ]]; then + if [[ -z "${RELEASE_ARCH:-}" ]]; then + echo "image-smoke: no $arch image in $(dirname "$prefix") for this host; the per-arch smoke already ran in the build job. Skipping." + exit 0 + fi + echo "image-smoke: $tar not found; run 'make build' first." >&2 + exit 1 +fi + +cid="" +work="" +image="" +# shellcheck disable=SC2329 # invoked by the EXIT trap below +cleanup() { + status=$? + set +e + if [[ -n "$cid" ]]; then + if [[ $status -ne 0 ]]; then + echo "image-smoke: container logs:" >&2 + docker logs "$cid" >&2 || true + fi + docker rm -f "$cid" >/dev/null 2>&1 || true + fi + [[ -z "$image" ]] || docker image rm "$image" >/dev/null 2>&1 || true + [[ -z "$work" ]] || rm -rf "$work" || true + exit "$status" +} +trap cleanup EXIT + +# `docker load` can't read the OCI-layout tar buildx writes (no manifest.json; +# with attestations the index nests another index), so skopeo converts the +# one image for $arch into a docker-archive first. Both live under dist/, +# which is mounted at /dist, so the paths also exist for a Docker daemon +# that shares the workspace but not this process's /tmp. +: "${SKOPEO_IMAGE:?SKOPEO_IMAGE is required (set by the Makefile)}" +echo "image-smoke: loading $tar (linux/$arch)" +tar_dir="$(cd "$(dirname "$tar")" && pwd)" +work="$(mktemp -d "$tar_dir/.image-smoke.XXXXXX")" +# mktemp makes the directory 0700; with rootless or userns-remapped Docker +# the container's user can't write into it. It only exists inside dist/, for +# the length of this script. +chmod 0777 "$work" +# Run (and later remove) a private reference, so cleanup never touches an +# image that already existed. +image="image-smoke-$RANDOM$RANDOM:smoke" +SKOPEO_DIST_DIR="$tar_dir" bash "$(dirname "${BASH_SOURCE[0]}")/skopeo.sh" -- \ + --override-os linux --override-arch "$arch" \ + copy "oci-archive:/dist/$(basename "$tar")" \ + "docker-archive:/dist/$(basename "$work")/image.tar:$image" +docker load -i "$work/image.tar" + +cid="$(docker run -d -p "127.0.0.1::${port}" "${run_args[@]}" "$image")" +mapped="$(docker port "$cid" "${port}/tcp" | head -n1 | sed 's/.*://')" +url="http://127.0.0.1:${mapped}${path}" + +echo "image-smoke: waiting up to ${timeout}s for $url" +deadline=$((SECONDS + timeout)) +while ((SECONDS < deadline)); do + if [[ "$(docker inspect -f '{{.State.Running}}' "$cid")" != "true" ]]; then + echo "image-smoke: container exited before answering" >&2 + exit 1 + fi + if curl -fsS -o /dev/null "$url" 2>/dev/null; then + echo "image-smoke: $url answered 2xx" + exit 0 + fi + sleep 1 +done +echo "image-smoke: timed out after ${timeout}s waiting for $url" >&2 +exit 1 diff --git a/scripts/runner-setup.d/README.md b/scripts/runner-setup.d/README.md new file mode 100644 index 0000000..814a87a --- /dev/null +++ b/scripts/runner-setup.d/README.md @@ -0,0 +1,24 @@ +# scripts/runner-setup.d/ + +Instance hook directory for the runner stage of the top-level +`Dockerfile`. `../runner-setup.sh` (template-owned, `replace` tier) creates +the group-0 runtime user, makes the home directory and `/app/data` +group-writable (and nothing else under `/app`), then runs every `*.sh` here in glob order as +`bash NN-name.sh APP_UID`, still as root, in the same `RUN` layer. + +Empty apart from this file in the template. An instance adds files such as +`10-ca-certificates.sh` (install extra packages, create extra writable +directories) instead of editing `runner-setup.sh`. + +## Do + +- Number files (`NN-name.sh`) so the order is explicit. +- Use `set -euo pipefail` and give any extra *writable* directory (cache, + uploads) group-0 ownership and `g+rwX`, as `runner-setup.sh` does for + `/app/data`. App code is not writable: the `Dockerfile` copies it after + this runs, with `--chown=:0` and a read-only `--chmod`. + +## Don't + +- Edit `../runner-setup.sh`; a template sync overwrites it. +- Put build-stage steps here; this runs in the `runner` stage only. diff --git a/scripts/runner-setup.sh b/scripts/runner-setup.sh new file mode 100755 index 0000000..bcbeba6 --- /dev/null +++ b/scripts/runner-setup.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Prepares the runner stage's runtime user for arbitrary-UID platforms +# (e.g. OpenShift, which ignores an image's own UID and always runs it +# under group 0), and a numeric (not named) final USER in the Dockerfile. +# +# Run it BEFORE copying app content. It creates the user and group 0 +# ownership only where the app writes at runtime: the home directory and +# /app/data (group read/write/execute). /app itself stays root-owned and not +# writable. Then COPY app code with `--chown=:0 --chmod=...` as +# read-only (e.g. `--chmod=u=rX,g=rX,o=`: 0550 directories, 0440 files), so it +# needs no recursive chgrp/chmod (which would copy every file into a new +# layer) and the runtime user can't modify it. Other writable directories +# (caches, upload dirs) are created by runner-setup.d hooks. +# +# Usage: runner-setup.sh APP_UID +# +# Afterwards runs every runner-setup.d/*.sh next to this script, in glob +# order, as `bash NN-name.sh APP_UID`; see runner-setup.d/README.md. +set -euo pipefail + +app_uid=$1 +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +useradd --create-home --uid "$app_uid" --gid 0 appuser +chgrp -R 0 /home/appuser +chmod -R g+rwX /home/appuser + +install -d -m 0755 /app +install -d -o "$app_uid" -g 0 -m 0770 /app/data + +shopt -s nullglob +for hook in "$here"/runner-setup.d/*.sh; do + bash "$hook" "$app_uid" +done diff --git a/scripts/skopeo.sh b/scripts/skopeo.sh new file mode 100755 index 0000000..74edcbc --- /dev/null +++ b/scripts/skopeo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Runs the pinned skopeo image (SKOPEO_IMAGE, set by the Makefile) as the +# caller's user with HOME=/tmp, and dist/ mounted at /dist. Shared by +# `make publish` and image-smoke.sh so the invocation lives in one place. +# +# Usage: skopeo.sh [DOCKER_RUN_ARGS...] -- SKOPEO_ARGS... +# DOCKER_RUN_ARGS extra `docker run` words before `--` (e.g. -i, +# -v "$auth:/auth:ro") +# SKOPEO_ARGS passed to skopeo (e.g. copy oci-archive:/dist/x.tar ...) +# SKOPEO_DIST_DIR directory to mount at /dist (default: ./dist) +set -euo pipefail + +: "${SKOPEO_IMAGE:?SKOPEO_IMAGE is required (set by the Makefile)}" + +docker_args=() +while [[ $# -gt 0 && "$1" != "--" ]]; do + docker_args+=("$1") + shift +done +[[ $# -gt 0 ]] || { echo "usage: $0 [DOCKER_RUN_ARGS...] -- SKOPEO_ARGS..." >&2; exit 2; } +shift + +dist="$(cd "${SKOPEO_DIST_DIR:-dist}" && pwd)" + +exec docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp \ + -v "$dist:/dist" "${docker_args[@]}" "$SKOPEO_IMAGE" "$@" From 80ce5b57a2416701576e01bc12a51e0f7793911a Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:46:35 +0000 Subject: [PATCH 02/21] fix(image.mk): mark unit-tests .PHONY --- image.mk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/image.mk b/image.mk index 178a467..d728e1e 100644 --- a/image.mk +++ b/image.mk @@ -18,6 +18,8 @@ IMAGE_SMOKE_RUN_ARGS := -e API_PROXY_TARGET=http://127.0.0.1:8000 # The unit tests that were release-check's whole recipe before the image # smoke test joined it as a prerequisite. +# .PHONY so a directory named unit-tests can't make Make skip the tests. +.PHONY: unit-tests release-check: unit-tests unit-tests: pnpm test From 25fa89c28e54e945d903f7c7b47a677fa1c92af9 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:06:49 +0000 Subject: [PATCH 03/21] feat(theme): Field Ledger design system and design-token lint hook - src/theme: MUI CSS-variable theme (light/dark, data selector), tokens, fonts, motion variants, ThemeProvider, useColorMode, contrast test - scripts/check-design-tokens.sh + design-tokens hook with self-test - docs/design principles, ADR 0004, manifest classification --- .github/template-sync-manifest.yml | 2 + .pre-commit-config.yaml | 10 ++ README.md | 4 + .../adrs/0004-design-system-and-token-lint.md | 60 ++++++++++ docs/design/README.md | 107 +++++++++++++++++ scripts/check-design-tokens.sh | 82 +++++++++++++ src/App.tsx | 2 +- src/main.tsx | 11 +- src/theme/ThemeProvider.tsx | 16 +++ src/theme/contrast.test.ts | 49 ++++++++ src/theme/fonts.ts | 4 + src/theme/index.ts | 5 + src/theme/motion.ts | 27 +++++ src/theme/theme.ts | 109 +++++++++++++++++- src/theme/tokens.ts | 84 ++++++++++++++ src/theme/useColorMode.ts | 20 ++++ 16 files changed, 578 insertions(+), 14 deletions(-) create mode 100644 docs/adrs/0004-design-system-and-token-lint.md create mode 100644 docs/design/README.md create mode 100755 scripts/check-design-tokens.sh create mode 100644 src/theme/ThemeProvider.tsx create mode 100644 src/theme/contrast.test.ts create mode 100644 src/theme/fonts.ts create mode 100644 src/theme/index.ts create mode 100644 src/theme/motion.ts create mode 100644 src/theme/tokens.ts create mode 100644 src/theme/useColorMode.ts diff --git a/.github/template-sync-manifest.yml b/.github/template-sync-manifest.yml index b4d1186..1c4ca46 100644 --- a/.github/template-sync-manifest.yml +++ b/.github/template-sync-manifest.yml @@ -106,6 +106,7 @@ replace: - scripts/docker/ - scripts/develop.d/10-corepack.sh - scripts/post-create.d/10-pnpm-install.sh + - scripts/check-design-tokens.sh - .vscode/launch.json - .prettierignore - .oxlintrc.json @@ -140,6 +141,7 @@ ignore: - docs/glossary.md - docs/stakeholders.md - docs/adrs/[0-9]*.md + - docs/design/ - docs/frs/FR-*.md - docs/nfrs/NFR-*.md - docs/plans/[0-9]*.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7285a4a..fc177e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -94,6 +94,16 @@ repos: always_run: true stages: [pre-push, manual] + # Colors and sizes live only in src/theme/ (ADR 0004). The self-test + # runs the checker against fixtures so the hook itself can't rot. + - id: design-tokens + name: design-tokens (no raw colors or px outside src/theme/) + entry: bash -c 'bash scripts/check-design-tokens.sh --self-test && bash scripts/check-design-tokens.sh src' + language: system + pass_filenames: false + files: ^(src/.*\.(ts|tsx|css)|scripts/check-design-tokens\.sh)$ + stages: [pre-commit, manual] + - id: template-sync-manifest name: template-sync-manifest (every tracked path classified) entry: python3 .github/scripts/template_sync_manifest.py diff --git a/README.md b/README.md index ebf513c..31747e9 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ decisions: - [`0003-re-parent-onto-template-docker.md`](docs/adrs/0003-re-parent-onto-template-docker.md) — the image layer from `template-docker`, the `Dockerfile` layout, `image.mk`, and the UI dependency set. +- [`0004-design-system-and-token-lint.md`](docs/adrs/0004-design-system-and-token-lint.md) — + the "Field Ledger" design system in `src/theme/` and the hook that keeps raw + colors and `px` out of the rest of `src/`; principles in + [`docs/design/`](docs/design/README.md). ## Getting started diff --git a/docs/adrs/0004-design-system-and-token-lint.md b/docs/adrs/0004-design-system-and-token-lint.md new file mode 100644 index 0000000..f5828b5 --- /dev/null +++ b/docs/adrs/0004-design-system-and-token-lint.md @@ -0,0 +1,60 @@ +# 0004. Design system ("Field Ledger") and the design-token lint hook + +## Status + +Accepted + +## Context + +Track UI of `docs/plans/2026-10-templates-sdks-ui.md` wants a UI that does +not look like default model output (Roboto/Inter, purple gradients, no +motion) and stays consistent as Claude and people add screens. The user +picked aesthetic direction A, "Field Ledger", from three RS3 prototypes +(2026-10-20): Fraunces + Instrument Sans, warm paper / vermilion / bottle +green, comfortable density, 2px radius, one quiet row reveal. The prototype +was a scratch theme; this ADR records how it became the production system. + +## Decision + +**Theme.** `src/theme/` holds one MUI theme (`createTheme` with +`cssVariables` and `colorSchemes` light and dark) built from `tokens.ts`, +the only place raw colors and px values live. `colorSchemeSelector` is +`"data"` (not the prototype's `"media"`) so the app can offer a light/dark +toggle: with `"media"` MUI cannot set the mode; with `"data"` and +`defaultMode="system"` it still follows the OS until the user chooses. +`useColorMode()` wraps MUI's `useColorScheme`. Fonts are imported from +`@fontsource-variable/*` (self-hosted; the Roboto import is gone from +`main.tsx`). + +**Motion.** Duration and easing tokens are shared by Motion and CSS. The row +reveal is a pair of Motion variants. `` also mounts +`MotionConfig reducedMotion="user"`, which drops transforms (keeping +opacity) under `prefers-reduced-motion`. + +**Lint.** `scripts/check-design-tokens.sh` rejects hex/`rgb()`/`hsl()` +literals and raw `px` values in `src/**/*.{ts,tsx,css}` outside +`src/theme/`, as the pre-commit hook `design-tokens`. An exception is an +inline `design-tokens: allow ` marker; a marker without a reason +fails. The hook runs the script's `--self-test` (fixtures for every +violation kind, the marker and the `src/theme/` exemption) before scanning, +so the checker cannot silently stop catching things. + +**Contrast** is asserted in `src/theme/contrast.test.ts` (WCAG 2.2 AA, 4.5:1 +for text); the ratios are in `docs/design/README.md`. + +## Consequences + +- Palette or radius changes happen in one file; components never hardcode. +- The check is a line-based grep, not a parser: it can flag a color-like + string in a comment (use the marker) and miss a color built at runtime + from string pieces. Accepted: it is a guardrail for Claude-written code, + not a security boundary. +- `rem`, `%` and theme spacing replace `px` in app code; `1px` hairlines + come from theme overrides. +- No pre-paint init script: a user whose saved choice differs from the OS + can see one frame of the OS scheme. Adding MUI's `InitColorSchemeScript` + to `index.html` fixes it if that matters; deferred to keep `index.html` + untouched here. +- The `ThemeProvider` wrapper is a third place (with `main.tsx` and + Storybook, UI3) that must mount the theme; stories should use the same + wrapper. diff --git a/docs/design/README.md b/docs/design/README.md new file mode 100644 index 0000000..e4645f5 --- /dev/null +++ b/docs/design/README.md @@ -0,0 +1,107 @@ +# docs/design/ + +The design system's principles: what the app should look and feel like, +and the rules that keep it that way. The tokens themselves live in code +(`src/theme/`); this page says how to use them. Why the system is shaped +this way is in +[`../adrs/0004-design-system-and-token-lint.md`](../adrs/0004-design-system-and-token-lint.md). + +## Direction: "Field Ledger" + +Chosen by the user on 2026-10-20 from three candidates (RS3). An editorial, +archival feel: a ledger kept in a field office. + +- **Type:** Fraunces (display, headings) and Instrument Sans (body, UI), + both self-hosted variable fonts from `@fontsource-variable/*`. No CDN. +- **Color:** warm paper surfaces, vermilion for the one primary action, + bottle green as the quiet secondary. Ink-dark text, never pure black. +- **Density:** comfortable. Table cells pad 14px/16px; buttons 8px/20px. +- **Shape:** 2px radius everywhere. Rules (hairlines) do the work that + shadows and rounding do elsewhere: a 2px ink rule under the app bar and + table header, 1px dividers between rows. +- **Motion:** one quiet row reveal (fade plus a 4px settle, staggered 30ms). + +## Anti-defaults + +Generic output is the failure mode this system exists to avoid. Do not: + +- fall back to Roboto, Inter or system-ui as the face of the UI; +- use purple/blue gradients, glass blur, or drop-shadow cards; +- round things beyond the 2px base, or use pill buttons; +- use ALL-CAPS anywhere except table column heads; +- add decoration motion (parallax, bouncing, looping). One reveal only; +- reach for a second accent color. Status colors (error, warning, info, + success) are for status, not decoration. + +## Token usage + +- Colors come from the MUI palette via CSS variables: `sx={{ color: + "text.secondary" }}`, or `theme.vars.palette.*` in style callbacks. + Never a hex, `rgb()` or `hsl()` literal outside `src/theme/`. +- Spacing is `theme.spacing(n)` (8px unit); `sx={{ p: 2 }}` already uses + it. Never a raw `px` outside `src/theme/`. Use `rem`, `%`, theme + spacing or a token. +- Radius comes from `theme.shape.borderRadius` (2px) or `tokens.radius`. +- Fonts: headings use the theme's typography variants (`h1`–`h6`, which + carry Fraunces); do not set `fontFamily` by hand. +- Motion durations and easings are `tokens.duration`, `tokens.easing`, + `tokens.cssMotion`. The row reveal is `rowRevealContainer` + `rowReveal` + from `src/theme/motion.ts`. +- `scripts/check-design-tokens.sh` (pre-commit hook `design-tokens`) + rejects violations in `src/**/*.{ts,tsx,css}`. A justified exception + carries an inline marker on the same line, with a reason: + `// design-tokens: allow `. A marker with no reason fails. + +Public API (`import { ... } from "./theme"`): `ThemeProvider`, `theme`, +`tokens`, `useColorMode`, `rowReveal`, `rowRevealContainer`. + +## Light and dark + +- Both schemes are first-class and ship together; a component is not done + until it is checked in both. +- The default is the OS preference. `useColorMode()` returns `mode` + (`"light" | "dark" | "system"`), `resolved`, `setMode` and `toggle`; + MUI persists the choice in `localStorage`. +- Colors switch through CSS variables under a data attribute on the root element, so + `sx` and `theme.vars.*` need no per-scheme branches. Dark is not an + inversion: ink and paper swap, and accents lighten (vermilion + `#E8795C`, green `#9CC0A8`) to keep contrast. +- Known limit: there is no pre-paint init script, so a user who chose a + scheme different from the OS can see one frame of the OS scheme on + load. + +## Contrast + +Target: WCAG 2.2 AA, 4.5:1 for text, 3:1 for large text and UI +boundaries. `src/theme/contrast.test.ts` asserts every pair below at +4.5:1 and fails the build if a palette edit breaks one. + +| Pair | Light | Dark | +| --------------------------------- | -----: | ----: | +| text.primary on paper | 15.74 | 13.56 | +| text.secondary on paper | 7.11 | 7.29 | +| text.secondary on default | 6.65 | 7.92 | +| primary on paper | 5.68 | 5.80 | +| primary contrastText on primary | 5.75 | 6.30 | +| secondary on paper | 9.09 | 8.33 | +| error on paper | 7.45 | 6.85 | +| warning on paper | 5.92 | 7.88 | +| info on paper | 6.35 | 7.82 | +| success on paper | 5.97 | 8.52 | + +Divider on paper is 1.47 (light) and 1.35 (dark). It is decoration, not +the only cue for anything; never use it as the sole boundary of an +interactive control (use the 2px ink rule or a text-colored border, +which clear 3:1). + +## Motion and accessibility + +- `` wraps the app in `MotionConfig reducedMotion="user"`: + under `prefers-reduced-motion`, transforms are dropped and only + opacity animates. CSS transitions in the theme are short (160ms) color + changes only. +- Motion never carries meaning on its own and never blocks input. +- Keep MUI's focus ring; do not remove outlines. +- Text scales: sizes are `rem`, so the browser's font-size setting + applies. Check layouts at 200% zoom. +- Run axe over every story and page (UI3 automates this). diff --git a/scripts/check-design-tokens.sh b/scripts/check-design-tokens.sh new file mode 100755 index 0000000..b4a3c29 --- /dev/null +++ b/scripts/check-design-tokens.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Design-token guard (ADR 0004): rejects hex/rgb/hsl color literals and raw +# `px` values in src/**/*.{ts,tsx,css} outside src/theme/, so every color and +# size comes from the theme (src/theme/tokens.ts). +# +# usage: check-design-tokens.sh [SRC_DIR] (default: src) +# check-design-tokens.sh --self-test (runs against built-in fixtures) +# +# A line may opt out with an inline marker and a non-empty reason: +# width: "1px", // design-tokens: allow hairline needed by third-party widget +# A marker without a reason is itself a violation. +set -euo pipefail + +COLOR_RE='#[0-9a-fA-F]{3,8}\b|\b(rgb|rgba|hsl|hsla)\(' +PX_RE='(^|[^A-Za-z0-9_.-])[0-9]*\.?[0-9]+px\b' +MARKER='design-tokens: allow' + +scan() { + local root="$1" status=0 file + while IFS= read -r -d '' file; do + local n=0 line + while IFS= read -r line || [ -n "$line" ]; do + n=$((n + 1)) + local kind="" + if [[ "$line" == *"$MARKER"* ]]; then + local reason="${line#*"$MARKER"}" + reason="${reason//[[:space:]*\/]/}" + if [ -z "$reason" ]; then + echo "$file:$n: '$MARKER' needs a reason after it" >&2 + status=1 + fi + continue + fi + if grep -Eq -- "$COLOR_RE" <<<"$line"; then kind="color literal"; fi + if [ -z "$kind" ] && grep -Eq -- "$PX_RE" <<<"$line"; then kind="raw px value"; fi + if [ -n "$kind" ]; then + echo "$file:$n: $kind outside src/theme/ (use a theme token): ${line#"${line%%[![:space:]]*}"}" >&2 + status=1 + fi + done <"$file" + done < <(find "$root" -path "$root/theme" -prune -o -type f \ + \( -name '*.ts' -o -name '*.tsx' -o -name '*.css' \) -print0 | sort -z) + return "$status" +} + +self_test() { + local d fail=0 + d="$(mktemp -d)" + trap 'rm -rf "$d"' RETURN + mkdir -p "$d/theme" "$d/ok" "$d/bad" + echo 'export const c = "#B3361B"; export const p = "4px";' >"$d/theme/tokens.ts" + printf 'const w = "100%%"; const id = "#root"; const v = "var(--x)";\n' >"$d/ok/clean.tsx" + printf 'const w = "1px"; // design-tokens: allow hairline for a third-party widget\n' >"$d/ok/marked.tsx" + echo 'a { color: #fff; }' >"$d/bad/hex.css" + echo 'const c = "#abcdef";' >"$d/bad/hex6.ts" + echo 'const c = "rgba(0, 0, 0, 0.5)";' >"$d/bad/rgb.ts" + echo 'const c = "hsl(10 20% 30%)";' >"$d/bad/hsl.ts" + echo 'const s = { width: "12px" };' >"$d/bad/px.tsx" + echo 'a { margin: 0.5px; }' >"$d/bad/px.css" + echo 'const w = "1px"; // design-tokens: allow' >"$d/bad/noreason.ts" + + scan "$d/ok" >/dev/null 2>&1 || { echo "self-test: clean fixtures were rejected" >&2; fail=1; } + local f + for f in hex.css hex6.ts rgb.ts hsl.ts px.tsx px.css noreason.ts; do + rm -rf "$d/one" && mkdir -p "$d/one" && cp "$d/bad/$f" "$d/one/" + if scan "$d/one" >/dev/null 2>&1; then + echo "self-test: $f was not rejected" >&2 + fail=1 + fi + done + # theme/ itself must be exempt. + mkdir -p "$d/only" && cp -r "$d/theme" "$d/only/theme" + scan "$d/only" >/dev/null 2>&1 || { echo "self-test: src/theme/ was not exempt" >&2; fail=1; } + [ "$fail" -eq 0 ] && echo "check-design-tokens self-test: ok" + return "$fail" +} + +if [ "${1:-}" = "--self-test" ]; then + self_test +else + scan "${1:-src}" +fi diff --git a/src/App.tsx b/src/App.tsx index d8a523a..a56f40d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,7 @@ function App() { return ( <> - + template-react diff --git a/src/main.tsx b/src/main.tsx index 78bf5af..4b96e29 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,21 +1,14 @@ -import "@fontsource/roboto/300.css"; -import "@fontsource/roboto/400.css"; -import "@fontsource/roboto/500.css"; -import "@fontsource/roboto/700.css"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import CssBaseline from "@mui/material/CssBaseline"; -import { ThemeProvider } from "@mui/material/styles"; import { AuthProvider } from "react-oidc-context"; import App from "./App.tsx"; import { oidcConfig } from "./auth/config"; -import { theme } from "./theme/theme"; +import { ThemeProvider } from "./theme"; createRoot(document.getElementById("root")!).render( - - + diff --git a/src/theme/ThemeProvider.tsx b/src/theme/ThemeProvider.tsx new file mode 100644 index 0000000..e5c5322 --- /dev/null +++ b/src/theme/ThemeProvider.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from "react"; +import CssBaseline from "@mui/material/CssBaseline"; +import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; +import { MotionConfig } from "motion/react"; +import "./fonts"; +import { theme } from "./theme"; + +/** Theme, CssBaseline and the reduced-motion policy, in one wrapper. */ +export function ThemeProvider({ children }: { children: ReactNode }) { + return ( + + + {children} + + ); +} diff --git a/src/theme/contrast.test.ts b/src/theme/contrast.test.ts new file mode 100644 index 0000000..92661ec --- /dev/null +++ b/src/theme/contrast.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { palette, type SchemePalette } from "./tokens"; + +function luminance(hex: string): number { + const [r, g, b] = [1, 3, 5].map((i) => { + const c = parseInt(hex.slice(i, i + 2), 16) / 255; + return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!; +} + +export function ratio(a: string, b: string): number { + const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); + return (hi! + 0.05) / (lo! + 0.05); +} + +// WCAG 2.2 AA: 4.5:1 for text. +describe.each(Object.entries(palette))( + "%s scheme text contrast", + (_name, p: SchemePalette) => { + const pairs: [string, string, string][] = [ + ["text.primary on default", p.text.primary, p.background.default], + ["text.primary on paper", p.text.primary, p.background.paper], + ["text.secondary on default", p.text.secondary, p.background.default], + ["text.secondary on paper", p.text.secondary, p.background.paper], + ["primary on paper", p.primary.main, p.background.paper], + ["primary on default", p.primary.main, p.background.default], + [ + "primary contrastText on primary", + p.primary.contrastText, + p.primary.main, + ], + ["secondary on paper", p.secondary.main, p.background.paper], + [ + "secondary contrastText on secondary", + p.secondary.contrastText, + p.secondary.main, + ], + ["error on paper", p.error.main, p.background.paper], + ["warning on paper", p.warning.main, p.background.paper], + ["info on paper", p.info.main, p.background.paper], + ["success on paper", p.success.main, p.background.paper], + ["success on default", p.success.main, p.background.default], + ]; + it.each(pairs)("%s >= 4.5:1", (_label, fg, bg) => { + expect(ratio(fg, bg)).toBeGreaterThanOrEqual(4.5); + }); + }, +); diff --git a/src/theme/fonts.ts b/src/theme/fonts.ts new file mode 100644 index 0000000..bc787ec --- /dev/null +++ b/src/theme/fonts.ts @@ -0,0 +1,4 @@ +// Self-hosted variable fonts (no CDN): Fraunces for display, Instrument Sans +// for body/UI. Imported for side effects (the @font-face rules). +import "@fontsource-variable/fraunces"; +import "@fontsource-variable/instrument-sans"; diff --git a/src/theme/index.ts b/src/theme/index.ts new file mode 100644 index 0000000..76fb914 --- /dev/null +++ b/src/theme/index.ts @@ -0,0 +1,5 @@ +export { ThemeProvider } from "./ThemeProvider"; +export { theme } from "./theme"; +export { tokens } from "./tokens"; +export { useColorMode, type ColorMode } from "./useColorMode"; +export { rowReveal, rowRevealContainer } from "./motion"; diff --git a/src/theme/motion.ts b/src/theme/motion.ts new file mode 100644 index 0000000..8025e68 --- /dev/null +++ b/src/theme/motion.ts @@ -0,0 +1,27 @@ +// The one quiet row reveal: a row fades in and settles 4px, staggered by +// the parent. Apply with Motion's `variants`: +// +// +// +// +// wraps the app in MotionConfig reducedMotion="user", so +// under prefers-reduced-motion the y-shift is dropped and only the fade stays. +import type { Variants } from "motion/react"; +import { duration, easing } from "./tokens"; + +const REVEAL_OFFSET = 4; +const STAGGER = 0.03; + +export const rowRevealContainer: Variants = { + hidden: {}, + visible: { transition: { staggerChildren: STAGGER } }, +}; + +export const rowReveal: Variants = { + hidden: { opacity: 0, y: REVEAL_OFFSET }, + visible: { + opacity: 1, + y: 0, + transition: { duration: duration.base, ease: [...easing.enter] }, + }, +}; diff --git a/src/theme/theme.ts b/src/theme/theme.ts index a0f2adb..b44ba2f 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -1,8 +1,109 @@ -// Single MUI theme, light/dark driven by the OS preference -- an instance -// swaps the palette/typography here rather than scattering `sx` overrides. +// The single MUI theme: CSS variables, light/dark schemes that follow the OS +// until the user picks one (see useColorMode), and the Field Ledger +// component overrides for comfortable density and the 2px radius. import { createTheme } from "@mui/material/styles"; +import { cssMotion, fontFamily, palette, radius, spacingUnit } from "./tokens"; export const theme = createTheme({ - colorSchemes: { light: true, dark: true }, - cssVariables: { colorSchemeSelector: "media" }, + cssVariables: { colorSchemeSelector: "data" }, + colorSchemes: { + light: { palette: palette.light }, + dark: { palette: palette.dark }, + }, + shape: { borderRadius: radius.base }, + spacing: spacingUnit, + typography: { + fontFamily: fontFamily.body, + fontSize: 15, + h1: { + fontFamily: fontFamily.display, + fontWeight: 600, + letterSpacing: "-0.02em", + }, + h2: { + fontFamily: fontFamily.display, + fontWeight: 600, + letterSpacing: "-0.02em", + }, + h3: { + fontFamily: fontFamily.display, + fontWeight: 600, + letterSpacing: "-0.02em", + }, + h4: { + fontFamily: fontFamily.display, + fontWeight: 600, + letterSpacing: "-0.02em", + }, + h5: { + fontFamily: fontFamily.display, + fontWeight: 600, + fontSize: "2.25rem", + letterSpacing: "-0.02em", + fontVariationSettings: '"opsz" 72', + }, + h6: { + fontFamily: fontFamily.display, + fontWeight: 600, + letterSpacing: "-0.01em", + }, + button: { textTransform: "none", fontWeight: 600 }, + }, + components: { + MuiAppBar: { + defaultProps: { elevation: 0, color: "transparent" }, + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: "transparent", + color: theme.vars.palette.text.primary, + borderBottom: `2px solid ${theme.vars.palette.text.primary}`, + }), + }, + }, + MuiPaper: { styleOverrides: { root: { backgroundImage: "none" } } }, + MuiTableCell: { + styleOverrides: { + root: ({ theme }) => ({ + padding: "14px 16px", + borderBottom: `1px solid ${theme.vars.palette.divider}`, + }), + head: ({ theme }) => ({ + fontFamily: fontFamily.body, + fontWeight: 600, + fontSize: "0.75rem", + letterSpacing: "0.08em", + textTransform: "uppercase", + color: theme.vars.palette.text.secondary, + borderBottom: `2px solid ${theme.vars.palette.text.primary}`, + }), + }, + }, + MuiChip: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: radius.base, + backgroundColor: "transparent", + border: `1px solid ${theme.vars.palette.divider}`, + fontWeight: 500, + }), + }, + }, + MuiButton: { + defaultProps: { disableElevation: true }, + styleOverrides: { + root: { borderRadius: radius.base, padding: "8px 20px" }, + }, + }, + MuiCssBaseline: { + styleOverrides: { + "tr.MuiTableRow-root td": { + transition: `background-color ${cssMotion.quick}`, + }, + "tr.MuiTableRow-root:hover td": { + backgroundColor: + "color-mix(in srgb, var(--mui-palette-primary-main) 6%, transparent)", + }, + }, + }, + }, }); diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts new file mode 100644 index 0000000..9e404f2 --- /dev/null +++ b/src/theme/tokens.ts @@ -0,0 +1,84 @@ +// Design tokens for the "Field Ledger" direction (docs/design/README.md, +// ADR 0004). This is the only place raw colors and px values live; every +// other file under src/ reads them through the MUI theme (CSS variables) or +// the exports below. scripts/check-design-tokens.sh enforces that. + +export const fontFamily = { + display: '"Fraunces Variable", Georgia, serif', + body: '"Instrument Sans Variable", system-ui, sans-serif', +} as const; + +export interface SchemePalette { + primary: { main: string; contrastText: string }; + secondary: { main: string; contrastText: string }; + error: { main: string; contrastText: string }; + warning: { main: string; contrastText: string }; + info: { main: string; contrastText: string }; + success: { main: string; contrastText: string }; + background: { default: string; paper: string }; + text: { primary: string; secondary: string }; + divider: string; +} + +// Warm paper, vermilion, bottle green. Contrast ratios are asserted in +// contrast.test.ts and recorded in docs/design/README.md. +export const palette: { light: SchemePalette; dark: SchemePalette } = { + light: { + primary: { main: "#B3361B", contrastText: "#FFF8EE" }, + secondary: { main: "#2F4A3A", contrastText: "#FFF8EE" }, + error: { main: "#9E1C1C", contrastText: "#FFF8EE" }, + warning: { main: "#8A5300", contrastText: "#FFF8EE" }, + info: { main: "#2D5F86", contrastText: "#FFF8EE" }, + success: { main: "#2F6B3C", contrastText: "#FFF8EE" }, + background: { default: "#F6EFE2", paper: "#FBF7EE" }, + text: { primary: "#231C14", secondary: "#5E5242" }, + divider: "#D9CDB6", + }, + dark: { + primary: { main: "#E8795C", contrastText: "#1B1510" }, + secondary: { main: "#9CC0A8", contrastText: "#1B1510" }, + error: { main: "#F08A7E", contrastText: "#1B1510" }, + warning: { main: "#E0A94A", contrastText: "#1B1510" }, + info: { main: "#8FB6DA", contrastText: "#1B1510" }, + success: { main: "#8CC79B", contrastText: "#1B1510" }, + background: { default: "#1B1510", paper: "#241D16" }, + text: { primary: "#F1E7D3", secondary: "#B8AA92" }, + divider: "#3D3328", + }, +}; + +// MUI multiplies theme.spacing(n) by this: an 8px grid. +export const spacingUnit = 8; + +// Radius scale in px, consumed by MUI's `shape.borderRadius` (2px base). +export const radius = { none: 0, base: 2, pill: 999 } as const; + +export const duration = { + instant: 0.08, + quick: 0.16, + base: 0.24, + slow: 0.4, +} as const; + +// cubic-bezier control points, usable by both Motion and CSS. +export const easing = { + standard: [0.2, 0, 0, 1], + enter: [0, 0, 0, 1], + exit: [0.3, 0, 1, 1], +} as const; + +const css = (e: readonly number[]) => `cubic-bezier(${e.join(", ")})`; +export const cssMotion = { + quick: `${duration.quick * 1000}ms ${css(easing.standard)}`, + base: `${duration.base * 1000}ms ${css(easing.standard)}`, +} as const; + +export const tokens = { + fontFamily, + palette, + spacingUnit, + radius, + duration, + easing, + cssMotion, +}; diff --git a/src/theme/useColorMode.ts b/src/theme/useColorMode.ts new file mode 100644 index 0000000..237a9d8 --- /dev/null +++ b/src/theme/useColorMode.ts @@ -0,0 +1,20 @@ +import { useColorScheme } from "@mui/material/styles"; + +export type ColorMode = "light" | "dark" | "system"; + +/** + * The color-scheme API for the app: `mode` is the user's choice (default + * "system" = follow the OS), `resolved` the scheme actually shown. `toggle` + * flips between light and dark from whatever is currently shown. + */ +export function useColorMode() { + const { mode, systemMode, setMode } = useColorScheme(); + const resolved: "light" | "dark" | undefined = + mode === "system" ? systemMode : mode; + return { + mode: mode as ColorMode | undefined, + resolved, + setMode: (next: ColorMode) => setMode(next), + toggle: () => setMode(resolved === "dark" ? "light" : "dark"), + }; +} From 511109cf79bae6ff04f6af09c3d77e86f32d2b8d Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:08:04 +0000 Subject: [PATCH 04/21] chore(deps): remove unused @fontsource/roboto --- package.json | 1 - pnpm-lock.yaml | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/package.json b/package.json index 20e8191..df39462 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "@emotion/styled": "11.14.1", "@fontsource-variable/fraunces": "5.3.0", "@fontsource-variable/instrument-sans": "5.3.0", - "@fontsource/roboto": "5.3.0", "@mui/icons-material": "9.4.0", "@mui/material": "9.4.0", "@tanstack/react-query": "5.104.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a45a336..dff9eb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,9 +144,6 @@ importers: "@fontsource-variable/instrument-sans": specifier: 5.3.0 version: 5.3.0 - "@fontsource/roboto": - specifier: 5.3.0 - version: 5.3.0 "@mui/icons-material": specifier: 9.4.0 version: 9.4.0(@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) @@ -827,12 +824,6 @@ packages: integrity: sha512-u4gKbDBTNFGkg997tfQn3eHOhHuquWUFTRT/rwzuKtrxX5P2ekfs2x+LgBPP4P32+cC+vUwF1Cr+IdRoPQbrGw==, } - "@fontsource/roboto@5.3.0": - resolution: - { - integrity: sha512-BapRJOWYP+LZ21zp+wBQjfpPYKRoxc4LspJ/RLuI+HSMBD5u/X4O+ESDrSvEqDSy0rAl7GwBJ+09mdc16cVQ1Q==, - } - "@joshwooding/vite-plugin-react-docgen-typescript@0.7.0": resolution: { @@ -3900,8 +3891,6 @@ snapshots: "@fontsource-variable/instrument-sans@5.3.0": {} - "@fontsource/roboto@5.3.0": {} - "@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2))": dependencies: glob: 13.0.6 From 9c7b14772162d033ec1f508c9b57db613b2abec4 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:04:53 +0000 Subject: [PATCH 05/21] fix(ui): review round 1 (token lint negative px, hex lengths, opsz font axis, contrast pairs) --- scripts/check-design-tokens.sh | 10 +++++++--- src/theme/contrast.test.ts | 15 +++++++++++++++ src/theme/fonts.ts | 3 ++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/scripts/check-design-tokens.sh b/scripts/check-design-tokens.sh index b4a3c29..342987c 100755 --- a/scripts/check-design-tokens.sh +++ b/scripts/check-design-tokens.sh @@ -6,13 +6,16 @@ # usage: check-design-tokens.sh [SRC_DIR] (default: src) # check-design-tokens.sh --self-test (runs against built-in fixtures) # +# Any `#` plus 3, 4, 6 or 8 hex characters counts as a color, so an id like +# `#fade` or `#123` also matches; opt those out with the marker below. +# # A line may opt out with an inline marker and a non-empty reason: # width: "1px", // design-tokens: allow hairline needed by third-party widget # A marker without a reason is itself a violation. set -euo pipefail -COLOR_RE='#[0-9a-fA-F]{3,8}\b|\b(rgb|rgba|hsl|hsla)\(' -PX_RE='(^|[^A-Za-z0-9_.-])[0-9]*\.?[0-9]+px\b' +COLOR_RE='#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|\b(rgb|rgba|hsl|hsla)\(' +PX_RE='(^|[^A-Za-z0-9_.])-?[0-9]*\.?[0-9]+px\b' MARKER='design-tokens: allow' scan() { @@ -56,12 +59,13 @@ self_test() { echo 'const c = "rgba(0, 0, 0, 0.5)";' >"$d/bad/rgb.ts" echo 'const c = "hsl(10 20% 30%)";' >"$d/bad/hsl.ts" echo 'const s = { width: "12px" };' >"$d/bad/px.tsx" + echo 'const s = { mt: "-4px" };' >"$d/bad/negpx.tsx" echo 'a { margin: 0.5px; }' >"$d/bad/px.css" echo 'const w = "1px"; // design-tokens: allow' >"$d/bad/noreason.ts" scan "$d/ok" >/dev/null 2>&1 || { echo "self-test: clean fixtures were rejected" >&2; fail=1; } local f - for f in hex.css hex6.ts rgb.ts hsl.ts px.tsx px.css noreason.ts; do + for f in hex.css hex6.ts rgb.ts hsl.ts px.tsx negpx.tsx px.css noreason.ts; do rm -rf "$d/one" && mkdir -p "$d/one" && cp "$d/bad/$f" "$d/one/" if scan "$d/one" >/dev/null 2>&1; then echo "self-test: $f was not rejected" >&2 diff --git a/src/theme/contrast.test.ts b/src/theme/contrast.test.ts index 92661ec..abb89bb 100644 --- a/src/theme/contrast.test.ts +++ b/src/theme/contrast.test.ts @@ -41,6 +41,21 @@ describe.each(Object.entries(palette))( ["info on paper", p.info.main, p.background.paper], ["success on paper", p.success.main, p.background.paper], ["success on default", p.success.main, p.background.default], + ["error on default", p.error.main, p.background.default], + ["warning on default", p.warning.main, p.background.default], + ["info on default", p.info.main, p.background.default], + ["error contrastText on error", p.error.contrastText, p.error.main], + [ + "warning contrastText on warning", + p.warning.contrastText, + p.warning.main, + ], + ["info contrastText on info", p.info.contrastText, p.info.main], + [ + "success contrastText on success", + p.success.contrastText, + p.success.main, + ], ]; it.each(pairs)("%s >= 4.5:1", (_label, fg, bg) => { expect(ratio(fg, bg)).toBeGreaterThanOrEqual(4.5); diff --git a/src/theme/fonts.ts b/src/theme/fonts.ts index bc787ec..f208b58 100644 --- a/src/theme/fonts.ts +++ b/src/theme/fonts.ts @@ -1,4 +1,5 @@ // Self-hosted variable fonts (no CDN): Fraunces for display, Instrument Sans // for body/UI. Imported for side effects (the @font-face rules). -import "@fontsource-variable/fraunces"; +// opsz.css carries the optical-size axis that the theme's h1-h5 use. +import "@fontsource-variable/fraunces/opsz.css"; import "@fontsource-variable/instrument-sans"; From 8f18bccc886aa78facc7f221ccdaee7a66447713 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:27:48 +0000 Subject: [PATCH 06/21] feat(claude): frontend-design and ui-verify skills, @mui/mcp --- .claude/README.md | 16 +++++ .claude/skills/frontend-design/SKILL.md | 75 +++++++++++++++++++++ .claude/skills/ui-verify/SKILL.md | 78 +++++++++++++++++++++ .claude/skills/ui-verify/shoot.mjs | 90 +++++++++++++++++++++++++ .mcp.json | 4 ++ 5 files changed, 263 insertions(+) create mode 100644 .claude/skills/frontend-design/SKILL.md create mode 100644 .claude/skills/ui-verify/SKILL.md create mode 100644 .claude/skills/ui-verify/shoot.mjs diff --git a/.claude/README.md b/.claude/README.md index 5d5de4d..b031c31 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -39,6 +39,16 @@ this directory). the rolling queue of `plan-executor` subagents, researched answers to escalations, serialized lanes, gates and close-out. - `owasp-scan` — a parallel OWASP Top Ten scan of `src/`. + - `frontend-design` — how to design UI here: the "Field Ledger" + direction and its anti-defaults, proposing 2-3 distinct options for + new screens so output doesn't converge on one stock layout, and the + token, motion and accessibility rules. Points at + `../docs/design/README.md` as the source of truth. + - `ui-verify` — the visual verification loop: start the dev server, + screenshot 3 viewports (390, 834, 1440) in light and dark, run axe, + compare against the intent, iterate. Ships `shoot.mjs`, which drives + the stack's existing Selenium over W3C WebDriver, because the + Playwright MCP's CDP attach to it fails with a 400. `../CLAUDE.md` points at the two plan skills, so they're used even when a request doesn't match their descriptions word for word. @@ -65,6 +75,11 @@ this directory). reason `scripts/develop.sh` installs it (pinned via `../.devcontainer/Dockerfile`'s `NODE_VERSION` ARG) — Claude Code itself doesn't need it. + - `mui-mcp` (`@mui/mcp`) — MUI's official local stdio MCP server: + current Material UI component docs and APIs, launched via `npx`. + Pinned to an exact version; `frontend-design` tells Claude to use it + rather than guess props. + An instance adds its own stack-specific MCP servers here (database, object storage, browser automation, ...), pinned to an exact version/commit the same way, and reading credentials via `${VAR}` from @@ -88,6 +103,7 @@ this directory). `scripts/develop.sh`. - **context7** / **security-guidance**: delete the plugin's `enabledPlugins` entry in `settings.json`. +- **mui-mcp**: delete its entry from `../.mcp.json`. - **clear-thought**: delete its entry from `../.mcp.json`; if nothing else needs Node.js, also remove its install block (and `NODE_VERSION` ARG) from `../.devcontainer/Dockerfile`/`scripts/develop.sh`. diff --git a/.claude/skills/frontend-design/SKILL.md b/.claude/skills/frontend-design/SKILL.md new file mode 100644 index 0000000..392144f --- /dev/null +++ b/.claude/skills/frontend-design/SKILL.md @@ -0,0 +1,75 @@ +--- +name: frontend-design +description: How to design UI in this repo. Use when building or restyling a screen, component or layout, or when asked for new UI options. Covers the "Field Ledger" direction, its anti-defaults, theme-token rules, motion and accessibility targets. +--- + +# Frontend design + +`docs/design/README.md` is the source of truth for the design system +(direction, tokens, contrast table, motion rules); `docs/adrs/0004-design-system-and-token-lint.md` +says why. Read the design README before designing anything, and do not +copy its rules into code comments or other docs: link to it. + +## Direction: Field Ledger + +An editorial, archival feel: a ledger kept in a field office. Warm paper +surfaces, ink-dark text, vermilion for the one primary action, bottle +green as the quiet secondary. Fraunces for headings, Instrument Sans for +body. 2px radius; hairline rules do the work that shadows and rounding do +elsewhere. Comfortable density. Aim for a screen that looks *kept* and +*considered*, not assembled from stock components. + +## Anti-defaults + +Generic MUI output is the failure mode. Do not ship: + +- the default MUI blue or any purple/blue gradient; +- default elevation shadows (`elevation`, `Paper` raised, drop-shadow + cards); use a rule or a 1px divider; +- Roboto, Inter or system-ui as the face of the UI; +- gratuitous gradients, glass blur or decoration; +- cards-everywhere: not every group of content needs a box. Prefer + typographic hierarchy, rules and whitespace; a table or ledger list + often beats a grid of cards; +- pill buttons, large radii, ALL-CAPS outside table column heads, or a + second accent color (status colors are for status only). + +## Vary across generations + +Left alone, generated UI converges on one stock layout (centered column, +stacked cards, hero plus three tiles). Resist it: + +- When asked for a new screen, propose 2-3 distinct options first + (different structure, not just different colors: e.g. ledger table vs. + two-pane index/detail vs. an annotated single column), with one line on + the trade-off each. Let the user pick before building the full thing. +- Pick a deliberate point of view per screen (what is the one thing the + eye should land on?) and let scale, weight and whitespace carry it. +- Do not reuse the layout of the last screen you built just because it + worked. + +## Always + +- Use theme tokens: palette via `sx` names or `theme.vars.palette.*`, + spacing via `theme.spacing`/`sx` numbers, radius via + `theme.shape.borderRadius`, motion via `tokens.*`. Never a hex/rgb + literal or raw `px` outside `src/theme/`. `scripts/check-design-tokens.sh` + (hook `design-tokens`) enforces it; a justified exception carries + `// design-tokens: allow `. Do not add the marker to silence it + by default. +- Typography via the theme's variants, never a hand-set `fontFamily`. +- Motion: one quiet row reveal (`rowReveal`/`rowRevealContainer` from + `src/theme/motion.ts`). No looping or decorative motion. The theme wraps + the app in `MotionConfig reducedMotion="user"`; keep it that way and + never let motion carry meaning. +- Accessibility: WCAG 2.2 AA (4.5:1 text, 3:1 large text and UI + boundaries); keep the focus ring; real landmarks (`main`, one `h1`); + rem sizes so text scales; check 200% zoom. Divider color is never the + sole boundary of an interactive control. +- Both color schemes are first-class: a component is not done until it + is checked in light and dark. +- Use the `@mui/mcp` server (see `.claude/README.md`) for MUI component + APIs and docs instead of guessing props. + +When the UI is built, run the `ui-verify` skill and iterate against what +you actually see. diff --git a/.claude/skills/ui-verify/SKILL.md b/.claude/skills/ui-verify/SKILL.md new file mode 100644 index 0000000..188fe8c --- /dev/null +++ b/.claude/skills/ui-verify/SKILL.md @@ -0,0 +1,78 @@ +--- +name: ui-verify +description: Verify a UI change visually. Starts the dev server, screenshots 3 viewports in light and dark, runs axe, compares with the stated design intent and iterates. Use after any UI change and before calling it done. +--- + +# UI verify + +A UI change is not done until you have looked at it. Loop: build -> +screenshot -> axe -> compare with intent -> fix -> repeat. + +## 1. Start the dev server + +```bash +pnpm dev --host --port 5173 --strictPort +``` + +`--host` so the browser container can reach it; a fixed port with +`--strictPort` so it fails instead of drifting. Use an IP address in the +URL the browser opens (Vite blocks unknown hostnames): the dev +container's own, from `hostname -i`. Run it in the background and wait +for `curl -sf http://127.0.0.1:5173/`. + +## 2. Screenshots: 3 viewports x light/dark, plus axe + +Viewports: 390 (phone), 834 (tablet), 1440 (desktop). Each in light and +dark: 6 screenshots per page. + +**Use the committed helper, not the Playwright MCP.** The Playwright MCP +attaches over CDP (`ws://selenium:4444/.../se/cdp`), which the stack's +Selenium Grid answers with HTTP 400, so it cannot drive that browser. Do +not start a second browser service. `shoot.mjs` instead talks W3C +WebDriver to the stack's existing Selenium, sets the viewport with a +CDP `Emulation.setDeviceMetricsOverride` command (Grid's `goog/cdp/execute` +endpoint works), switches the scheme through the `mui-mode` localStorage +key MUI persists, injects `axe-core` (a transitive dependency of +`@axe-core/playwright`) and writes the PNGs: + +```bash +node .claude/skills/ui-verify/shoot.mjs http://:5173/ +# writes /-{390,834,1440}-{light,dark}.png and -axe.json +# exit 1 on any axe violation or if the wrong scheme was applied +``` + +`SELENIUM_URL` overrides the default `http://selenium:4444`. The Grid +has one session slot: if a run is killed, the stale session blocks the +next one for up to 5 minutes; delete it with `DELETE +http://selenium:4444/session/` (id from `/status`). + +Pages behind sign-in need the same flow driven first; extend the helper +or script the login before the loop. Signed-out pages are fine for a +first pass. + +Then look at every PNG (Read the image files). The Playwright MCP is fine +for anything that does not need the stack's Selenium (for example if it +is configured with its own local browser). + +## 3. Compare and iterate + +For each screenshot, check against the stated intent and +`docs/design/README.md`: + +- Field Ledger: paper surfaces, ink text, one vermilion action, 2px + radius, rules not shadows, Fraunces headings / Instrument Sans body. +- No anti-defaults crept in (default blue, shadows, Roboto, cards + everywhere, gradients). +- Layout: no wrapped or clipped controls or headings at 390, no + horizontal scroll, sensible use of space at 1440. +- Dark is not an inversion: contrast holds, accents are the lightened ones. +- Axe: zero violations. Fix real ones in the app; do not disable rules. + +Fix, re-run, and repeat until both the screenshots and axe are clean. + +## 4. Report + +In your final message list the 6 screenshot paths per page, the axe +summary per screenshot (violation ids and counts), what you compared +against, and what you changed or still see wrong. Never claim a pass +without having read the screenshots. diff --git a/.claude/skills/ui-verify/shoot.mjs b/.claude/skills/ui-verify/shoot.mjs new file mode 100644 index 0000000..c3c47e9 --- /dev/null +++ b/.claude/skills/ui-verify/shoot.mjs @@ -0,0 +1,90 @@ +// Screenshots one page at 3 viewports x light/dark through the stack's +// Selenium over W3C WebDriver, and runs axe-core on each. See SKILL.md. +// +// usage: node shoot.mjs [name] +// env: SELENIUM_URL (default http://selenium:4444) +import { createRequire } from "node:module"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; + +const [url, outDir, name = "page"] = process.argv.slice(2); +if (!url || !outDir) { + console.error("usage: node shoot.mjs [name]"); + process.exit(2); +} +const hub = process.env.SELENIUM_URL ?? "http://selenium:4444"; +const viewports = [ + { label: "390", width: 390, height: 844 }, + { label: "834", width: 834, height: 1112 }, + { label: "1440", width: 1440, height: 900 }, +]; +const modes = ["light", "dark"]; + +// axe-core is a transitive dependency of @axe-core/playwright. +const require = createRequire(import.meta.url); +const axePath = createRequire(require.resolve("@axe-core/playwright")).resolve( + "axe-core/axe.min.js", +); +const axeSource = readFileSync(axePath, "utf8"); + +async function call(method, path, body) { + const res = await fetch(hub + path, { + method, + headers: { "Content-Type": "application/json" }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + const json = await res.json(); + if (!res.ok) throw new Error(`${method} ${path}: ${JSON.stringify(json)}`); + return json.value; +} + +const created = await call("POST", "/session", { + capabilities: { + alwaysMatch: { + browserName: "chrome", + "goog:chromeOptions": { args: ["--hide-scrollbars"] }, + }, + }, +}); +const sid = created.sessionId; +const s = (p) => `/session/${sid}${p}`; +const run = (script, args = []) => call("POST", s("/execute/sync"), { script, args }); +mkdirSync(outDir, { recursive: true }); + +const summary = []; +try { + for (const vp of viewports) { + // Emulate the viewport exactly; a window resize cannot go below the + // browser chrome's minimum width. + await call("POST", s("/goog/cdp/execute"), { + cmd: "Emulation.setDeviceMetricsOverride", + params: { width: vp.width, height: vp.height, deviceScaleFactor: 1, mobile: vp.width < 600 }, + }); + for (const mode of modes) { + await call("POST", s("/url"), { url }); + await run("localStorage.setItem('mui-mode', arguments[0])", [mode]); + await call("POST", s("/url"), { url }); + await new Promise((r) => setTimeout(r, 1500)); + // MUI sets data-light / data-dark on for the resolved scheme. + const scheme = await run( + "return document.documentElement.hasAttribute('data-dark') ? 'dark' : document.documentElement.hasAttribute('data-light') ? 'light' : null", + ); + const png = await call("GET", s("/screenshot")); + const file = `${outDir}/${name}-${vp.label}-${mode}.png`; + writeFileSync(file, Buffer.from(png, "base64")); + const axe = await call("POST", s("/execute/async"), { + script: + axeSource + + ";var done=arguments[arguments.length-1];axe.run(document).then(function(r){done(r.violations.map(function(v){return {id:v.id,impact:v.impact,help:v.help,nodes:v.nodes.length,targets:v.nodes.slice(0,3).map(function(n){return n.target.join(' ')})}}))})", + args: [], + }); + summary.push({ file, scheme, violations: axe }); + console.log(`${file} scheme=${scheme} axe violations=${axe.length}`); + for (const v of axe) console.log(` - [${v.impact}] ${v.id}: ${v.help} (${v.nodes} nodes) ${v.targets.join(" | ")}`); + } + } +} finally { + await call("DELETE", s("")); +} +writeFileSync(`${outDir}/${name}-axe.json`, JSON.stringify(summary, null, 2)); +const bad = summary.some((r) => r.violations.length > 0 || r.scheme !== r.file.split("-").pop().replace(".png", "")); +process.exit(bad ? 1 : 0); diff --git a/.mcp.json b/.mcp.json index bfbfce5..6b9ca9b 100644 --- a/.mcp.json +++ b/.mcp.json @@ -3,6 +3,10 @@ "clear-thought": { "command": "npx", "args": ["-y", "@waldzellai/clear-thought-onepointfive@0.2.1"] + }, + "mui-mcp": { + "command": "npx", + "args": ["-y", "@mui/mcp@0.1.8"] } } } From 0649c1c26044c5641e2f1a0a8281bc23b4ccbe5e Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:34:16 +0000 Subject: [PATCH 07/21] fix(ui): review round 2 (heading scale, color toggle before mode resolves) --- src/theme/fonts.ts | 2 +- src/theme/theme.ts | 3 +-- src/theme/useColorMode.ts | 11 ++++++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/theme/fonts.ts b/src/theme/fonts.ts index f208b58..b7e58df 100644 --- a/src/theme/fonts.ts +++ b/src/theme/fonts.ts @@ -1,5 +1,5 @@ // Self-hosted variable fonts (no CDN): Fraunces for display, Instrument Sans // for body/UI. Imported for side effects (the @font-face rules). -// opsz.css carries the optical-size axis that the theme's h1-h5 use. +// opsz.css carries the optical-size axis that the theme's h1 uses. import "@fontsource-variable/fraunces/opsz.css"; import "@fontsource-variable/instrument-sans"; diff --git a/src/theme/theme.ts b/src/theme/theme.ts index b44ba2f..839a183 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -19,6 +19,7 @@ export const theme = createTheme({ fontFamily: fontFamily.display, fontWeight: 600, letterSpacing: "-0.02em", + fontVariationSettings: '"opsz" 72', }, h2: { fontFamily: fontFamily.display, @@ -38,9 +39,7 @@ export const theme = createTheme({ h5: { fontFamily: fontFamily.display, fontWeight: 600, - fontSize: "2.25rem", letterSpacing: "-0.02em", - fontVariationSettings: '"opsz" 72', }, h6: { fontFamily: fontFamily.display, diff --git a/src/theme/useColorMode.ts b/src/theme/useColorMode.ts index 237a9d8..add435a 100644 --- a/src/theme/useColorMode.ts +++ b/src/theme/useColorMode.ts @@ -15,6 +15,15 @@ export function useColorMode() { mode: mode as ColorMode | undefined, resolved, setMode: (next: ColorMode) => setMode(next), - toggle: () => setMode(resolved === "dark" ? "light" : "dark"), + // Before the mode resolves, fall back to the OS preference so the first + // click flips what the user sees. + toggle: () => { + const shown = + resolved ?? + (window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"); + setMode(shown === "dark" ? "light" : "dark"); + }, }; } From eb88a9371e963afba7a9734f1218e41e7f29004d Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:44:04 +0000 Subject: [PATCH 08/21] docs(adr): note UI1 removed @fontsource/roboto --- docs/adrs/0003-re-parent-onto-template-docker.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/adrs/0003-re-parent-onto-template-docker.md b/docs/adrs/0003-re-parent-onto-template-docker.md index 5b5b761..c4a9f75 100644 --- a/docs/adrs/0003-re-parent-onto-template-docker.md +++ b/docs/adrs/0003-re-parent-onto-template-docker.md @@ -83,6 +83,8 @@ and later): We stay on MUI (plan decision P-D4). `@fontsource/roboto` stays until UI1 swaps the theme's fonts. +Update: UI1 (ADR 0004) swapped the fonts and removed `@fontsource/roboto`. + `pnpm-workspace.yaml` (new, merge tier) sets `allowBuilds: { esbuild: false }`. pnpm 12 fails an install on an unreviewed dependency build script, and Storybook pulls in esbuild, whose postinstall is only a fast path (its binary From 9d896639a739ec3fb55b548f71871085d64faaa0 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:44:15 +0000 Subject: [PATCH 09/21] fix(lint): token lint skips test files, keeps stories --- docs/adrs/0004-design-system-and-token-lint.md | 4 +++- scripts/check-design-tokens.sh | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/adrs/0004-design-system-and-token-lint.md b/docs/adrs/0004-design-system-and-token-lint.md index f5828b5..f6d2025 100644 --- a/docs/adrs/0004-design-system-and-token-lint.md +++ b/docs/adrs/0004-design-system-and-token-lint.md @@ -37,7 +37,9 @@ literals and raw `px` values in `src/**/*.{ts,tsx,css}` outside inline `design-tokens: allow ` marker; a marker without a reason fails. The hook runs the script's `--self-test` (fixtures for every violation kind, the marker and the `src/theme/` exemption) before scanning, -so the checker cannot silently stop catching things. +so the checker cannot silently stop catching things. Test files +(`*.test.ts`, `*.test.tsx`) are skipped because they may assert computed +colors; stories (`*.stories.tsx`) stay covered since they model token usage. **Contrast** is asserted in `src/theme/contrast.test.ts` (WCAG 2.2 AA, 4.5:1 for text); the ratios are in `docs/design/README.md`. diff --git a/scripts/check-design-tokens.sh b/scripts/check-design-tokens.sh index 342987c..fa61031 100755 --- a/scripts/check-design-tokens.sh +++ b/scripts/check-design-tokens.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash # Design-token guard (ADR 0004): rejects hex/rgb/hsl color literals and raw # `px` values in src/**/*.{ts,tsx,css} outside src/theme/, so every color and -# size comes from the theme (src/theme/tokens.ts). +# size comes from the theme (src/theme/tokens.ts). Test files (*.test.ts, +# *.test.tsx) are skipped because they may assert computed colors; stories +# (*.stories.tsx) stay covered since they model token usage. # # usage: check-design-tokens.sh [SRC_DIR] (default: src) # check-design-tokens.sh --self-test (runs against built-in fixtures) @@ -42,7 +44,8 @@ scan() { fi done <"$file" done < <(find "$root" -path "$root/theme" -prune -o -type f \ - \( -name '*.ts' -o -name '*.tsx' -o -name '*.css' \) -print0 | sort -z) + \( -name '*.ts' -o -name '*.tsx' -o -name '*.css' \) \ + ! -name '*.test.ts' ! -name '*.test.tsx' -print0 | sort -z) return "$status" } @@ -54,6 +57,9 @@ self_test() { echo 'export const c = "#B3361B"; export const p = "4px";' >"$d/theme/tokens.ts" printf 'const w = "100%%"; const id = "#root"; const v = "var(--x)";\n' >"$d/ok/clean.tsx" printf 'const w = "1px"; // design-tokens: allow hairline for a third-party widget\n' >"$d/ok/marked.tsx" + echo 'const c = "#abcdef"; const p = "12px";' >"$d/ok/color.test.tsx" + echo 'const c = "#abcdef"; const p = "12px";' >"$d/ok/color.test.ts" + echo 'const c = "#abcdef";' >"$d/bad/color.stories.tsx" echo 'a { color: #fff; }' >"$d/bad/hex.css" echo 'const c = "#abcdef";' >"$d/bad/hex6.ts" echo 'const c = "rgba(0, 0, 0, 0.5)";' >"$d/bad/rgb.ts" @@ -65,7 +71,7 @@ self_test() { scan "$d/ok" >/dev/null 2>&1 || { echo "self-test: clean fixtures were rejected" >&2; fail=1; } local f - for f in hex.css hex6.ts rgb.ts hsl.ts px.tsx negpx.tsx px.css noreason.ts; do + for f in hex.css hex6.ts rgb.ts hsl.ts px.tsx negpx.tsx px.css noreason.ts color.stories.tsx; do rm -rf "$d/one" && mkdir -p "$d/one" && cp "$d/bad/$f" "$d/one/" if scan "$d/one" >/dev/null 2>&1; then echo "self-test: $f was not rejected" >&2 From 023a24e6eac8ef3d5ec749cc54727ab9fcbaee36 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:47:43 +0000 Subject: [PATCH 10/21] fix(ui-verify): read data-mui-color-scheme for the scheme check --- .claude/skills/ui-verify/shoot.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/ui-verify/shoot.mjs b/.claude/skills/ui-verify/shoot.mjs index c3c47e9..a30ef26 100644 --- a/.claude/skills/ui-verify/shoot.mjs +++ b/.claude/skills/ui-verify/shoot.mjs @@ -64,9 +64,9 @@ try { await run("localStorage.setItem('mui-mode', arguments[0])", [mode]); await call("POST", s("/url"), { url }); await new Promise((r) => setTimeout(r, 1500)); - // MUI sets data-light / data-dark on for the resolved scheme. + // With colorSchemeSelector "data", MUI sets data-mui-color-scheme on . const scheme = await run( - "return document.documentElement.hasAttribute('data-dark') ? 'dark' : document.documentElement.hasAttribute('data-light') ? 'light' : null", + "return document.documentElement.getAttribute('data-mui-color-scheme')", ); const png = await call("GET", s("/screenshot")); const file = `${outDir}/${name}-${vp.label}-${mode}.png`; From dda67726bc856c56d3ba80da77560566476b1e97 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:50:16 +0000 Subject: [PATCH 11/21] fix(ui-verify,lint): keep partial axe results, wait for scheme, tolerate trailing slash --- .claude/skills/ui-verify/shoot.mjs | 14 ++++++++++++-- scripts/check-design-tokens.sh | 4 ++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.claude/skills/ui-verify/shoot.mjs b/.claude/skills/ui-verify/shoot.mjs index a30ef26..ec1acc6 100644 --- a/.claude/skills/ui-verify/shoot.mjs +++ b/.claude/skills/ui-verify/shoot.mjs @@ -63,7 +63,16 @@ try { await call("POST", s("/url"), { url }); await run("localStorage.setItem('mui-mode', arguments[0])", [mode]); await call("POST", s("/url"), { url }); - await new Promise((r) => setTimeout(r, 1500)); + // Wait (up to 10 s) for the scheme to resolve, then for fonts. + for (let i = 0; i < 40; i++) { + const cur = await run("return document.documentElement.getAttribute('data-mui-color-scheme')"); + if (cur === mode) break; + await new Promise((r) => setTimeout(r, 250)); + } + await call("POST", s("/execute/async"), { + script: "var done=arguments[arguments.length-1];document.fonts.ready.then(function(){done(true)})", + args: [], + }); // With colorSchemeSelector "data", MUI sets data-mui-color-scheme on . const scheme = await run( "return document.documentElement.getAttribute('data-mui-color-scheme')", @@ -83,8 +92,9 @@ try { } } } finally { + // Written even when a shot throws, so earlier results are kept. + writeFileSync(`${outDir}/${name}-axe.json`, JSON.stringify(summary, null, 2)); await call("DELETE", s("")); } -writeFileSync(`${outDir}/${name}-axe.json`, JSON.stringify(summary, null, 2)); const bad = summary.some((r) => r.violations.length > 0 || r.scheme !== r.file.split("-").pop().replace(".png", "")); process.exit(bad ? 1 : 0); diff --git a/scripts/check-design-tokens.sh b/scripts/check-design-tokens.sh index fa61031..526d91e 100755 --- a/scripts/check-design-tokens.sh +++ b/scripts/check-design-tokens.sh @@ -22,6 +22,7 @@ MARKER='design-tokens: allow' scan() { local root="$1" status=0 file + root="${root%/}" while IFS= read -r -d '' file; do local n=0 line while IFS= read -r line || [ -n "$line" ]; do @@ -78,6 +79,9 @@ self_test() { fail=1 fi done + # A trailing slash on the source dir must not defeat the theme/ exemption. + mkdir -p "$d/slash" && cp -r "$d/theme" "$d/slash/theme" + scan "$d/slash/" >/dev/null 2>&1 || { echo "self-test: trailing slash broke the theme/ exemption" >&2; fail=1; } # theme/ itself must be exempt. mkdir -p "$d/only" && cp -r "$d/theme" "$d/only/theme" scan "$d/only" >/dev/null 2>&1 || { echo "self-test: src/theme/ was not exempt" >&2; fail=1; } From 6401755e5c5c95b1a8950400e4d4384c9e103c13 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:51:18 +0000 Subject: [PATCH 12/21] fix(ui-verify): report HTTP status for non-JSON hub errors --- .claude/skills/ui-verify/shoot.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/ui-verify/shoot.mjs b/.claude/skills/ui-verify/shoot.mjs index ec1acc6..b550777 100644 --- a/.claude/skills/ui-verify/shoot.mjs +++ b/.claude/skills/ui-verify/shoot.mjs @@ -32,9 +32,9 @@ async function call(method, path, body) { headers: { "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); - const json = await res.json(); - if (!res.ok) throw new Error(`${method} ${path}: ${JSON.stringify(json)}`); - return json.value; + const text = await res.text(); + if (!res.ok) throw new Error(`${method} ${path}: HTTP ${res.status} ${text.slice(0, 500)}`); + return JSON.parse(text).value; } const created = await call("POST", "/session", { From 66b4aedc7c9453fac9098a018f85cbbf43e0dfa9 Mon Sep 17 00:00:00 2001 From: crazyracer98 Date: Thu, 22 Oct 2026 07:34:44 +0000 Subject: [PATCH 13/21] feat(ui): Storybook, visual regression and axe checks over the stories - Storybook 10 with light/dark toolbar, auth and API stubs, stories for App, HeroesPage and HealthStatus - Playwright toHaveScreenshot and axe suite in tests/visual/, run in the pinned Playwright image; arm64 baselines committed - visual.yml CI job (arm64 runner, report artifact on failure); ADR 0005; manifest tiers - Signed-out page: main landmark, h1, labelled spinners, no wrapping at 390px; matchMedia guard in useColorMode --- .github/template-sync-manifest.yml | 17 +++ .github/workflows/README.md | 4 + .github/workflows/visual.yml | 47 +++++++ .gitignore | 5 + .storybook/README.md | 22 ++++ .storybook/main.ts | 10 ++ .storybook/preview.tsx | 62 +++++++++ .storybook/stubs.tsx | 120 ++++++++++++++++++ README.md | 6 + ...005-storybook-visual-regression-and-axe.md | 88 +++++++++++++ package.json | 4 +- src/App.stories.tsx | 20 +++ src/App.test.tsx | 17 ++- src/App.tsx | 19 ++- src/components/HealthStatus.stories.tsx | 16 +++ src/components/HealthStatus.tsx | 35 ++++- src/pages/HeroesPage.stories.tsx | 36 ++++++ src/pages/HeroesPage.tsx | 6 +- src/theme/useColorMode.ts | 3 +- tests/visual/README.md | 22 ++++ .../app-app--auth-loading-desktop-dark.png | Bin 0 -> 10835 bytes .../app-app--auth-loading-desktop-light.png | Bin 0 -> 10757 bytes .../app-app--auth-loading-phone-dark.png | Bin 0 -> 8003 bytes .../app-app--auth-loading-phone-light.png | Bin 0 -> 7925 bytes .../app-app--backend-down-desktop-dark.png | Bin 0 -> 13605 bytes .../app-app--backend-down-desktop-light.png | Bin 0 -> 13415 bytes .../app-app--backend-down-phone-dark.png | Bin 0 -> 11307 bytes .../app-app--backend-down-phone-light.png | Bin 0 -> 11131 bytes .../app-app--signed-in-desktop-dark.png | Bin 0 -> 26584 bytes .../app-app--signed-in-desktop-light.png | Bin 0 -> 26105 bytes .../app-app--signed-in-phone-dark.png | Bin 0 -> 23045 bytes .../app-app--signed-in-phone-light.png | Bin 0 -> 22711 bytes .../app-app--signed-out-desktop-dark.png | Bin 0 -> 13352 bytes .../app-app--signed-out-desktop-light.png | Bin 0 -> 13170 bytes .../app-app--signed-out-phone-dark.png | Bin 0 -> 10455 bytes .../app-app--signed-out-phone-light.png | Bin 0 -> 10280 bytes ...ts-healthstatus--checking-desktop-dark.png | Bin 0 -> 6676 bytes ...s-healthstatus--checking-desktop-light.png | Bin 0 -> 6597 bytes ...onents-healthstatus--down-desktop-dark.png | Bin 0 -> 6477 bytes ...nents-healthstatus--down-desktop-light.png | Bin 0 -> 6437 bytes ...mponents-healthstatus--up-desktop-dark.png | Bin 0 -> 6209 bytes ...ponents-healthstatus--up-desktop-light.png | Bin 0 -> 6204 bytes ...roespage--add-dialog-open-desktop-dark.png | Bin 0 -> 28627 bytes ...oespage--add-dialog-open-desktop-light.png | Bin 0 -> 31810 bytes .../pages-heroespage--empty-desktop-dark.png | Bin 0 -> 12227 bytes .../pages-heroespage--empty-desktop-light.png | Bin 0 -> 12055 bytes ...es-heroespage--load-error-desktop-dark.png | Bin 0 -> 15205 bytes ...s-heroespage--load-error-desktop-light.png | Bin 0 -> 14968 bytes ...pages-heroespage--loading-desktop-dark.png | Bin 0 -> 9023 bytes ...ages-heroespage--loading-desktop-light.png | Bin 0 -> 8880 bytes ...s-heroespage--with-heroes-desktop-dark.png | Bin 0 -> 19714 bytes ...-heroespage--with-heroes-desktop-light.png | Bin 0 -> 19243 bytes tests/visual/playwright.config.ts | 47 +++++++ tests/visual/run.sh | 25 ++++ tests/visual/serve.mjs | 40 ++++++ tests/visual/stories.visual.ts | 85 +++++++++++++ 56 files changed, 741 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/visual.yml create mode 100644 .storybook/README.md create mode 100644 .storybook/main.ts create mode 100644 .storybook/preview.tsx create mode 100644 .storybook/stubs.tsx create mode 100644 docs/adrs/0005-storybook-visual-regression-and-axe.md create mode 100644 src/App.stories.tsx create mode 100644 src/components/HealthStatus.stories.tsx create mode 100644 src/pages/HeroesPage.stories.tsx create mode 100644 tests/visual/README.md create mode 100644 tests/visual/__screenshots__/app-app--auth-loading-desktop-dark.png create mode 100644 tests/visual/__screenshots__/app-app--auth-loading-desktop-light.png create mode 100644 tests/visual/__screenshots__/app-app--auth-loading-phone-dark.png create mode 100644 tests/visual/__screenshots__/app-app--auth-loading-phone-light.png create mode 100644 tests/visual/__screenshots__/app-app--backend-down-desktop-dark.png create mode 100644 tests/visual/__screenshots__/app-app--backend-down-desktop-light.png create mode 100644 tests/visual/__screenshots__/app-app--backend-down-phone-dark.png create mode 100644 tests/visual/__screenshots__/app-app--backend-down-phone-light.png create mode 100644 tests/visual/__screenshots__/app-app--signed-in-desktop-dark.png create mode 100644 tests/visual/__screenshots__/app-app--signed-in-desktop-light.png create mode 100644 tests/visual/__screenshots__/app-app--signed-in-phone-dark.png create mode 100644 tests/visual/__screenshots__/app-app--signed-in-phone-light.png create mode 100644 tests/visual/__screenshots__/app-app--signed-out-desktop-dark.png create mode 100644 tests/visual/__screenshots__/app-app--signed-out-desktop-light.png create mode 100644 tests/visual/__screenshots__/app-app--signed-out-phone-dark.png create mode 100644 tests/visual/__screenshots__/app-app--signed-out-phone-light.png create mode 100644 tests/visual/__screenshots__/components-healthstatus--checking-desktop-dark.png create mode 100644 tests/visual/__screenshots__/components-healthstatus--checking-desktop-light.png create mode 100644 tests/visual/__screenshots__/components-healthstatus--down-desktop-dark.png create mode 100644 tests/visual/__screenshots__/components-healthstatus--down-desktop-light.png create mode 100644 tests/visual/__screenshots__/components-healthstatus--up-desktop-dark.png create mode 100644 tests/visual/__screenshots__/components-healthstatus--up-desktop-light.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--add-dialog-open-desktop-dark.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--add-dialog-open-desktop-light.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--empty-desktop-dark.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--empty-desktop-light.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--load-error-desktop-dark.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--load-error-desktop-light.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--loading-desktop-dark.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--loading-desktop-light.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--with-heroes-desktop-dark.png create mode 100644 tests/visual/__screenshots__/pages-heroespage--with-heroes-desktop-light.png create mode 100644 tests/visual/playwright.config.ts create mode 100755 tests/visual/run.sh create mode 100644 tests/visual/serve.mjs create mode 100644 tests/visual/stories.visual.ts diff --git a/.github/template-sync-manifest.yml b/.github/template-sync-manifest.yml index 1c4ca46..9a4c9ed 100644 --- a/.github/template-sync-manifest.yml +++ b/.github/template-sync-manifest.yml @@ -116,6 +116,18 @@ replace: - vite.config.ts - vitest.config.ts - index.html + # Storybook and the visual-regression suite (ADR 0005). Generic: the story + # list comes from the build. .storybook/stubs.tsx is not here, it is + # instance content (below). + - .storybook/main.ts + - .storybook/preview.tsx + - .storybook/README.md + - tests/visual/README.md + - tests/visual/playwright.config.ts + - tests/visual/serve.mjs + - tests/visual/stories.visual.ts + - tests/visual/run.sh + - .github/workflows/visual.yml ignore: # Each instance chooses its own license and copyright holder. @@ -131,6 +143,11 @@ ignore: # sync never overwrites its code or static assets. - src/ + # Story fakes for this app's API and auth (preview.tsx imports its + # exports), and the baselines rendered from this app's stories. + - .storybook/stubs.tsx + - tests/visual/__screenshots__/ + # Env-var documentation for the instance's own deployment. - .env.example diff --git a/.github/workflows/README.md b/.github/workflows/README.md index d25cc43..0760f28 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -26,6 +26,10 @@ `dist/` as `release-assets-`; then one job downloads them all, runs `make release-check` inside the devcontainer and `make finalize` on the host, and creates a GitHub release with everything in `dist/`. +- `visual.yml` — template-react only: builds Storybook and runs the + Playwright screenshot and axe suite (`tests/visual/`) inside the pinned + Playwright image on the `arm64` runner, uploading the HTML report on + failure (see `docs/adrs/0005-storybook-visual-regression-and-axe.md`). - `moderate-bug-triage.yml` / `moderate-bug-fix.yml` / `moderate-bug-fix-apply.yml` / `moderate-feature-triage.yml` / `moderate-feature-build.yml` / `moderate-feature-build-apply.yml` / diff --git a/.github/workflows/visual.yml b/.github/workflows/visual.yml new file mode 100644 index 0000000..52925a9 --- /dev/null +++ b/.github/workflows/visual.yml @@ -0,0 +1,47 @@ +# Visual regression and axe over the built Storybook (ADR 0005). Runs inside +# the same pinned Playwright image as tests/visual/run.sh, on arm64: the +# committed baselines were rendered by that image on arm64, and a different +# CPU architecture can differ at the pixel level. +name: Visual + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + visual: + # renovate: datasource=github-runners depName=ubuntu + runs-on: ${{ vars.CI_RUNNER_ARM64 || 'ubuntu-24.04-arm' }} + # Keep the tag equal to @playwright/test in package.json and to IMAGE in + # tests/visual/run.sh. + container: + image: mcr.microsoft.com/playwright:v1.64.1-noble + options: --ipc=host + steps: + - name: Check out repository + uses: actions/checkout@v7.0.1 + + - name: Install dependencies + run: | + corepack enable + pnpm install --frozen-lockfile + + - name: Build Storybook + run: pnpm exec storybook build --quiet + + - name: Run visual and axe tests + run: pnpm exec playwright test --config tests/visual/playwright.config.ts + + # The report holds expected/actual/diff images for each failure. + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@v7.0.2 + with: + name: playwright-report + path: | + playwright-report/ + test-results/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 036e059..740d468 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,8 @@ lerna-debug.log* # Claude Code CLI: personal, per-user overrides — never the shared # .claude/settings.json. .claude/settings.local.json + +# Storybook build and Playwright output (ADR 0005) +storybook-static/ +playwright-report/ +test-results/ diff --git a/.storybook/README.md b/.storybook/README.md new file mode 100644 index 0000000..969c0da --- /dev/null +++ b/.storybook/README.md @@ -0,0 +1,22 @@ +# .storybook/ + +Storybook 10 configuration (ADR 0005). + +- `main.ts` — story globs (`src/**/*.stories.tsx`) and addons. +- `preview.tsx` — wraps every story in the app's `ThemeProvider`, adds the + light/dark toolbar switch (`theme` global) and installs the per-story fakes. +- `stubs.tsx` — the fakes: `parameters.auth` (`signedOut` | `signedIn` | + `loading`) and `parameters.api` (`health`, `heroes`), plus fixtures. This is + the instance-specific file; `preview.tsx` only needs `installApiStub`, + `StubAuthProvider` and `StubParameters` from it. + +## Do + +- Write a `*.stories.tsx` next to every component, covering its loading, + empty and error states, and signed-in and signed-out where auth matters. +- Use the theme (`sx`, `theme.vars`) in stories; the design-token lint covers + them. + +## Don't + +- Hit a real backend or Keycloak from a story. diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..43b8e39 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,10 @@ +import type { StorybookConfig } from "@storybook/react-vite"; + +const config: StorybookConfig = { + stories: ["../src/**/*.stories.@(ts|tsx)"], + addons: ["@storybook/addon-a11y"], + framework: "@storybook/react-vite", + core: { disableTelemetry: true }, +}; + +export default config; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000..156bb56 --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,62 @@ +import { useEffect, type ReactNode } from "react"; +import { useColorScheme } from "@mui/material/styles"; +import type { Preview } from "@storybook/react-vite"; +import { ThemeProvider } from "../src/theme"; +import { installApiStub, StubAuthProvider, type StubParameters } from "./stubs"; + +type Scheme = "light" | "dark"; + +/** Pushes the toolbar's choice into MUI, which then sets the data attribute. */ +function SchemeSync({ + scheme, + children, +}: { + scheme: Scheme; + children: ReactNode; +}) { + const { setMode } = useColorScheme(); + useEffect(() => setMode(scheme), [scheme, setMode]); + return children; +} + +const preview: Preview = { + globalTypes: { + theme: { + description: "Color scheme", + toolbar: { + title: "Scheme", + icon: "circlehollow", + items: [ + { value: "light", title: "Light" }, + { value: "dark", title: "Dark" }, + ], + dynamicTitle: true, + }, + }, + }, + initialGlobals: { theme: "light" }, + decorators: [ + (Story, { globals, parameters }) => { + const { api, auth } = parameters as StubParameters; + installApiStub(api); + return ( + + + + + + + + ); + }, + ], + parameters: { + layout: "fullscreen", + controls: { disable: true }, + // Fails the story's a11y panel on any violation; the Playwright suite + // (tests/visual/) enforces serious and critical ones in CI. + a11y: { test: "error" }, + }, +}; + +export default preview; diff --git a/.storybook/stubs.tsx b/.storybook/stubs.tsx new file mode 100644 index 0000000..420b99a --- /dev/null +++ b/.storybook/stubs.tsx @@ -0,0 +1,120 @@ +// Stubs for stories: a fake signed-in/out OIDC session and a fake backend, +// both chosen per story through `parameters` (see preview.tsx). Stories never +// touch Keycloak or the real API, so every render is deterministic. +import type { ReactNode } from "react"; +import { AuthContext, type AuthContextProps } from "react-oidc-context"; +import type { Hero } from "../src/api/heroes"; + +export type AuthState = "signedOut" | "signedIn" | "loading"; + +export interface ApiStub { + /** `GET /api/health/live`: "pending" never answers (the "checking" state). */ + health?: "up" | "down" | "pending"; + /** `GET` of the heroes list: rows, "pending" (loading) or an HTTP error. */ + heroes?: Hero[] | "pending" | { status: number; detail: string }; +} + +/** What a story may set in `parameters`; preview.tsx reads both. */ +export interface StubParameters { + auth?: AuthState; + api?: ApiStub; +} + +/** Two fixed heroes (stable ids and dates: screenshots must not drift). */ +const base = { + owner_id: "stub-owner", + is_draft: false, + archived_at: null, + publish_at: null, + unpublish_at: null, + is_locked: false, + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", +}; +export const heroFixtures: Hero[] = [ + { + ...base, + id: 1, + name: "Captain Ledger", + powers: ["Foresight", "Filing"], + power_level: 72, + }, + { + ...base, + id: 2, + name: "The Margin", + powers: ["Annotation"], + power_level: 41, + }, + { + ...base, + id: 3, + name: null, + powers: null, + power_level: null, + is_draft: true, + }, +]; + +const noop = async () => undefined; + +export function stubAuth(state: AuthState): AuthContextProps { + const signedIn = state === "signedIn"; + return { + isLoading: state === "loading", + isAuthenticated: signedIn, + user: signedIn + ? ({ + access_token: "stub-token", + profile: { preferred_username: "ada.lovelace" }, + } as AuthContextProps["user"]) + : null, + signinRedirect: noop, + removeUser: noop, + } as unknown as AuthContextProps; +} + +export function StubAuthProvider({ + state, + children, +}: { + state: AuthState; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +const json = (status: number, body: unknown) => + new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); + +const never = () => new Promise(() => {}); + +/** Replaces `window.fetch` with a fake of the two endpoints the app calls. */ +export function installApiStub(api: ApiStub = {}) { + const { health = "up", heroes = [] } = api; + window.fetch = async (input, init) => { + const url = new URL(String(input), window.location.origin); + const method = (init?.method ?? "GET").toUpperCase(); + if (url.pathname === "/api/health/live") { + if (health === "pending") return never(); + return health === "up" + ? json(200, { status: "ok" }) + : json(503, { title: "down" }); + } + if (url.pathname === "/api/crud/v1/heroes/v2/json" && method === "GET") { + if (heroes === "pending") return never(); + if (!Array.isArray(heroes)) { + return json(heroes.status, { detail: heroes.detail }); + } + return json(200, heroes); + } + return json(404, { title: "Not stubbed" }); + }; +} diff --git a/README.md b/README.md index 31747e9..6ee4ec5 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ decisions: the "Field Ledger" design system in `src/theme/` and the hook that keeps raw colors and `px` out of the rest of `src/`; principles in [`docs/design/`](docs/design/README.md). +- [`0005-storybook-visual-regression-and-axe.md`](docs/adrs/0005-storybook-visual-regression-and-axe.md) — + Storybook for every component, Playwright screenshot baselines and axe + checks over the stories, rendered in a pinned Playwright image. ## Getting started @@ -46,6 +49,9 @@ pnpm lint # oxlint pnpm format # prettier --check . pnpm test # vitest run pnpm build # production bundle -> dist/ +pnpm storybook # component workshop, http://localhost:6006 +tests/visual/run.sh # visual regression + axe over the stories (Docker) +tests/visual/run.sh update # rewrite the screenshot baselines ``` ## Connecting to the backend diff --git a/docs/adrs/0005-storybook-visual-regression-and-axe.md b/docs/adrs/0005-storybook-visual-regression-and-axe.md new file mode 100644 index 0000000..b27dc07 --- /dev/null +++ b/docs/adrs/0005-storybook-visual-regression-and-axe.md @@ -0,0 +1,88 @@ +# 0005. Storybook, visual regression and axe checks + +## Status + +Accepted + +## Context + +Track UI of `docs/plans/2026-10-templates-sdks-ui.md` wants every component +visible in isolation and a pass/fail signal for how it looks, so that neither +a person nor Claude can change the Field Ledger system (ADR 0004) without +noticing. Pixel baselines are only worth having if the same inputs always give +the same pixels, and CI has to reproduce what a developer generated locally. + +The plan said to generate baselines "in the toolchain container against the +stack's Chromium". Two things stood in the way. The stack's Selenium Grid +cannot be driven by Playwright: its CDP endpoint answers `ws://selenium:4444/.../se/cdp` +with HTTP 400 (found in UI1 and UI2), and `toHaveScreenshot` needs Playwright's +own browser connection. And the toolchain image has no fonts or browser +libraries, and installing them with `playwright install --with-deps` pulls +unpinned Debian packages, so the renderer would depend on the day it was built. + +## Decision + +**Storybook 10** (`@storybook/react-vite`, `@storybook/addon-a11y`) with a +story next to every component (`*.stories.tsx`). `.storybook/preview.tsx` wraps +every story in the app's own `ThemeProvider` and a toolbar switch for the +`theme` global (light or dark, applied through MUI's `useColorScheme`, so the +`data-mui-color-scheme` attribute is the real one). Sign-in state and the +backend are faked per story with `parameters.auth` (`signedOut`, `signedIn`, +`loading`) and `parameters.api` (`health`, `heroes`), implemented in +`.storybook/stubs.tsx`; stories never reach Keycloak or the API. Stories are +covered by the design-token lint. + +**Browser: Playwright's own Chromium, in the pinned official Playwright image** +(`mcr.microsoft.com/playwright:v1.64.1-noble`), not the stack's Selenium +Chromium and not a browser installed into the toolchain image. The image tag +equals the `@playwright/test` version, ships exactly that version's browser +build and a fixed font set, and is the single renderer for both baselines and +checks: `tests/visual/run.sh` runs it locally against the working tree +(`docker run`, no new service) and `visual.yml` runs the job inside the same +image. The app's own fonts are self-hosted (`@fontsource-variable/*`), so system +fonts only matter for fallbacks. This differs from the plan's wording in the +image, not in intent: the baselines are rendered in a container, by one browser +build, with fonts that cannot drift. + +**Architecture.** The baselines are rendered on `arm64`, so the CI job runs on +the `arm64` runner (`vars.CI_RUNNER_ARM64`, default `ubuntu-24.04-arm`). Chromium +output can differ between CPU architectures at the pixel level; one pinned +architecture is what makes a zero-pixel tolerance reachable. Developers on +`amd64` hosts should run `run.sh` with `--platform linux/arm64` (emulated and +slow) or let CI be the judge and download its report. + +**Tests.** `tests/visual/stories.visual.ts` reads the built Storybook's +`index.json`, so a new story is covered without editing the spec. For each +story it runs, in light and dark, `toHaveScreenshot` (full page, animations +disabled, `maxDiffPixels: 0`) at 1280x720, and an axe scan +(`@axe-core/playwright`) that fails on serious and critical violations. +Whole-app stories (title `App/...`) also run at 390x844. Baselines are +committed under `tests/visual/__screenshots__/`. + +**Updating baselines.** After an intended visual change: +`tests/visual/run.sh update`, then look at every changed PNG in the diff +(`git diff --stat`, open the images) before committing them with the change +that caused them. Never update to make a red run green without looking. A +Playwright version bump changes the image tag in `run.sh` and `visual.yml` +together and regenerates all baselines in the same commit. + +**CI.** `.github/workflows/visual.yml` runs on pull requests and on demand: +install, `storybook build`, `playwright test`; on failure it uploads +`playwright-report/` and `test-results/` (expected, actual and diff images) as +an artifact. + +## Consequences + +- A visual change is a reviewable diff of PNGs, and an accessibility + regression fails the build before it reaches the page. +- The suite needs Docker locally and a Playwright image pull (about 1.5 GB); + Playwright's version is pinned in three places (`package.json`, `run.sh`, + `visual.yml`) that must move together. A mismatch fails loudly: Playwright + refuses a browser build that is not its own. +- Baselines are valid for `arm64` only. Moving CI to `amd64` means + regenerating them there. +- axe only sees what a story renders. It finds roughly the automatable half of + WCAG issues; keyboard and screen-reader behaviour still need a human. +- `.storybook/stubs.tsx` and the baselines are instance content; the rest of + the setup is template-owned and copies to other instances (it is the base for + `lib-crud-views-ts`). diff --git a/package.json b/package.json index df39462..54be735 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "format": "prettier --check .", "format:write": "prettier --write .", "test": "vitest run", - "preview": "vite preview" + "preview": "vite preview", + "storybook": "storybook dev --port 6006 --no-open", + "storybook:build": "storybook build" }, "dependencies": { "@emotion/react": "11.14.0", diff --git a/src/App.stories.tsx b/src/App.stories.tsx new file mode 100644 index 0000000..833756a --- /dev/null +++ b/src/App.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { heroFixtures } from "../.storybook/stubs"; +import App from "./App"; + +const meta = { + title: "App/App", + component: App, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const SignedOut: Story = { parameters: { auth: "signedOut" } }; +export const AuthLoading: Story = { parameters: { auth: "loading" } }; +export const SignedIn: Story = { + parameters: { auth: "signedIn", api: { heroes: heroFixtures } }, +}; +export const BackendDown: Story = { + parameters: { auth: "signedOut", api: { health: "down" } }, +}; diff --git a/src/App.test.tsx b/src/App.test.tsx index d616720..e81f21e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,11 +1,14 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; import App from "./App"; const { useAuthMock } = vi.hoisted(() => ({ useAuthMock: vi.fn() })); vi.mock("react-oidc-context", () => ({ useAuth: useAuthMock })); describe("App", () => { + // Vitest globals are off, so Testing Library does not clean up on its own. + afterEach(cleanup); + it("prompts sign-in when unauthenticated", () => { useAuthMock.mockReturnValue({ isLoading: false, isAuthenticated: false }); @@ -19,6 +22,16 @@ describe("App", () => { ).toBeInTheDocument(); }); + it("has a main landmark and one h1 when unauthenticated", () => { + useAuthMock.mockReturnValue({ isLoading: false, isAuthenticated: false }); + + render(); + + expect(screen.getByRole("main")).toBeInTheDocument(); + expect(screen.getByRole("banner")).toBeInTheDocument(); + expect(screen.getAllByRole("heading", { level: 1 })).toHaveLength(1); + }); + it("shows a loading indicator while auth is resolving", () => { useAuthMock.mockReturnValue({ isLoading: true, isAuthenticated: false }); diff --git a/src/App.tsx b/src/App.tsx index a56f40d..56768a2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,14 +17,22 @@ function App() { <> - + template-react - + {auth.isAuthenticated ? ( <> - + {auth.user?.profile.preferred_username} @@ -44,10 +53,10 @@ function App() { - + {auth.isLoading ? ( - + ) : auth.isAuthenticated ? ( diff --git a/src/components/HealthStatus.stories.tsx b/src/components/HealthStatus.stories.tsx new file mode 100644 index 0000000..721f761 --- /dev/null +++ b/src/components/HealthStatus.stories.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { HealthStatus } from "./HealthStatus"; + +const meta = { + title: "Components/HealthStatus", + component: HealthStatus, + parameters: { layout: "centered" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Up: Story = { parameters: { api: { health: "up" } } }; +export const Down: Story = { parameters: { api: { health: "down" } } }; +/** The request never answers, so the chip stays in its first state. */ +export const Checking: Story = { parameters: { api: { health: "pending" } } }; diff --git a/src/components/HealthStatus.tsx b/src/components/HealthStatus.tsx index a38a262..822e1a7 100644 --- a/src/components/HealthStatus.tsx +++ b/src/components/HealthStatus.tsx @@ -2,11 +2,23 @@ // connection (wrong VITE_API_BASE_URL, backend down) is visible immediately // instead of surfacing only once someone opens the Heroes page. import { useEffect, useState } from "react"; +import Box from "@mui/material/Box"; import Chip from "@mui/material/Chip"; import { checkLive } from "../api/health"; const POLL_INTERVAL_MS = 30_000; +// Visually hidden, still in the accessibility tree. +const srOnly = { + position: "absolute", + width: "0.0625rem", + height: "0.0625rem", + margin: "-0.0625rem", + overflow: "hidden", + clip: "rect(0 0 0 0)", + whiteSpace: "nowrap", +} as const; + type Status = "checking" | "up" | "down"; export function HealthStatus() { @@ -33,12 +45,27 @@ export function HealthStatus() { }; }, []); - const label = + // "Backend" is always read by screen readers but only shown from the sm + // breakpoint up, so the app bar stays on one line on a phone. + const detail = status === "checking" - ? "Checking backend…" + ? "Checking…" : status === "up" - ? "Backend up" - : "Backend unreachable"; + ? "up" + : "unreachable"; + const label = ( + <> + ({ + [theme.breakpoints.down("sm")]: srOnly, + })} + > + Backend{" "} + + {detail} + + ); const color = status === "up" ? "success" : status === "down" ? "error" : "default"; diff --git a/src/pages/HeroesPage.stories.tsx b/src/pages/HeroesPage.stories.tsx new file mode 100644 index 0000000..917a7be --- /dev/null +++ b/src/pages/HeroesPage.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { userEvent, within } from "storybook/test"; +import { heroFixtures } from "../../.storybook/stubs"; +import { HeroesPage } from "./HeroesPage"; + +const meta = { + title: "Pages/HeroesPage", + component: HeroesPage, + parameters: { auth: "signedIn", layout: "padded" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const WithHeroes: Story = { + parameters: { api: { heroes: heroFixtures } }, +}; +export const Empty: Story = { parameters: { api: { heroes: [] } } }; +export const Loading: Story = { parameters: { api: { heroes: "pending" } } }; +export const LoadError: Story = { + parameters: { + api: { heroes: { status: 403, detail: "You need the viewer role." } }, + }, +}; + +/** The "Add hero" dialog, opened the way a user opens it. */ +export const AddDialogOpen: Story = { + parameters: { api: { heroes: heroFixtures } }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + await canvas.findByRole("button", { name: "Add hero" }), + ); + await within(document.body).findByRole("dialog", { name: "Add hero" }); + }, +}; diff --git a/src/pages/HeroesPage.tsx b/src/pages/HeroesPage.tsx index 32f2816..8b73a8c 100644 --- a/src/pages/HeroesPage.tsx +++ b/src/pages/HeroesPage.tsx @@ -100,7 +100,9 @@ export function HeroesPage() { direction="row" sx={{ justifyContent: "space-between", alignItems: "center" }} > - Heroes + + Heroes + @@ -110,7 +112,7 @@ export function HeroesPage() { {loading ? ( - + ) : ( diff --git a/src/theme/useColorMode.ts b/src/theme/useColorMode.ts index add435a..dfdd878 100644 --- a/src/theme/useColorMode.ts +++ b/src/theme/useColorMode.ts @@ -20,7 +20,8 @@ export function useColorMode() { toggle: () => { const shown = resolved ?? - (window.matchMedia("(prefers-color-scheme: dark)").matches + (typeof window.matchMedia === "function" && + window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"); setMode(shown === "dark" ? "light" : "dark"); diff --git a/tests/visual/README.md b/tests/visual/README.md new file mode 100644 index 0000000..1c41472 --- /dev/null +++ b/tests/visual/README.md @@ -0,0 +1,22 @@ +# tests/visual/ + +Playwright visual regression and axe checks over the built Storybook +(ADR 0005). + +- `run.sh` — builds Storybook and runs the suite inside the pinned Playwright + image; `run.sh update` rewrites the baselines. +- `stories.visual.ts` — one `toHaveScreenshot` and one axe test per story, + scheme and viewport, generated from the build's `index.json`. +- `playwright.config.ts`, `serve.mjs` — config and a static server for + `storybook-static/`. +- `__screenshots__/` — the committed baselines. + +## Do + +- Generate and check baselines only through `run.sh` (or the `visual.yml` + job). Look at every changed PNG before committing it. + +## Don't + +- Generate baselines with a locally installed browser, or raise + `maxDiffPixels` to quiet a diff. diff --git a/tests/visual/__screenshots__/app-app--auth-loading-desktop-dark.png b/tests/visual/__screenshots__/app-app--auth-loading-desktop-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1e9abe2d40073e96cb0e356c258519d85fba47c7 GIT binary patch literal 10835 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 literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--auth-loading-desktop-light.png b/tests/visual/__screenshots__/app-app--auth-loading-desktop-light.png new file mode 100644 index 0000000000000000000000000000000000000000..321d8c0b7ef2735f740f52424b81356add4d58d7 GIT binary patch literal 10757 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 Mar>$Auh;JVHzf%UrT_o{ literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--auth-loading-phone-dark.png b/tests/visual/__screenshots__/app-app--auth-loading-phone-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c85ba1f2f306ae1ad568f32771beda0f00d127f3 GIT binary patch literal 8003 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 Bi*En` literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--auth-loading-phone-light.png b/tests/visual/__screenshots__/app-app--auth-loading-phone-light.png new file mode 100644 index 0000000000000000000000000000000000000000..e8715e2bfe071fea84f5daad83b47d47f6c0999c GIT binary patch literal 7925 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 literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--backend-down-phone-light.png b/tests/visual/__screenshots__/app-app--backend-down-phone-light.png new file mode 100644 index 0000000000000000000000000000000000000000..aa0aa214e024f2c4f5cb33c45ad1575547079839 GIT binary patch literal 11131 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 literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--signed-in-desktop-dark.png b/tests/visual/__screenshots__/app-app--signed-in-desktop-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..3bb00674b3143872b0f12a5fa708d351f7053dd1 GIT binary patch literal 26584 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 literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--signed-in-desktop-light.png b/tests/visual/__screenshots__/app-app--signed-in-desktop-light.png new file mode 100644 index 0000000000000000000000000000000000000000..c6c6541f20ac5ea7d71eab981223f02ea4dd7a9d GIT binary patch literal 26105 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 literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--signed-out-desktop-dark.png b/tests/visual/__screenshots__/app-app--signed-out-desktop-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f93bca633ad226dbc6f3b55b41cef2cdcbe8d00b GIT binary patch literal 13352 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 literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--signed-out-desktop-light.png b/tests/visual/__screenshots__/app-app--signed-out-desktop-light.png new file mode 100644 index 0000000000000000000000000000000000000000..4924bbba8ddec4fa3c26c157f584d295d4c02f98 GIT binary patch literal 13170 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 F{{dOSj~V~~ literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--signed-out-phone-dark.png b/tests/visual/__screenshots__/app-app--signed-out-phone-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2d0b06a0b9caa2945a7fad17be73b0aaf30e1dbb GIT binary patch literal 10455 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 Re<-hBS{e*o1)E?NKp literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/app-app--signed-out-phone-light.png b/tests/visual/__screenshots__/app-app--signed-out-phone-light.png new file mode 100644 index 0000000000000000000000000000000000000000..e181817d80a4c9e562812bcc3e73146a35e4e31a GIT binary patch literal 10280 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 Gm;MX&g3z!4 literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/components-healthstatus--down-desktop-light.png b/tests/visual/__screenshots__/components-healthstatus--down-desktop-light.png new file mode 100644 index 0000000000000000000000000000000000000000..56fe9caf021683ee676796717235aad4d67e325c GIT binary patch literal 6437 zcmeI0>08ss7RP^z1r;GxTNhArTdq#54g6iva?9EiItl zs(`E!D+!1RWd|dPiUehg0YcaULSzj|2qYn7z46`cy?;XU<~uWIo_U@#pJ&c<&Ya(T z&w7|_HrosUfQjd~r+xr{&veuE?>Da34adM<>j2<$!1L5`|D@tYZg@^^R78ik$+SCX z_b=9m_o;>j-_~souH0apv9b19rP#;os~$?@_vjee`pAUt{TJ>}g#r7&yhpY_4JjOc z!pgqfefsHE+ieXS^EMe8yrOIt98Jiyy=M4!OI`P#UH$Qfs=6aLkF_3MQl^}Ei|K7! zaT7TOP9%@77RS>YsbXB!R!?5QEvU90a35>ngV+(5R^XV8|R4n(|& zxE^?#?)_+8#g4Ws-V3W{erYFB^9H_f-4OuT`ak-;LTS3knPuk>0Li+obJbTk!Ch!2 z2ccFm3F`Jk2wYrlAOIg&9IvIV)|-gHEs%tn$)j zcSg9-AMlMzG1&$LW-6#0>*nef;y1a^P4Rsr3NX@(qg|+}bh9@DnlB}!uWLRyaEB1f zliX@L8{XWU%uZdgD8zGFtGGL9HJe=R--}tlQ8dPh`@KU;temeyQzH|aNp;m#+OK`o zny-F2n}{`4At)pZRByz`YtBBP&=k+WxXwRHUk9!D6Bp+9%$X|~(wzV%_fLZ@FWE#< ze^CEq$n%gA!bha(g#!#xM1Ah-(rG0LMsytv3}oH9II5>@Gmo? zRSWjzps-|8)Jo0nRc_cCbXtrO_%v83~+!TYiG0z4(oQi$p3m0?k-P4#0nyaxJ+xZ9=<1PrnE*n&~V!bfRH?ybw+{ znwW!Nui_Ki8fgVpJ6e~Pv+@VO1CQWqS8IjZy-~ft!-xFT=tbxbO4y&#(t1DY>CnC} zOX8w0Fej@CuXn8!gS9hdVvstny@;-v92w1qGn){td`Vyco|SJ5Jk8&G0ic*ly@do= z9BAo-5zb{K!6F(bqTYofA12Q(bA;9imoZN2WBW2%&CytiyddHy%tUW#-|aduEpefH z86qa*YsFwv!j{l*P3)5;!_@{FS0Q`3$ZkR>|MU?uRhF?V*UZf9>v4dWSY<~{^#z;D z76nI#xGyjt-zOqmMR$r~vL9cZtYt7t4EA;yWre~|!qV`9h4Z;KgHbDKtWo95nO9v$ zi;QO%p?LuoV0=fUHw;3h?OH&NIxFr8TkiSMmM{)gr%Ol`@TM2JcdeU6gZwTY_(vDf z2cQeM7@I#7*&;YI`moYcZBtrua(8oW1=z4(VGph3CYk?sN2b^ zBeZMHyx$0kDbv47)#a1%i0OTW^2xaK_fqvvURk$B?*hdA%VvYe0x4QUN&-o zW3m@K=Zt-xRFm4T@kA0@1f?w|?H)!N8Ozd=$zc|zgjOmqF@0CQ{XEYtN&S`jGPxth9CNoeFslRP*<;B;5o#qnBCZ06moPje958vaYg;JEQrBQe%1 zl~=jFn%2i00AHI$zyO2YHCB_ndWWgCot`;w&dG(Lk(XWV6W>9{gIlNQ2VtA43)8y( zH6bfDgJcHvPpuUNpRFFQjenQd$!;+ghRWqnJA3D53x2eV6~5@)}1+*U0Hw?-LNvH*If)&`ZS({Y@^|Z2X?y{ z(HtwHS!3)fg(IA% z`g;dJuU`^N^J^V}il6aI)a`^XbSY9ZO9!Qj?#}MWuO46zw?_X{_wnxi)C+J5f}tY( zxKVHig+IK+;N*R)-m5R_apyA)o!x9M+rvUGDate%r9Y^<4{5U=eV;Eul`Oj(fl7o5 zNgX5FWIG>%M1{w6^kDoour7^la?%FfIOt#-6sfvUUWDifx|V^ov%=AC$5cz??8x?~-)-bB~N*syMk zUiAe2&IEh`;NQpo8dv`xX?lyEfS!P!z~6+xq*hlVXtj1LJ^{Lk;it;R02tWX@3i{p So}JDDc)FiGMLBWd#(w}G>A2JY literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/components-healthstatus--up-desktop-dark.png b/tests/visual/__screenshots__/components-healthstatus--up-desktop-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9dfbc300ba2af125ee8ff16991ea646da9116b GIT binary patch literal 6209 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 HiM{n7g$%NH literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/pages-heroespage--add-dialog-open-desktop-dark.png b/tests/visual/__screenshots__/pages-heroespage--add-dialog-open-desktop-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6c73e46d35b4c11693e66aaf419ee012c9104ec3 GIT binary patch literal 28627 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 W3yL$0DR!rK^H5pqLE(L~H~$O1***sV literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/pages-heroespage--empty-desktop-dark.png b/tests/visual/__screenshots__/pages-heroespage--empty-desktop-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..557315e92c40790a49dcba68cba9131810195c68 GIT binary patch literal 12227 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 A=l}o! literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/pages-heroespage--load-error-desktop-dark.png b/tests/visual/__screenshots__/pages-heroespage--load-error-desktop-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..42584b69d985e5958b05fd1042aa18abb7d618dc GIT binary patch literal 15205 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 I+0F;dNGynhq literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/pages-heroespage--load-error-desktop-light.png b/tests/visual/__screenshots__/pages-heroespage--load-error-desktop-light.png new file mode 100644 index 0000000000000000000000000000000000000000..cc026e22a01388e618c16af28e5485b0b27f85ab GIT binary patch literal 14968 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 literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/pages-heroespage--loading-desktop-dark.png b/tests/visual/__screenshots__/pages-heroespage--loading-desktop-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f265250f5de0d1d830335e2034f276cc2f1fafc6 GIT binary patch literal 9023 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 NE;?PPI(O~f-vF~^%}oFR literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/pages-heroespage--with-heroes-desktop-dark.png b/tests/visual/__screenshots__/pages-heroespage--with-heroes-desktop-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1825369f06774992ae33362eff8e6228f7474b9f GIT binary patch literal 19714 zcmeIacU;riwl^HdvCJSa7DS|E97a(QQIRf04W!&zMuR4asGkiw_)$K*Z%Fj*7v*C z+HsGK^n`yr|04hZ5Z3?g{u2P;h~VRQ!^gi9{JIN#cnARa37~)fo>@fBI#D!wBJ|@P z-?%0>F7JZRai8Oo-{~B`pX+g2Xc+nUC_dZDvMr6ExOLX%itBIoI{sHO_bB-mYmHUY zVl6SO^NeFnKfZ5z_u!ZBZ{1aSC;U$7opemDFa}e*<^S`~KZ!87(b~+C+8uVVwtEn- zZHTkO&Pg1SYfKUdGTX7ybo`rKKR*`;bo>S2C;;&D@$b$70MCCu{#4NL;{UKg;-sJx zJuH7Uos~A9|D|E-7)3ND0eggn#ANd3o)!A1}=e6p_e*O0+Sf>MG zBH*w+-wXa0aPOyf!O!pS{XaMSk+|}>E9m+j9){t+eStzcvM?@uj4l81 zL_{^Wz1kFdkW##%r8nP8=k-sLD~)vpeGZS>)T?jp>Dc^CXmBOQi)U2EwLsSEQo|R6 ziQ94;sNEjpgGk*V@C+K|)HRB!n8L9W(bBbH1J%ncP}b%wGS5EsHAc|UpdHx4q|+(V z!-Rcd5T@38w-{lewc7)_R!#`j9ZY~lZEqZeP9CCC9%JK9xoI zwj9Yn_I#UO^rmu~RwJWX9v-pWGP-LneC$0E+B>RS0BhW&Oza+0UZr&|s$&zOYfECG zBeCrtfrkZpYQI@@oxMMvhEz1v9m)32qd~S^eQjy(-M;m4hOQIzMKnqw{+6ioC@y|Y zwa?l2)JW`S6X98_gStbMJW^Rt=~Qp1H?JY!b4>;l@@K#`j3a!gv_8Rd{FC$6m}1lB zmjjQjFLH3}w?5k890&IkYAA{)F(^OD+c9bjiM>s}$a(FrimM$ieP#uIStLM}7txtkAZ4OCMRTkF* zRUyoU+}ivWLQDC25nrV~0scOknhT5YbZ_q7=GYxfVWD$x`D3k+GZpNmG*#~IJgk)q zH=5>=x7OtNG;bFeN`+W3g}UChRpib0yz4;hzHiT&Q87%V;tp1S*QD=vy~rB>Nb)PE zQa0MWzZ{7)^e%;b?mX=H&7{01$Ak)Ihs6%Jga;Y{Hu6Hemgv zVShJ1Pi~iB-Y)FVFRbwjTPnI=cKYFu8m_+H4YP+X(dKen7K^Oq%a+YC(p{Q~>)^hr zscF6URD4?vlvIXPgedh5YDNa!E8Zcf2uovf-?u$wzqQI3LDlwIT2_4BU=E^bHDgMa zNVw%2t9>`f!Kay8A=!@dADAIl+EtRr`i1 zuA&5gY-X~a4E=?eU;V|{kTv6;AquTR3+dFE_($&a)ECP<8}`z zVyIxlT6W>vYLvEIh)?eKh0L_2d1s}6jY`o6>sMkH8j-ke8;alblU%|L&V;HQY@e~? z+$&)WQk`2;m=-r~vr&dPv4<~QJlz6+T@UG5^|kG;EH`*y64kMGo|MN5@f;g4@uMbo zmPxwte65|ZN^+v>Ipi1SQTL5ri1mXwGeWO&YVpQ=zb(iR5&Ct4Y+&he2E`jUmf2e% z5X{~oqIkapLCuw(YrPal?$Yn-t3svmAUjSkHW}vTj%olk^1EBLA)hG^_?Amd`5I?+ zUu?@F6J~(-kBZ)lYlTgnX{q<-2T=Aj^9VAf%|L$lAA#_JIB5p0dmFn~ER=%fNY3p#ej5(G2hUy45XnUWORGFuDRsNZ@r-__i&vN3Znawp4`x^{hI zX3tt@2*;nxfC+S?O>W)FXiEU`OC1&Vu;NsajWGOTJz4uGnf>LaW2Dm#mdBW9zJ!`8 zV-MEJu+V*cO|46H_n^SGs;RW0JRdCCnp{)$pSFdSLgmGBUz4w1MMk^o^JjL9CetH= zgEi8AG^X9W));uV36im4nfpW;9cZ=%NF^19Zfu&)Cr!P3^9HX`8Y&4o9`1*tj_C~FST$od}(=Z}e#p>p+fR3%wl z@7L$1=CH_((j&cE$i!S|R1k2-jZ!xiNcoYsMTQ1Jcls(2(dt}GC>74$4W^Q`x@|Hl zakQ7!y&22eTcb0VanxEo{f@Iny0kIh)jM3lycN1-5LaSrD&xKs;tfoAaxAn}EUna$ z8@R-V)0=ldcX6c@*v2vgyPU>{kZGr3C@+kAy1>0r`;l}V%-+2}9<|aAl?{jPn$Wz< zE?tX8F_!L|I~7MvFSv}@icMsfX-7S2Oza+WO}p4|`ywTf0}r{8+)oeq%RymdjWK&= z@t$_eV3z9|k<16#J_nSsEo)LKVKeR;#!pQbIsNpf!J zmc{H~)wG;(1alXWsp(u|-pDo5I9IW4ysGjGWw%sbJSi#4&g3U#g@Y2?KaN?PtF&}* znyDGQG(S?SVjt+$Hor76Rwn5zC!VOW^he1N={KoQrC+oVvIx?e@)Ra;vVhx4`MMEe z<8&wN1{G03Y6`xqB9Ad{)WW9`nu3d0TC$WSO=AOEOLvWl?0Vx9Yzh^+*H2a~pJ@Xp zm-FDF;TPRFUo!jjO6`6xjt};?)bPf=J$%^u{dAl3K)!wN2kV z)IA1n9XaBHQQeG*<1-gc`Rp-<@tJ;!fZlp?>*~B|?Ri$wMh;EN>YIm_Z{A3vq%)zc z-5ybi5T~gR(wJvUyG{IUzxw*#c!i4Xpm~~7K2>q@<`pnMHB9Z}&L_n%lFOjDomoS0 zUU@^VKdP?79O-Lx-#W%Hf$m;flv%5pgkj{Doys1o$Pn|ke-HkP5bTG5@om|56B#q&Qs)kJ{W3 z&ugI4#i`?bhLK^$RX&BU%?_ennDMg+bKo0vC%1m`_Mucho9KRFJSKkzv8~ zjv1G&ucz*Zdf-X~KI+qFVbs@P=I85qnec3In1dgtzy#x2LCA@S4*AoWt#wpbDtb$| zIGk|PM%sIq*^-p~R?^X6qol)XXcef*nl~1qkt2JgtVhyACCSm6TlZ@dm zux5qt*t>O8>^=z2o+&9q?$5Yi*FgGHPUEzcSJPu1Cx`3utup5OA0(|z`*E(?OjOu6 z*C3XY6sE&`^$}iO`NDD;cEd_q1`m3avq{D>BwjxrxeER{ba!8K?^(<_{sc(3jSD#41%fCZG@r&Su&f+r;MR2$8KI2 zm)W32ONDc{DT>XTj5cCxG~QgBz0m5%T@so;X6?eJka_)@*GU`?VxJ>q$-)aI1#fK@bo&1wBY@PmmI7_SU7zOL@fAvH=a!C`nlYT--yT9Szog=w9wim&%f z74xk;81sigu_^HAUe6B#2VEAi-rpfBVz4EvB+oS1xm&$ZFa1EN1m$A6isc?>f(!a&#Q?N@ zfOP=DIea`@@GA~c#9&;Ho}d@0i^{f7 zw1R7Vbxl zV<{=(Hv{SI>%-LpCw7Gdd&=b9&}hfx&8eI1f)4*H^!v}q`(FU*|J2pjKS{8P*1Ygt zSBMS1O$|cW5wjh$th;)!S14;dy*JH&-x@@%Y_5rN$U)y)m?(y7(qdF4nRY62QPeIu z89r^TrHE=1%G(3kY5%Mm+}}br=5!=vhYh7xQ_;75N^NfY67oZZPX?@eFdzGROxiSl z+!nl(QCr)F(_u#bX~cT)RwinHu`$U@72Oim6%2z!b|e@7X`$|JcMxOz>>YQx5o=aR z@t@dQ7kH0xGlN3>&3=&YhPP`!*ySVmV>;!Yn2C{!MCkmncNe$&iX#U6t;&!FD$tv1)_ zVJLMNtD9a=C<>=TB$ti&+XD9Etle38`(n6Zy zYdHGpjGWAA23?~dl#)#Rk>KvGl6|H1>E`o|;*jf?dH0dY5@5yY(qPv7q1@U=$I$5F z`9nY&u6gXr5!&>#27)&BO0K3WuF|$SDs;?vsjpA9;eLB{;gn8&kfEUt1jcrGza~bw7#}qXz#y8S;wT{6ty6HmeQJf46ct;Q zYh{RpAM+1uOa3Z9nwqX+)Th3nL_YYU2Z9pD6w}d}Q_aTkWZMj`+uF-Qe-#vJFSZ}M z68(@#_6mc|ivSdsA<&9wd_`M7+VQ%mAHD>F3^Q1&ag~e*g}Je2Mmt}<>ONFa8&wZ! z-JWt)o)&5QSt?K=>_kA&)EKtHlRJ_CA~o-}%DLr??+m=aAr-PY>j`4QG^|#^Sl*5g zadWgqVTx!>&zabJOgDL#3O!L_@Sr{Repxbvlk=ASV2YmSfL=IU<16V9vBoMpKMFr9 z-r(JmAJ#D++IxDt6+WurMXa2Chx5P28zZaSZ-!_9fzsp#_j?C4gqT=t)qs3LBv-wWi2y#*Oi}rw74CeP363)h`k) z;Wk{a30zI6u{If(hl1%)}WQtTrFxv#{G zAMv?u6WV_qoh^xgIm$O`CT8@%tmaSPQ2Tf>WO|P642q^?-fKE^*`u~_yV*^#o-{D# zY!d|AuaJjB_q&vMbNNC)F0Thd%4QsZ3O>@lD^a+w2KlFL^fINvx#pXUKC#x65G1+9 zX=<&aI2^S5?m(3j?qRjF-a>exWij%Ta(U!_BL?OPzU@D~O^6abr>0WBI^66YrlX@n zY>C0Z9rhcSkcGh2iu32Ql7G`cvlAsMY>VL^8fa0u_a?muWm+X@#-Q3@H!dzwb>Vr-LGqJJ@MLGqi&-;eS7u<-lI<>syjfoNT}8N3D_LEzF@(Hm4eR zg`siA3=pLKN-%C?uSvB)hqYszfVb4bxKT##2`f|6QD^OE%9_-aTsll88*aDlt{KFw z0279^3VhuL=hIq6f6-{K{frAW#=dvB;mnYWSJWtob~Se$laCrJyV5)DbA~~x32t0f@V6*8+?zSBec8(rq)91s z$bhplM}&6_zrC2} z_+;44zPtikbka}pXdi~3K@_K`HmO(`i z_z|CVFY<4+tp45>NsONY!MPkQ?H@=DbA^4Kas&#lvdm-ugeF%#T-R{C!X;=aKB?8= zJ()B0;lkET9e-oeZG6}m##~b{1zsgSZgpn96!mY4SW(Y_gg(t4xQrlMF+109aX^Oy z9;pW*(?*<7b}H+ztCF>xE4G`McX*dxQIH^6# zUTlc1YcI9eG{;z+KGddY;~MUf-K=inav%m^4tif)e~rIDS*oXsdY*K=DlBE%Fo910 z6Y!k7ZSzAcvI~7o5Dx+*U;3wmF#pMS{D;CefA>EG@tol_E&ekBuo8rmHs|0;B5zJU z6?}OSpeEbiMX@_4U=rF#Qt=n$H10q6|0tOCKgY%X^5Fa7Uy?O9WSf1bpFOLK*B)P! zY?x>>;S6cg*U@WDC6H;AOo<-cjxPd@ULGXySw`OjeiAu!qV9E0gRhDl8DXG&ALwOW zojB7lg~YctQ?r z;cjlr)fR#;h8nn|g@*tne-c-qk$_)!FR!I#r?yh@6yAYPh|ga&e}Fm)jefi}Mfs#y zr@s109us1?qwgvNhcPpY3%YAxe}V?_AtNKQ-&D5;&JE|uJ%BB>K#Z=+1Q_khLFQ_uLjL%zR7Qk`eE)5aX zoTN?}Fmp3uqIV@=QFwoC&lvwGQ>~)>(sv>c%Ac-$fspoN3f#F8|G`S_QS8ZT zbcPXo>;1a=LS>7u(=P-%^AT@KxLL`+Z7pw!4wd<{YudF~BOO;gJ z&eEGE66!7%P#9=FN<|W&{o0v!G^N=yM89}a248f_74tp-`{I5Z>rW~=YT|=7d#W@^ zOmP6rc+-o~90*@+BNuTgig|gQ#osAY%%O&r60cG~a;OxvpSW;Lcd1aw{mzYg4P3#q z;n!5y?GN+{d=j7Nq!Lh08QxOOt9>>vT5O2}dZ5i}+I%8w_Gf{Yst@wDQ8n`;bA5fZ zCuV1`L-EgbYN{D?`YCkY2SlZp`8;>;k8&?Iqo>$i(Z*I%`qjV}V;HMgb$MWKKZ&JS zk&^s6oV=BFznCVOnMj~DG3f*!fZD6VxAM!6z#bzTyrd;z2`#s$X3Z-0m;%`Ct@wlb z9XIV8g&M`r_WtZghud0gxBRf9sw)L{u(nk{c#F;9Z{0igo|09@Q`bP)Tzam=ae$7g z8hFWYa7`TkfyJ0|ZLx8-M#v2DbY=5)n{N0}S+a;um>(P+L$l~4d7~HQYhhczeJ{21 z#JnG#i-~{(ExLAE&UB|P$*a=rJF8C%2?ecPb)3?EETy>eRe@vd}Bt7~>vUyP6rPrRi#pc~T8G?mh3X(E!e^UXQR?*Mf(O45ByoifED&@{{UV zQZ_2Z*Lzzg{1_lvYj?sxoG$XhD&r04{4hFt?T<|4qDV{V{ki?_Kdr{kom5&zws76a z*JZ|K!_nN2$tFZF;||4zBK`E0g&xP?vd!Xh@KBRaL`r;*e@;ySH2agvV73NCzc;Oe z(*SOWGJS^!ujuLw$#3KV4JzOB_ulo0tD||r0OqvMaBCmO&(88{1DD~ zpJPj%`S)}loH+mZow&K8{SEKsnNiwHY;_m}C&6s|H!k3quw>xK^DpIwONF&ewa?ZI zpDq}tk{%jonG?APV?ERGb!il`p4Qec0DZ4S)~u9whd9~%k)PEvW5n%)cix|Lr>H8s z(+JHeBydk%SAC>m>t#jz*VoREDx!V1GtgSdO$4#_qQl-GG2AFC75X(CxchRtQn6S6 z;*+W7YUNtk-r)4I`4WwQiZq`qR80gbm0K>zd@V)PP)_So7KebUYYkn7#y?b-e{nj^ z^PYeGrJH-tt6xS2%$fh3x`D~B3M5Q`I13Xa2smuDMOIB@W~LHSr3XTDvfvv{Mg7vN zLWN3K<6qH84fIcN^IMz^%7%v%K>O~GiY-A5>t1VeR`qR(t_-8>&5Fghg_1+J-VIMQc@&1(P}RCTmJgw zn2o+u%9k&D$-Y-KTqfVgH9C?C{8nd1<4Yn2I(t=5eARm2deU2!kLG3MRj1+~;D7>$zcJLRuv05<;BMjTXGVx$k0fbzMno1&{ zGkKjk9o{JhF;zl`P0bqH-(uL-);z3~n%KKMeSs9-sp@|&-Bvr4^JL7smDx{n@lfTL zlX^WD9zeHi>RwNA`KzYcucA}a`VGWUZ)e!aY7UdFj(&sjqvj&V=d0>Fj|W(U8HK$o zM-XBCUCt9bK$HtFF*4~eBDfzRXM_&9>*T{p7OpOd+8=a;J@a(Y!sekav&U>7I0WvO z|1c(V4Y~J#oPWJm2J&>RuZZsvg5*%sbwjg9kslAEdmOLH~F+(R_HpPJOIRmTEziIdfmGT^6P`df`V*}m9rHs<}o-ZmrnxAJxN zHsM0d5%p>h2tzvpFU{xa&_kboZEdK~!B2u`qycKoC8qR~x0Xuuxg%n9qu3l$hP@J^ z%6Z?uFgx*N;H1qn@s*ZmzT+1$f!CZt1s9e3i}}ux*NP4HA-)*2R(!mMYmbVndoh_* zRD4ms@9et9dxh#}pGUhJqjH@E?ufO1a4(|fh_^j_EaAzV6K zy)ls7RoGuPt~ghk6-hA%`JgcyaTi|()jaX;FALw;d*?BuFyA)QjU6Aw_?Abwx-1mP zr)OuRrcHQGCN&CAqkOKu5jP&nxw$$=I$bFdN;Q5^2$xMCh<^HoxPu5AI%%vZHt}m1 z)&t1rIx-XB`8DD#E!mJFo zTVkNsj-5u*TY1$*_62iQ_4fgI3(Vs;=bxCpJ8fInlj)hC!^0=F-T@EgrY`p!O=D^dN%;v|`hh|7*Lg>{2t(yoRpINK0 zj4v}*w6|FVt-QloZe<2W{QacVv$Vj0Xg@5ex==K^Ws_rl#Qg-Cu|3k!lc-<|-5HfO zO-)k{wa)!<$Ju1;^FehRDW(}X3wZ^47JlscPfLt*b~&c4A)g(JgA$h*K(4?o^j6xGo#V1AvpU|Aie>s}~_3VmL$yG1%# zr32~8>7m$1O@Ea{UgO|8u(+`EH``=xg|5$LXk(J1 zHE7kF-|%TKAv|&QHoYOR5gR!-1rj}{T<1;P5Y0G`OCO3**+RtW%0$%KJQVCj1tg7n)L0p`K>MBX`q8wR|hzn;#mH4^A8{d9I(!A zMF&yF3)eC*mIgWB%RlsuV$Olx@5}mO=#sKnFY$z1!C)p3+954Uyme!14`q0)saWr! zOLOy+y!uWsM8ObhALm?entr(@?jE}NsCj{1te>(YA^HJ?&wY$)OQ_7RN1 z8P0EHDo=N~R$o|vZBHJ6j3i(SdykF5*Efo+$Y6t(RQ;QR=!t9wb0+|=`G#nWko=@ z!)5aDA>aGgM0dYqAHMX7OyK>@Uhf=nai_yB#$HPY9c-{Wvh7V8eYP$AqSqTVg7!9^4tvaMB&$(EUUEW46E{&WJGifOUcV2y4yx;x7Z!_I^Cpq0$_^-RP&E z*nALgTNJINNw8Ac{ip{ALROjjWh_*~`;$-q@(z&$;l|Xm)UQ;pY*G1JTM_qI-HL6* zU9I-CS|l-;2f?OCyDt0UhtRDqCMI{Rs~hr*(CnJ(oa-p(vf^m!+7E`{qRuFX{bA)& z!u=o)^R^=l5k=Y+ADZ4W2-dcFpD)Jt1NS1UUCC& zRui$FAv04gzsJKJ1wM$B4&tHRQ)I7M+z1)PqV3IYy7bk>OqjI3?MF732TUv?Q?j?i zVa+2BtUA|?J*Sym7aHa`VDnx$9wKVDo!KvPqr(~k#`%$?q%W+hDk&nh>J(Q!KAxO) zTPw@BPNIlMe?mA~x4>PWm_QaXo||vDJgM1F+kR==`{qqlS@g&kh#h?9F_R(;wilN_ z7tMjV5U`l=FC9s-+mt+IXU4FRc`a~;xXH1V+~gUU59Nl~reyad9W0AJ_yp|m?sFp^ zD2ieoBA=lNjZ2WP8zKVP>YTX02i>-|1%FRvZPrBtj{R!h{B*Q4r&^VC8)8=8pwc&F zRPi2){UcUf^SWrV@UGYnuq}-DawN7hc6|c?0Nw#b&Z58g6*u^{(ouQFEv!C6^E4#V=zP)zySeoMuvyse+9$$(KMK$u*8ZvWZtP|2=9cnXg2bxX%U*Ag z_rCix|J5sB>uOPctluifiday4Afb;5dSA z+2{hjR)>t+Or{UA7}8j+E1GgU=k`ZuORn^Llhpd$KLP+APT!sLxIk~);j7@N^TSht?8%>GbWuUU zLE+r}88|#CS@^GGV({%LqF1#!IgA(;x!jPYj7m@KQlGgUtoBaG{$CKe$>Omt*VBr6)WHo9NrZFG${FI-ULKC{(6f4{TZg8<{ruQnUYMCAF> zeG-xpXOGYBBWhMbhMl$fMGn$RMNJ2D`WSHGm7i11&HZ&v@L#^j#7wN-CH*8_W94@* z#X)!Lp#PYWLay~l`NIWBY|^#WH}Kr6i9OD%xa8zN{>I{HNF1W4SbZMW2MLVKlsAx* zLA|69B_9RsoTt^8W}hm3nX7s2z_%I(aQ%Zq8jaY@OHb{5tE5it#{?*Gcqhx?Bhe} zVApaNRN%jH0f>?)cI{3EJ^5<{BV4}_(dY2zkFh7NzgKp3@A_5B`D_h$sXtlK`=;hME&25+hJtv?yU|0*Nd@1}6Ym>I8D6iOf2$w|o#e=cF; zDz)@7ASKu3`GW4tSE8+d<2HB;rZSFpsD;S7NN2qx38(jB_SZwl{+VLn7hjxjS9kQKBf1uaRBE9hY9QR=;0Ju8U2I z;)8IDEUjlNACk>Y>1}+hzpj8P8B>k=#*Q@h8MqH3HlP#51*NBp(+XSGNw(mg7b%a7 zcY)O@gGWW|+o+M^yCw3Fh@hawy^tR$3VD{?O6zR_Y30=?Pkt!fxtnD9jlT*90#EiQ zyDLNeW*)6~e#x)gBGeCN_-flC+}I3oy52E68rV+;5K#43UU4=4i#Z76z8mRs^pj(J z8Rbq8aiPEO>VgjJ_FceExQ5DiYG)PCL&yF=3sDB)wl+5c_TI;J9ci@Vbi~A2wSHXK z$O|wj_gQ941?T4{ROW=NR>h)2``>AWCqBApiOs^GKRvTT>H)8LU!856cY?U|(;aP; zqVbQmHVQDH!25MiyK}4!wudAF2RnOp8|^d$oqmZIKbw2Ad@s>yWM53c?E21-s~0wH zH)KGU%~gWC*V9Z!J}4T0O=wI}$jiLGYSuPIA{YuWbFJ*Sm4nI8J5DN|zg2g`TE+LC zr!O9n@ahDpj982DP8<;IfL>7Rg~pHvx+7H=Vv;nI#?!U#Fg3^0pV2w{HCaUkueuY} zG%;74z41d321(PcsB*uTYk5Y+A#fMViwLb-qd2xWxqV)l_LmxwZ}qI)mS}`gexHhy zae+1T_+ZZQ@52RI%DIsX=B#or{>FrntzC8_W>64<(0K)|9f<^RfD?+n!RW7v}6S%12_cQNay z1l$8KTUaXnG{^pSRaMy5dW~URq2T>ra2J(+jp4izr>c~|f(T7-;BD)DalzDhq?A^j zfe#2zVMqn;G`-F|u6SUVQ6x=T3~#OgH($2_*pFd`eDg$L7gLd9pI z7hZcXG4_$YuCf?rfG(+OB!d+vmtjl@ZU7>ECecT>AeCW{dnSKZ5e2AypA^e;P`#1G zZ>K3amezvA=r3Ia$p4x7)C)09J%hIsy)9OEH2ZuUC$&K72n7k{270o7^HlZB43AxX z1Wfa^v#buV=qiza1%K1(anG0Z+)1}|blPH~XlPPU$+EqQv6AUuee1UXg@Dh9h4v-; z_8EzCZp%z$AZ%LNyeRj2S0x7x6~I0tL#9uPaT$Zw4Oi&24t`}ty0 zN~wA6Lws^uh?V~5nfTlurhg7Up*Ka*_>*nzzcU@95C`%{Z=c^VlLU!yveK^LL>T!u z-eoL>JHr;rB0u$78t_)k$wa?~x~*iS5v(NT2k4X9XD_n2LD?gv&&Xwi*0u1#1c$9J z!hqNhtHb1Zg7pIMeFn7AzdeK1QGK$#)a+JT6ic(qYV2mjAkz3+rY0(COWTN?6t3~y*$S#L|dLT7ZFz5IzAjT3_B6uZU z33l>Db~TfIT970iazVNL6P1xx*63JiI6h8m(wJC}n1r0gq(xlh zmVgvh-Ran3XSuVEQ5}==$C!OdDi#yZmh?=Pc#VpEGkmX94*3pX_Gecnicx0CNRaEB ze~H;kdV5(AdB}wmuc)kLmDLo3RC;>)q$GNV^s7h}+9uqGzPBt?Xun@Mrr}5xF)s|x zhz=a6y61jIWbNI1{%kx$bJUF<7Hse!>AJoVo6uBhSfC#RN7wIVjYIu!<1kILom11~ ztlp{L%$m9iFbwmVz9nl=Fb;G-dCtV$O(UNq-dF3@U1?g}~`t7T%wcXHa zqnKv72Wd$uGyjpV5F0fX!aq7U2JSk%(2cdTJGm>vscqGh@~GeCI9BB59-2M=o7qsV zHl#kooMhSa^=3OK{c`qA6oR^9i`H5z539bcM0d#`?Se3Q+g%%+8R=irtnLJxAolwF z?b0z&i&a86#L_>?PBlGydQZ!_x{cQn%jzXz!MOWV<&hJ)4`w1dO77WjZ?~L4M-P}^ z%&qxl7IH@Z+>LzyHs>LRMDW?6Y%)X1eS?wN|K!NwaH(n7@$uC44Etsm6gNe7LGF!$C*OHIXfJJ2kvRV9{P z+eS6{T2j*9XO|s#6MsM75O&45YhHaCb5Jtn!{WnYB zuhr2fHjPRf`@|Iz1>WDTyaCxe&+7B+G+lBG%0r!&ad`YDF$jD9W|T)UVoKDa#BuL~ zzC|SouY=Y$Y1XJt=|A?K_S8ZM@KcL0RrFm!FM%Lyg^8uS{Ha|*{_mnSxnBg{Ig=_U zI%7FW8*lJsp)$4>!E(BK)5n~|R|Ln{c1xLR{p~FdRn4~u;@f*IY7P>8v=O3%D{JVkrf2`r_pCa}Su{nRu$9*g!_q@!p zOn`6zvn%R}AFW&O{_oX%{$oS`7kLO5WCd9FxookX334W9#vY^1qPQ2hyaWg`mS^e8 zn+q@FvD$KwkAwPh_UuCMEaR)Tdk3}HJGtoB2xIOdkCR_OxqKv`%_Y#J&1-dS&5VJL zUkasXWY|@E==>E%bAw21_&IJGm8o7D(XPG}R$;C+QEbU4{7h3giwj z#?-*cAD)O5>i*uYryV4*Roz^f>Ax<;>N?`vy*?R!U7F8E$23V^W&`5=hyP_raNe* zvTCR#D^li0{-?3{sn)A4=(Z5QuELI^;)V*g3=2VaM?0qgoRUY0UT-@*Fno34GCT%x z(Vy1|EkNHOX^$S z8CuWeDW|J$@a8+$R#^uD0_bVx92$BxFGDinmv2b;r^<98!DQOYuE%R>23ka#^13-9 z@J;SbcRBHa_v+sI8xpdW&9s`C2wILFBGn{ptNruE!_M6O;!|bm?*{En?BSehq!EwQ zP8gNgSuXG6Pkw!~fR;3{Q!Tw?ZeOioGOUbQi>Vp4R1t*E9VV;2@eCw3LxJ!(+seXW zLhX`N;gkB(%c`2`*Pq575^R(2%euZ(`*vhIInTW-H0-F_B3Tjx3^9H7l=3Wli$Ll- z`mVJ}145;59p4!4=QW{2Zg%ZU&P`WJsKH~=;2Hnrwzqy1D{Dj*S$Xw?;lwpKFTl*| z&d=UV+0)SKmO$k#;mEGTH3qBJl}al3OJsy$xq`g0fR2veU;gzW?56i>rB*t%pK{0H%~&=0!)ILx zhjDeq{!B&glaZ!;D+fBXxmRq7+K9aLEiRzi{K*HhGiKkl6D8h_!-(|UnYP0>)2&Dm zfB-+%eA{?@X8)?`@Z_qDi8uS;kUI+q?m8~wltG} zC??dq*ir?AV|6;bJo?OX=yC<)X8}oc+$Mqcope6|ax*VmGG)!nEBun*HG1Nk4)CXR zBkwzaCb~q7HMN+oAs$jroTZ|{x;REP96lAX8BpF|!Z~`z;##?%)S|DY)r>46y6#Rj z1a}%N7M_GCw){1V=!N~g;9F}4W6PG?#_bA!c9~P-}yo4&}&J09BR^IEgp3U?_M&=-pE1`Yl<Bd1?P_a!%H4D4b0;>x}X&LuQ_Vkoi)>SnGtx1@AO9NO#%P9 zip6T5mR9d!e7oxa0CZkGM7mjkOip%E2>n|z4C0?HhWV#1NBFyjQjlf$Q{kFq&~$yA ztIYNbfouWyKlR_{o|ewnkvUXJc>Sl~(d-KQw=?Fzs2u30onBvoCjr>23O@0VU(N7$ zNB>vDzm)jjSKIVo_=W#Wnbg0!`fqgL|JMs8h>%?P*RuToZdv}d-Tyc4Hu{&H{Fj~l u@7p+gd;tRD^MeLLzBz+Ozd@420O(22NYrzmO~Dg@zOK>z%D+5)@jn1atjawA literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/pages-heroespage--with-heroes-desktop-light.png b/tests/visual/__screenshots__/pages-heroespage--with-heroes-desktop-light.png new file mode 100644 index 0000000000000000000000000000000000000000..ae5e93ac9c7a683489f5e8834465095848c562e8 GIT binary patch literal 19243 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 literal 0 HcmV?d00001 diff --git a/tests/visual/playwright.config.ts b/tests/visual/playwright.config.ts new file mode 100644 index 0000000..a6579cb --- /dev/null +++ b/tests/visual/playwright.config.ts @@ -0,0 +1,47 @@ +// Visual regression and axe over the built Storybook (ADR 0005). +// Run it through tests/visual/run.sh: baselines are only valid when rendered +// by the pinned Playwright image, never by a locally installed browser. +import { defineConfig } from "@playwright/test"; + +const PORT = 6006; + +export default defineConfig({ + testDir: ".", + // Not *.spec.ts: Vitest would pick that up. + testMatch: "*.visual.ts", + // One folder per spec, one file per story/scheme/viewport; no OS suffix + // because the image is the only supported renderer. + snapshotPathTemplate: "{testDir}/__screenshots__/{arg}{ext}", + outputDir: "../../test-results", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: 0, + reporter: [ + ["list"], + ["html", { outputFolder: "../../playwright-report", open: "never" }], + ], + expect: { + toHaveScreenshot: { + animations: "disabled", + caret: "hide", + scale: "css", + // Anything above zero would hide real regressions; the pinned image is + // what makes zero reachable. + maxDiffPixels: 0, + }, + }, + use: { + baseURL: `http://127.0.0.1:${PORT}`, + colorScheme: "light", + locale: "en-US", + timezoneId: "UTC", + reducedMotion: "reduce", + deviceScaleFactor: 1, + }, + webServer: { + command: `node serve.mjs ${PORT}`, + cwd: import.meta.dirname, + url: `http://127.0.0.1:${PORT}/index.json`, + reuseExistingServer: false, + }, +}); diff --git a/tests/visual/run.sh b/tests/visual/run.sh new file mode 100755 index 0000000..3758a75 --- /dev/null +++ b/tests/visual/run.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Runs the visual-regression and axe suite inside the pinned Playwright image, +# the only renderer whose output is a valid baseline (ADR 0005). +# +# tests/visual/run.sh build Storybook, compare against baselines +# tests/visual/run.sh update build Storybook, rewrite the baselines +# +# Extra arguments after the mode go to `playwright test` (e.g. -g HeroesPage). +set -euo pipefail + +# Must equal the @playwright/test version in package.json: the image ships +# exactly that version's browser build, and Playwright refuses a mismatch. +IMAGE="mcr.microsoft.com/playwright:v1.64.1-noble" + +cd "$(dirname "$0")/../.." +mode="${1:-test}" +[ "$#" -gt 0 ] && shift + +args=(--config tests/visual/playwright.config.ts) +[ "$mode" = update ] && args+=(--update-snapshots) + +exec docker run --rm --ipc=host --init \ + -u "$(id -u):$(id -g)" -e HOME=/tmp -e CI="${CI:-}" \ + -v "$PWD:$PWD" -w "$PWD" "$IMAGE" \ + bash -c 'node_modules/.bin/storybook build --quiet && exec node_modules/.bin/playwright test "$@"' _ "${args[@]}" "$@" diff --git a/tests/visual/serve.mjs b/tests/visual/serve.mjs new file mode 100644 index 0000000..85bbf3c --- /dev/null +++ b/tests/visual/serve.mjs @@ -0,0 +1,40 @@ +// Minimal static file server for the built Storybook (storybook-static/). +// usage: node tests/visual/serve.mjs [port] +import { createReadStream, existsSync, statSync } from "node:fs"; +import { createServer } from "node:http"; +import { extname, join, normalize, resolve } from "node:path"; + +const root = resolve(import.meta.dirname, "../../storybook-static"); +const port = Number(process.argv[2] ?? 6006); +const types = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".woff2": "font/woff2", + ".woff": "font/woff", + ".png": "image/png", + ".ico": "image/x-icon", +}; + +createServer((req, res) => { + const { pathname } = new URL(req.url ?? "/", "http://localhost"); + const rel = normalize(decodeURIComponent(pathname)).replace(/^([/\\])+/, ""); + let file = resolve(join(root, rel)); + if (!file.startsWith(root)) { + res.writeHead(403).end(); + return; + } + if (existsSync(file) && statSync(file).isDirectory()) + file = join(file, "index.html"); + if (!existsSync(file)) { + res.writeHead(404).end("not found"); + return; + } + res.writeHead(200, { + "Content-Type": types[extname(file)] ?? "application/octet-stream", + }); + createReadStream(file).pipe(res); +}).listen(port, "127.0.0.1", () => console.log(`serving ${root} on :${port}`)); diff --git a/tests/visual/stories.visual.ts b/tests/visual/stories.visual.ts new file mode 100644 index 0000000..1e8a3b8 --- /dev/null +++ b/tests/visual/stories.visual.ts @@ -0,0 +1,85 @@ +// One screenshot test and one axe test per story, in light and dark, at a +// fixed viewport (plus a phone width for the whole-app stories). The story +// list comes from the built Storybook's index.json, so a new story is +// covered the moment it exists; its baseline is created with `run.sh update`. +import AxeBuilder from "@axe-core/playwright"; +import { expect, test, type Page } from "@playwright/test"; +import { readFileSync } from "node:fs"; + +interface IndexEntry { + id: string; + type: string; + title: string; +} + +const index = JSON.parse( + readFileSync( + new URL("../../storybook-static/index.json", import.meta.url), + "utf8", + ), +) as { entries: Record }; +const stories = Object.values(index.entries).filter((e) => e.type === "story"); + +const schemes = ["light", "dark"] as const; +const viewports = { + desktop: { width: 1280, height: 720 }, + phone: { width: 390, height: 844 }, +}; +// Whole-app stories (title "App/...") are also checked at phone width. +const viewportsFor = (title: string) => + title.startsWith("App/") + ? (["desktop", "phone"] as const) + : (["desktop"] as const); + +async function openStory(page: Page, id: string, scheme: string) { + await page.goto( + `/iframe.html?id=${id}&viewMode=story&globals=theme:${scheme}`, + ); + // Storybook has rendered the story and run its play function. + await page.waitForFunction(() => { + const phase = ( + window as unknown as { + __STORYBOOK_PREVIEW__?: { currentRender?: { phase?: string } }; + } + ).__STORYBOOK_PREVIEW__?.currentRender?.phase; + return phase === "finished" || phase === "errored" || phase === "aborted"; + }); + // colorSchemeSelector "data" makes MUI set an empty data- attribute. + await expect(page.locator(`html[data-${scheme}]`)).toBeAttached(); + await page.evaluate(() => document.fonts.ready); + await page.waitForLoadState("networkidle"); +} + +for (const story of stories) { + for (const vp of viewportsFor(story.title)) { + for (const scheme of schemes) { + const name = `${story.id}-${vp}-${scheme}`; + + test.describe(`${story.title} / ${story.id}`, () => { + test.use({ viewport: viewports[vp], colorScheme: scheme }); + + test(`screenshot ${vp} ${scheme}`, async ({ page }) => { + await openStory(page, story.id, scheme); + await expect(page).toHaveScreenshot(`${name}.png`, { + fullPage: true, + }); + }); + + test(`axe ${vp} ${scheme}`, async ({ page }) => { + await openStory(page, story.id, scheme); + const { violations } = await new AxeBuilder({ page }).analyze(); + const blocking = violations.filter( + (v) => v.impact === "serious" || v.impact === "critical", + ); + expect( + blocking.map((v) => ({ + id: v.id, + impact: v.impact, + nodes: v.nodes.map((n) => n.target), + })), + ).toEqual([]); + }); + }); + } + } +} From 271952e3d24bb54be3db891e87c92d66a76dd676 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:37:31 +0000 Subject: [PATCH 14/21] fix(ui-verify): read html[data-dark]/[data-light] for the scheme check - shoot.mjs fails on a null or mismatched scheme - SKILL.md, design README and ADRs 0004/0005 name the real attribute --- .claude/skills/ui-verify/SKILL.md | 3 ++- .claude/skills/ui-verify/shoot.mjs | 12 +++++++----- docs/adrs/0004-design-system-and-token-lint.md | 3 ++- .../adrs/0005-storybook-visual-regression-and-axe.md | 2 +- docs/design/README.md | 3 ++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.claude/skills/ui-verify/SKILL.md b/.claude/skills/ui-verify/SKILL.md index 188fe8c..2677482 100644 --- a/.claude/skills/ui-verify/SKILL.md +++ b/.claude/skills/ui-verify/SKILL.md @@ -32,7 +32,8 @@ not start a second browser service. `shoot.mjs` instead talks W3C WebDriver to the stack's existing Selenium, sets the viewport with a CDP `Emulation.setDeviceMetricsOverride` command (Grid's `goog/cdp/execute` endpoint works), switches the scheme through the `mui-mode` localStorage -key MUI persists, injects `axe-core` (a transitive dependency of +key MUI persists, checks that `html[data-dark]` or `html[data-light]` +matches the requested scheme, injects `axe-core` (a transitive dependency of `@axe-core/playwright`) and writes the PNGs: ```bash diff --git a/.claude/skills/ui-verify/shoot.mjs b/.claude/skills/ui-verify/shoot.mjs index b550777..0c7ee2e 100644 --- a/.claude/skills/ui-verify/shoot.mjs +++ b/.claude/skills/ui-verify/shoot.mjs @@ -50,6 +50,9 @@ const s = (p) => `/session/${sid}${p}`; const run = (script, args = []) => call("POST", s("/execute/sync"), { script, args }); mkdirSync(outDir, { recursive: true }); +const schemeScript = + "var h=document.documentElement;return h.hasAttribute('data-dark')?'dark':h.hasAttribute('data-light')?'light':null"; + const summary = []; try { for (const vp of viewports) { @@ -65,7 +68,7 @@ try { await call("POST", s("/url"), { url }); // Wait (up to 10 s) for the scheme to resolve, then for fonts. for (let i = 0; i < 40; i++) { - const cur = await run("return document.documentElement.getAttribute('data-mui-color-scheme')"); + const cur = await run(schemeScript); if (cur === mode) break; await new Promise((r) => setTimeout(r, 250)); } @@ -73,10 +76,9 @@ try { script: "var done=arguments[arguments.length-1];document.fonts.ready.then(function(){done(true)})", args: [], }); - // With colorSchemeSelector "data", MUI sets data-mui-color-scheme on . - const scheme = await run( - "return document.documentElement.getAttribute('data-mui-color-scheme')", - ); + // With colorSchemeSelector "data", MUI sets an empty html[data-dark] or + // html[data-light]; a null scheme or a mismatch fails the run below. + const scheme = await run(schemeScript); const png = await call("GET", s("/screenshot")); const file = `${outDir}/${name}-${vp.label}-${mode}.png`; writeFileSync(file, Buffer.from(png, "base64")); diff --git a/docs/adrs/0004-design-system-and-token-lint.md b/docs/adrs/0004-design-system-and-token-lint.md index f6d2025..32c36cf 100644 --- a/docs/adrs/0004-design-system-and-token-lint.md +++ b/docs/adrs/0004-design-system-and-token-lint.md @@ -19,7 +19,8 @@ was a scratch theme; this ADR records how it became the production system. **Theme.** `src/theme/` holds one MUI theme (`createTheme` with `cssVariables` and `colorSchemes` light and dark) built from `tokens.ts`, the only place raw colors and px values live. `colorSchemeSelector` is -`"data"` (not the prototype's `"media"`) so the app can offer a light/dark +`"data"` (MUI sets an empty `data-dark` or `data-light` attribute on +``; not the prototype's `"media"`) so the app can offer a light/dark toggle: with `"media"` MUI cannot set the mode; with `"data"` and `defaultMode="system"` it still follows the OS until the user chooses. `useColorMode()` wraps MUI's `useColorScheme`. Fonts are imported from diff --git a/docs/adrs/0005-storybook-visual-regression-and-axe.md b/docs/adrs/0005-storybook-visual-regression-and-axe.md index b27dc07..cb0bdfa 100644 --- a/docs/adrs/0005-storybook-visual-regression-and-axe.md +++ b/docs/adrs/0005-storybook-visual-regression-and-axe.md @@ -26,7 +26,7 @@ unpinned Debian packages, so the renderer would depend on the day it was built. story next to every component (`*.stories.tsx`). `.storybook/preview.tsx` wraps every story in the app's own `ThemeProvider` and a toolbar switch for the `theme` global (light or dark, applied through MUI's `useColorScheme`, so the -`data-mui-color-scheme` attribute is the real one). Sign-in state and the +`html[data-dark]` or `html[data-light]` attribute is the real one). Sign-in state and the backend are faked per story with `parameters.auth` (`signedOut`, `signedIn`, `loading`) and `parameters.api` (`health`, `heroes`), implemented in `.storybook/stubs.tsx`; stories never reach Keycloak or the API. Stories are diff --git a/docs/design/README.md b/docs/design/README.md index e4645f5..e22ce09 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -62,7 +62,8 @@ Public API (`import { ... } from "./theme"`): `ThemeProvider`, `theme`, - The default is the OS preference. `useColorMode()` returns `mode` (`"light" | "dark" | "system"`), `resolved`, `setMode` and `toggle`; MUI persists the choice in `localStorage`. -- Colors switch through CSS variables under a data attribute on the root element, so +- Colors switch through CSS variables under an `html[data-dark]` or `html[data-light]` attribute (MUI's + `colorSchemeSelector: "data"`), so `sx` and `theme.vars.*` need no per-scheme branches. Dark is not an inversion: ink and paper swap, and accents lighten (vermilion `#E8795C`, green `#9CC0A8`) to keep contrast. From 5bfe9fdf10539ddda7b29c44f9f0a766ba339645 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:38:50 +0000 Subject: [PATCH 15/21] build(storybook): type-check .storybook through tsconfig.app.json - dot directories are not matched by a bare directory include, so list the globs --- tsconfig.app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.app.json b/tsconfig.app.json index 6830b6f..e5a30f0 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -22,5 +22,5 @@ "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"] + "include": ["src", ".storybook/*.ts", ".storybook/*.tsx"] } From 29923011eaee8f0c6c1eab23d5661c0efd941592 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:39:04 +0000 Subject: [PATCH 16/21] build(visual): pin the Playwright image by digest as well as tag - run.sh and visual.yml use the multi-arch index digest - ADR 0005 documents bumping tag and digest together --- .github/workflows/visual.yml | 6 +++--- docs/adrs/0005-storybook-visual-regression-and-axe.md | 5 +++-- tests/visual/run.sh | 8 +++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/visual.yml b/.github/workflows/visual.yml index 52925a9..7053fe9 100644 --- a/.github/workflows/visual.yml +++ b/.github/workflows/visual.yml @@ -15,10 +15,10 @@ jobs: visual: # renovate: datasource=github-runners depName=ubuntu runs-on: ${{ vars.CI_RUNNER_ARM64 || 'ubuntu-24.04-arm' }} - # Keep the tag equal to @playwright/test in package.json and to IMAGE in - # tests/visual/run.sh. + # Keep the tag equal to @playwright/test in package.json, and the tag + # and digest equal to IMAGE in tests/visual/run.sh. container: - image: mcr.microsoft.com/playwright:v1.64.1-noble + image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:7852dc5a7d72f5fe458a597d690f6c42fbc8e9e26ddfa95f8e6ef552558d9bbf options: --ipc=host steps: - name: Check out repository diff --git a/docs/adrs/0005-storybook-visual-regression-and-axe.md b/docs/adrs/0005-storybook-visual-regression-and-axe.md index cb0bdfa..ac10646 100644 --- a/docs/adrs/0005-storybook-visual-regression-and-axe.md +++ b/docs/adrs/0005-storybook-visual-regression-and-axe.md @@ -63,8 +63,9 @@ committed under `tests/visual/__screenshots__/`. `tests/visual/run.sh update`, then look at every changed PNG in the diff (`git diff --stat`, open the images) before committing them with the change that caused them. Never update to make a red run green without looking. A -Playwright version bump changes the image tag in `run.sh` and `visual.yml` -together and regenerates all baselines in the same commit. +Playwright version bump changes the image tag and its `@sha256:` digest +(`docker buildx imagetools inspect `, the index digest) in `run.sh` and +`visual.yml` together and regenerates all baselines in the same commit. **CI.** `.github/workflows/visual.yml` runs on pull requests and on demand: install, `storybook build`, `playwright test`; on failure it uploads diff --git a/tests/visual/run.sh b/tests/visual/run.sh index 3758a75..0fecb53 100755 --- a/tests/visual/run.sh +++ b/tests/visual/run.sh @@ -8,9 +8,11 @@ # Extra arguments after the mode go to `playwright test` (e.g. -g HeroesPage). set -euo pipefail -# Must equal the @playwright/test version in package.json: the image ships -# exactly that version's browser build, and Playwright refuses a mismatch. -IMAGE="mcr.microsoft.com/playwright:v1.64.1-noble" +# The tag must equal the @playwright/test version in package.json: the image +# ships exactly that version's browser build, and Playwright refuses a +# mismatch. The digest (the multi-arch index) pins the exact bytes and is +# bumped together with the tag. +IMAGE="mcr.microsoft.com/playwright:v1.64.1-noble@sha256:7852dc5a7d72f5fe458a597d690f6c42fbc8e9e26ddfa95f8e6ef552558d9bbf" cd "$(dirname "$0")/../.." mode="${1:-test}" From cabb9b52f82ba3b439e054f1d1aef6e93c2cf334 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:39:12 +0000 Subject: [PATCH 17/21] docs(adr): note the digest pin in ADR 0005 --- docs/adrs/0005-storybook-visual-regression-and-axe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adrs/0005-storybook-visual-regression-and-axe.md b/docs/adrs/0005-storybook-visual-regression-and-axe.md index ac10646..5534c81 100644 --- a/docs/adrs/0005-storybook-visual-regression-and-axe.md +++ b/docs/adrs/0005-storybook-visual-regression-and-axe.md @@ -33,7 +33,7 @@ backend are faked per story with `parameters.auth` (`signedOut`, `signedIn`, covered by the design-token lint. **Browser: Playwright's own Chromium, in the pinned official Playwright image** -(`mcr.microsoft.com/playwright:v1.64.1-noble`), not the stack's Selenium +(`mcr.microsoft.com/playwright:v1.64.1-noble`, also pinned by digest), not the stack's Selenium Chromium and not a browser installed into the toolchain image. The image tag equals the `@playwright/test` version, ships exactly that version's browser build and a fixed font set, and is the single renderer for both baselines and From 7b32c791eb721ca0dae47987d036b2e5e3dfcec2 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:40:18 +0000 Subject: [PATCH 18/21] fix(theme): ink rule instead of drop shadow on dialogs and popovers - MuiDialog and MuiPopover paper: no shadow, 2px/1px text-color border - regenerate the Add-hero dialog baselines (only these two changed) --- src/theme/theme.ts | 17 +++++++++++++++++ ...roespage--add-dialog-open-desktop-dark.png | Bin 28627 -> 27024 bytes ...oespage--add-dialog-open-desktop-light.png | Bin 31810 -> 26838 bytes 3 files changed, 17 insertions(+) diff --git a/src/theme/theme.ts b/src/theme/theme.ts index 839a183..4ac58c4 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -60,6 +60,23 @@ export const theme = createTheme({ }, }, MuiPaper: { styleOverrides: { root: { backgroundImage: "none" } } }, + // Rules, not shadows (docs/design): floating surfaces get an ink rule. + MuiDialog: { + styleOverrides: { + paper: ({ theme }) => ({ + boxShadow: "none", + border: `2px solid ${theme.vars.palette.text.primary}`, + }), + }, + }, + MuiPopover: { + styleOverrides: { + paper: ({ theme }) => ({ + boxShadow: "none", + border: `1px solid ${theme.vars.palette.text.primary}`, + }), + }, + }, MuiTableCell: { styleOverrides: { root: ({ theme }) => ({ diff --git a/tests/visual/__screenshots__/pages-heroespage--add-dialog-open-desktop-dark.png b/tests/visual/__screenshots__/pages-heroespage--add-dialog-open-desktop-dark.png index 6c73e46d35b4c11693e66aaf419ee012c9104ec3..ea0f24e13d5cc4ce4918bbd88c634a45236bcdb2 100644 GIT binary patch literal 27024 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઱t&7; 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 W3yL$0DR!rK^H5pqLE(L~H~$O1***sV From 4d7b79eb936e402b9ae4535f3fe0f9bda331f503 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:47:14 +0000 Subject: [PATCH 19/21] fix: review round 1 (visual harness, health label, dockerignore) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - run.sh forces linux/arm64; stories fail unless the render phase is finished - serve.mjs: separator-safe traversal guard, 400 on malformed escapes - HealthStatus reads 'Backend checking…' (baselines regenerated) - shoot.mjs keeps the requested mode instead of parsing the file name - .dockerignore skips tests and Storybook/Playwright output --- .claude/skills/ui-verify/shoot.mjs | 4 ++-- .dockerignore | 4 ++++ src/components/HealthStatus.tsx | 2 +- ...ts-healthstatus--checking-desktop-dark.png | Bin 6676 -> 6418 bytes ...s-healthstatus--checking-desktop-light.png | Bin 6597 -> 6363 bytes tests/visual/run.sh | 4 +++- tests/visual/serve.mjs | 13 ++++++++++--- tests/visual/stories.visual.ts | 9 +++++++++ 8 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.claude/skills/ui-verify/shoot.mjs b/.claude/skills/ui-verify/shoot.mjs index 0c7ee2e..2fd34c9 100644 --- a/.claude/skills/ui-verify/shoot.mjs +++ b/.claude/skills/ui-verify/shoot.mjs @@ -88,7 +88,7 @@ try { ";var done=arguments[arguments.length-1];axe.run(document).then(function(r){done(r.violations.map(function(v){return {id:v.id,impact:v.impact,help:v.help,nodes:v.nodes.length,targets:v.nodes.slice(0,3).map(function(n){return n.target.join(' ')})}}))})", args: [], }); - summary.push({ file, scheme, violations: axe }); + summary.push({ file, mode, scheme, violations: axe }); console.log(`${file} scheme=${scheme} axe violations=${axe.length}`); for (const v of axe) console.log(` - [${v.impact}] ${v.id}: ${v.help} (${v.nodes} nodes) ${v.targets.join(" | ")}`); } @@ -98,5 +98,5 @@ try { writeFileSync(`${outDir}/${name}-axe.json`, JSON.stringify(summary, null, 2)); await call("DELETE", s("")); } -const bad = summary.some((r) => r.violations.length > 0 || r.scheme !== r.file.split("-").pop().replace(".png", "")); +const bad = summary.some((r) => r.violations.length > 0 || r.scheme !== r.mode); process.exit(bad ? 1 : 0); diff --git a/.dockerignore b/.dockerignore index 7e60ec4..7230634 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,6 +7,10 @@ .vscode .claude docs +tests +storybook-static +playwright-report +test-results README.md CLAUDE.md diff --git a/src/components/HealthStatus.tsx b/src/components/HealthStatus.tsx index 822e1a7..1be4b6d 100644 --- a/src/components/HealthStatus.tsx +++ b/src/components/HealthStatus.tsx @@ -49,7 +49,7 @@ export function HealthStatus() { // breakpoint up, so the app bar stays on one line on a phone. const detail = status === "checking" - ? "Checking…" + ? "checking…" : status === "up" ? "up" : "unreachable"; diff --git a/tests/visual/__screenshots__/components-healthstatus--checking-desktop-dark.png b/tests/visual/__screenshots__/components-healthstatus--checking-desktop-dark.png index d29b8216e0f6d0b63d92d729e14317b7cbed2361..9f36ecdae8e7c7f6f40c4643f6fa38da03fe4080 100644 GIT binary patch literal 6418 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 H{`>y|R&|rO literal 6597 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 { const { pathname } = new URL(req.url ?? "/", "http://localhost"); - const rel = normalize(decodeURIComponent(pathname)).replace(/^([/\\])+/, ""); + let decoded; + try { + decoded = decodeURIComponent(pathname); + } catch { + res.writeHead(400).end(); + return; + } + const rel = normalize(decoded).replace(/^([/\\])+/, ""); let file = resolve(join(root, rel)); - if (!file.startsWith(root)) { + if (file !== root && !file.startsWith(root + sep)) { res.writeHead(403).end(); return; } diff --git a/tests/visual/stories.visual.ts b/tests/visual/stories.visual.ts index 1e8a3b8..87b9e50 100644 --- a/tests/visual/stories.visual.ts +++ b/tests/visual/stories.visual.ts @@ -36,6 +36,12 @@ async function openStory(page: Page, id: string, scheme: string) { `/iframe.html?id=${id}&viewMode=story&globals=theme:${scheme}`, ); // Storybook has rendered the story and run its play function. + const phaseOf = () => + ( + window as unknown as { + __STORYBOOK_PREVIEW__?: { currentRender?: { phase?: string } }; + } + ).__STORYBOOK_PREVIEW__?.currentRender?.phase; await page.waitForFunction(() => { const phase = ( window as unknown as { @@ -44,6 +50,9 @@ async function openStory(page: Page, id: string, scheme: string) { ).__STORYBOOK_PREVIEW__?.currentRender?.phase; return phase === "finished" || phase === "errored" || phase === "aborted"; }); + // A story that threw or whose play function failed must fail the test, not + // be baselined as an error overlay. + expect(await page.evaluate(phaseOf)).toBe("finished"); // colorSchemeSelector "data" makes MUI set an empty data- attribute. await expect(page.locator(`html[data-${scheme}]`)).toBeAttached(); await page.evaluate(() => document.fonts.ready); From 19a198fe431a85d9aece28f89303d95c453e28f5 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:52:45 +0000 Subject: [PATCH 20/21] fix: review round 2 (run.sh arch guard and argument parsing, single phase check) - run.sh refuses non-arm64 hosts and no longer swallows a leading option - stories wait for phase finished with one expect.poll - .dockerignore skips .pnpm-store --- .dockerignore | 1 + tests/visual/README.md | 3 ++- tests/visual/run.sh | 15 ++++++++++----- tests/visual/stories.visual.ts | 18 ++++++------------ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.dockerignore b/.dockerignore index 7230634..2a55e1b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,5 +19,6 @@ CLAUDE.md # the container's node_modules with the host's, which may not match its # platform/arch. node_modules +.pnpm-store coverage dist diff --git a/tests/visual/README.md b/tests/visual/README.md index 1c41472..abdecde 100644 --- a/tests/visual/README.md +++ b/tests/visual/README.md @@ -4,7 +4,8 @@ Playwright visual regression and axe checks over the built Storybook (ADR 0005). - `run.sh` — builds Storybook and runs the suite inside the pinned Playwright - image; `run.sh update` rewrites the baselines. + image; `run.sh update` rewrites the baselines. Arm64 hosts only (the + baselines are arm64 renders); the script refuses to run elsewhere. - `stories.visual.ts` — one `toHaveScreenshot` and one axe test per story, scheme and viewport, generated from the build's `index.json`. - `playwright.config.ts`, `serve.mjs` — config and a static server for diff --git a/tests/visual/run.sh b/tests/visual/run.sh index 950f019..bb39ae2 100755 --- a/tests/visual/run.sh +++ b/tests/visual/run.sh @@ -15,15 +15,20 @@ set -euo pipefail IMAGE="mcr.microsoft.com/playwright:v1.64.1-noble@sha256:7852dc5a7d72f5fe458a597d690f6c42fbc8e9e26ddfa95f8e6ef552558d9bbf" cd "$(dirname "$0")/../.." -mode="${1:-test}" -[ "$#" -gt 0 ] && shift +mode=test +case "${1:-}" in test | update) mode="$1" && shift ;; esac + +# The baselines are arm64 renders (ADR 0005) and node_modules is mounted from +# the host, so an amd64 host can neither compare nor update them. +if [ "$(uname -m)" != aarch64 ] && [ "$(uname -m)" != arm64 ]; then + echo "tests/visual/run.sh needs an arm64 host (baselines are arm64, ADR 0005); CI runs it." >&2 + exit 1 +fi args=(--config tests/visual/playwright.config.ts) [ "$mode" = update ] && args+=(--update-snapshots) -# Baselines are arm64 only (ADR 0005): force it so an amd64 host emulates -# instead of rendering differently or rewriting the baselines on `update`. -exec docker run --rm --platform linux/arm64 --ipc=host --init \ +exec docker run --rm --ipc=host --init \ -u "$(id -u):$(id -g)" -e HOME=/tmp -e CI="${CI:-}" \ -v "$PWD:$PWD" -w "$PWD" "$IMAGE" \ bash -c 'node_modules/.bin/storybook build --quiet && exec node_modules/.bin/playwright test "$@"' _ "${args[@]}" "$@" diff --git a/tests/visual/stories.visual.ts b/tests/visual/stories.visual.ts index 87b9e50..ac5b055 100644 --- a/tests/visual/stories.visual.ts +++ b/tests/visual/stories.visual.ts @@ -35,24 +35,18 @@ async function openStory(page: Page, id: string, scheme: string) { await page.goto( `/iframe.html?id=${id}&viewMode=story&globals=theme:${scheme}`, ); - // Storybook has rendered the story and run its play function. const phaseOf = () => ( window as unknown as { __STORYBOOK_PREVIEW__?: { currentRender?: { phase?: string } }; } ).__STORYBOOK_PREVIEW__?.currentRender?.phase; - await page.waitForFunction(() => { - const phase = ( - window as unknown as { - __STORYBOOK_PREVIEW__?: { currentRender?: { phase?: string } }; - } - ).__STORYBOOK_PREVIEW__?.currentRender?.phase; - return phase === "finished" || phase === "errored" || phase === "aborted"; - }); - // A story that threw or whose play function failed must fail the test, not - // be baselined as an error overlay. - expect(await page.evaluate(phaseOf)).toBe("finished"); + // Storybook has rendered the story and run its play function. A story that + // threw or whose play function failed never reaches "finished", so it fails + // the test instead of being baselined as an error overlay. + await expect + .poll(() => page.evaluate(phaseOf), { timeout: 30_000 }) + .toBe("finished"); // colorSchemeSelector "data" makes MUI set an empty data- attribute. await expect(page.locator(`html[data-${scheme}]`)).toBeAttached(); await page.evaluate(() => document.fonts.ready); From 3bacd8d513d5135f00ece48ab76b018009bfa31a Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:51:02 +0000 Subject: [PATCH 21/21] chore(template-sync): sync to template-docker v1.2.0 - replace tier: Makefile, image scripts (smoke, scan, skopeo, runner-setup), test-image-scripts.sh, claude/plan docs - merge tier: .dockerignore, .pre-commit-config.yaml, manifest, READMEs, docs/TEMPLATE.md - state: v1.0.0 -> v1.2.0 --- .claude/README.md | 11 ++- .claude/agents/plan-executor.md | 69 +++++++++++++- .claude/skills/plan-execution/SKILL.md | 72 ++++++++++----- .dockerignore | 3 + .github/template-sync-manifest.yml | 1 + .github/template-sync-state.json | 2 +- .github/workflows/README.md | 11 +-- .pre-commit-config.yaml | 23 +++-- CLAUDE.md | 8 +- Makefile | 73 ++++++++++----- docs/TEMPLATE.md | 86 +++++++++++++----- docs/plans/README.md | 15 +++- scripts/README.md | 16 ++-- scripts/check-dockerfile-versions.sh | 39 +++++--- scripts/develop.d/README.md | 7 +- scripts/image-scan.sh | 27 +++++- scripts/image-smoke.sh | 27 ++++-- scripts/post-create.d/README.md | 7 +- scripts/runner-setup.d/README.md | 7 +- scripts/runner-setup.sh | 12 ++- scripts/skopeo.sh | 4 +- scripts/test-image-scripts.sh | 119 +++++++++++++++++++++++++ 22 files changed, 503 insertions(+), 136 deletions(-) create mode 100755 scripts/test-image-scripts.sh diff --git a/.claude/README.md b/.claude/README.md index b031c31..0aa1b3f 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -30,14 +30,19 @@ this directory). the main session as orchestrator (see `../CLAUDE.md`, "Execute multi-item work as an orchestrator"). It holds the executor's side of the protocol: what a brief contains, the write-set and no-push rules, - the escalation triggers, and the report format. + the escalation triggers, and the report format. The same agent, briefed + to land a finished item, is a lander (its "Lander mode" section): + diff check, merge, verification lane, PR, `/code-review`, CI and, + within the plan's standing approvals, merge and release. - `skills/` — project skills Claude loads when a request matches their `description` (or that you invoke as `/`): - `plan-creation` — writing a `docs/plans/` plan and, when the user has setup to do first, its pre-flight runbook. - `plan-execution` — the orchestrator's side of executing a plan: - the rolling queue of `plan-executor` subagents, researched answers - to escalations, serialized lanes, gates and close-out. + the rolling queue of executors and landers, researched answers to + escalations, serialized lanes, gates and close-out. The main session + decides and raises gates; landers do the reviewing, merging, + verifying and releasing. - `owasp-scan` — a parallel OWASP Top Ten scan of `src/`. - `frontend-design` — how to design UI here: the "Field Ledger" direction and its anti-defaults, proposing 2-3 distinct options for diff --git a/.claude/agents/plan-executor.md b/.claude/agents/plan-executor.md index 1e6be0d..080ff9e 100644 --- a/.claude/agents/plan-executor.md +++ b/.claude/agents/plan-executor.md @@ -1,10 +1,11 @@ --- name: plan-executor -description: Executes exactly one work item of a docs/plans/ plan, briefed by the orchestrating main session. Works only inside its brief's write set, in its own worktree, and hands back to the orchestrator instead of improvising when it hits an escalation trigger. Use one per independent work item; run independent items in parallel. +description: Executes exactly one work item of a docs/plans/ plan, briefed by the orchestrating main session, or (lander mode) lands one finished item or one repo's wave PR. Works only inside its brief's write set, in its own worktree, and hands back to the orchestrator instead of improvising when it hits an escalation trigger. Use one per independent work item; run independent items in parallel. model: sonnet --- -You execute one work item from a plan under `docs/plans/`. The +You execute one work item from a plan under `docs/plans/`, or, when +your brief says you are the lander ("Lander mode" below), land one. The orchestrator (the main session) wrote your brief. It owns every decision outside that brief and talks to the user. Your job is to finish your item and report it accurately. @@ -70,3 +71,67 @@ COMMIT: ESCALATION: ``` + +## Lander mode + +A brief that says "lander" makes you the one who lands a finished item, +or one repo's wave PR, instead of implementing. The executor rules above +still apply except where this section says otherwise: you may push the +branches and open or update the wave PRs the brief names and, only as +its standing approvals allow, merge and release. Never tag, publish, +delete a branch or create a repo unless the brief names that step as a +standing approval. Merging `wip/` into the integration branch (step 2) +is always yours, in the repo checkout the brief names, and isn't a +"merge" in the sense below; merging the wave PR (step 7) and releasing +need a standing approval, so none in the brief means no PR merge and no +release. + +**The brief contains:** the item ID(s) and the executor's report; the +original brief's write set and "Done when"; the repo, its integration +branch and the wave PR (existing or to open); the verification lane's +exact commands, and whether you hold the slot for suites that share the +devcontainer's services or fixed ports; and the standing approvals that +say whether you may merge and release in this repo. A PR the user merges +is never yours to merge. + +**Steps:** + +1. Check the executor's diff against the write set and "Done when": every + changed file inside the write set, `CHECKS` showing real output. +2. Merge `wip/` into the repo's integration branch. +3. Run the verification lane. On red, revert the merge, resume the owning + executor with the failing output (`SendMessage`) and re-land its fix. + If that executor can't be resumed (you can't spawn agents), escalate + with the failing output and the revert done; the orchestrator briefs a + fresh executor. Escalate also if the fix can't be made inside the + write set. +4. Push and open or update the wave PR. +5. Run `/code-review` on the PR, naming the PR or the `origin/main...HEAD` + diff explicitly, never the shell's cwd. Fix findings inside the write + set and re-run the lane after each round. +6. Wait for CI. +7. Merge and trigger releases only where the standing approvals allow, + once CI is green and the review is clean. + +**Escalate** (same report shape, same rule of keeping the worktree as it +is) on: a red lane you can't fix inside the write set; a review finding +that needs a decision, an interface or dependency change, or a wider write +set; two review rounds in a row that find new correctness issues in the +same area; anything outside the standing approvals; and every user gate. If a +finding that needs a decision arrives after the merge, revert the merge +(`git revert -m 1`) before escalating, so nobody builds on it; if other +merges sit on top of it and the revert conflicts, say so in the +escalation instead of forcing it. + +**Report:** + +``` +STATUS: done | escalate +PR: +MERGE: "> +CI: +REVIEW: +LANE: verbatim output tail> +NOTES: +ESCALATION: +``` diff --git a/.claude/skills/plan-execution/SKILL.md b/.claude/skills/plan-execution/SKILL.md index ca29d1d..a0eca5c 100644 --- a/.claude/skills/plan-execution/SKILL.md +++ b/.claude/skills/plan-execution/SKILL.md @@ -1,14 +1,19 @@ --- name: plan-execution -description: Execute a docs/plans/ plan, or any task that splits into several independent items, as the orchestrator - briefing plan-executor subagents in their own worktrees, answering their escalations with researched decisions, merging, verifying and running the user gates. Use when the user asks to execute or continue a plan. +description: Execute a docs/plans/ plan, or any task that splits into several independent items, as the orchestrator - briefing plan-executor subagents (executors and landers) in their own worktrees, answering their escalations with researched decisions and running the user gates. Use when the user asks to execute or continue a plan. --- # Plan execution (orchestrator) -The main session orchestrates; it doesn't implement work items itself. -Executors are `plan-executor` subagents: their side of the protocol -(brief contents, write-set rules, escalation triggers, report format) is -in `.claude/agents/plan-executor.md` and isn't repeated here. Plan layout +The main session orchestrates; it doesn't implement work items, review +diffs, run verification lanes or `/code-review`, merge or trigger +releases itself. It keeps the queue, writes briefs, researches and +decides escalations, raises user gates and keeps the plan's Status and +Decision log current. Executors and landers are `plan-executor` +subagents on the faster model by default: their side of the protocol +(brief contents, write-set rules, escalation triggers, lander mode, +report formats) is in `.claude/agents/plan-executor.md` and isn't +repeated here. Plan layout is in `docs/plans/README.md`. ## Before the first item @@ -23,9 +28,10 @@ is in `docs/plans/README.md`. ## The queue -- Keep the plan's target number of executors running. Waves define - dependencies, not batches: whenever an executor reports, start the - next **ready** item (dependencies merged and verified, write set +- Keep the plan's target number of agents (executors and landers + together) running. Waves define + dependencies, not batches: whenever an agent reports, start the + next **ready** item (dependencies landed, write set disjoint from every running item), critical path first, then the item that unblocks the most. - Start each with the `Agent` tool, `subagent_type: "plan-executor"`, @@ -39,12 +45,22 @@ is in `docs/plans/README.md`. ## When an executor reports -**`done`:** read the diff, not just the summary. Every changed file is -inside the write set; `CHECKS` shows real output with new files staged. -Merge into the integration branch, run the verification lane, update the -plan's Status, then start the next ready item. +**`done`:** don't read the diff yourself. Start a lander for the item +(or for the repo's wave PR once its items are done): a `plan-executor` +run on the faster model unless the plan names another, briefed per "Lander mode" in +`.claude/agents/plan-executor.md` with the executor's report, the +original write set and "Done when", the integration branch and PR, the +verification lane's commands, and the standing approvals. At most one +lander per repo at a time. Then start the next ready item. -**`escalate`:** research before deciding: +**A lander's report:** `done` means landed: PR, merge commit (where it may +merge), CI, review rounds with dispositions, the lane tail and NOTES. +Record it in the plan's Status and start whatever it unblocks. A red lane +is the lander's to handle: it reverts the merge and resumes the owning +executor with the failing output. Treat what it escalates like an +executor's escalation below, with its evidence in hand. + +**`escalate`** (executor or lander): research before deciding: 1. context7 for the library or framework involved; 2. WebSearch/WebFetch for the exact error text, the tool's issue tracker, @@ -73,27 +89,35 @@ Then: ## Serialized lanes -Run these yourself, one at a time, never in parallel executors: +Landers run these, one at a time, never in parallel with each other where +they share resources; you sequence them through the queue: -- **Verification:** after each merge, the repo's full suite including - integration and e2e (they share the devcontainer's services and ports). - On red: revert that merge on the integration branch, send the failing - output to the owning executor as an escalation-style message, and keep - the queue moving. -- **Releases**, in the plan's dependency order, each behind its gate. -- **Measurements**, only in quiet windows with no executors running. +- **Verification:** after each merge into a repo's integration branch, + the repo's full suite including integration and e2e. Only one lander at + a time runs suites that share the devcontainer's services or fixed + ports, across all repos. On red the lander reverts that merge, sends the + failing output to the owning executor and keeps going; it escalates + only when the executor can't fix it inside its write set. +- **Releases**, in the plan's dependency order, by a lander, only where + the standing approvals allow; every other release is a gate you raise. +- **Measurements**, only in quiet windows with no other agents running. ## Gates and the user +- Landers never merge a PR the user merges and never act outside the + standing approvals; they escalate and you raise the gate. - Batch same-kind gates in a wave (PR merges, releases) into one request. Batch non-blocking questions too. -- Show evidence with every gate: check output, CI links, release runs. -- After each merged batch, add a dated progress bullet to the plan's +- Show evidence with every gate: it comes from lander reports (check and + lane output, CI links, review dispositions, release runs). If a report + lacks it, send the lander back for it rather than checking yourself. +- After each landed batch, add a dated progress bullet to the plan's Status, so a fresh session can resume from the plan alone. ## Close-out -Final verification lane over every repo the plan touched; fold what's +A final verification lane over every repo the plan touched, run by a +lander; fold what's worth keeping into ADRs and docs; ask for the final gate; then delete the plan (and its runbook) per `docs/plans/README.md`, and give the user a commit message. diff --git a/.dockerignore b/.dockerignore index 2a55e1b..177001b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,8 @@ # Build context excludes for the runtime image (top-level Dockerfile). The # devcontainer image has its own: .devcontainer/Dockerfile.dockerignore. +**/node_modules +coverage +target .git .github .devcontainer diff --git a/.github/template-sync-manifest.yml b/.github/template-sync-manifest.yml index 9a4c9ed..6e634f8 100644 --- a/.github/template-sync-manifest.yml +++ b/.github/template-sync-manifest.yml @@ -93,6 +93,7 @@ replace: - scripts/check-dockerfile-versions.sh - scripts/image-scan.sh - scripts/skopeo.sh + - scripts/test-image-scripts.sh # template-react's own layer on top of docker's: the runtime image (an # nginx-served SPA) and its assets, the dev-service compose additions, the diff --git a/.github/template-sync-state.json b/.github/template-sync-state.json index ce2bf42..3edc349 100644 --- a/.github/template-sync-state.json +++ b/.github/template-sync-state.json @@ -1,4 +1,4 @@ { "template_repo": "craftainer/template-docker", - "synced_tag": "v1.0.0" + "synced_tag": "v1.2.0" } diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 0760f28..885b702 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -6,15 +6,6 @@ [`devcontainers/ci`](https://github.com/devcontainers/ci)), under a per-run `COMPOSE_PROJECT_NAME` that a final step tears down. Runs as a two-leg `amd64`/`arm64` matrix (see "Architecture matrix" below). -- `smoke.yml` — builds the `runner` stage of the root `Dockerfile` via - the root `compose.yml`, starts it against the real Postgres, RustFS, - Redis, and Keycloak backing services, and confirms `/health/live` and - `/health/ready` both return 200 -- on push, on pull requests, and on - demand. Runs as the same two-leg `amd64`/`arm64` matrix as - `checks.yml` — this is the check that would actually catch an - arch-specific runtime break (e.g. a C-extension dependency missing an - arm64 wheel), since `checks.yml`'s test run uses `MODE=mock` - in-process fakes for some paths rather than the built image. - `release.yml` — manually triggered. Takes a release channel (`alpha`/`beta`/`rc`/`full`) and a SemVer 2 bump (`major`/`minor`/`patch`/`none`), computes the next tag via @@ -48,7 +39,7 @@ e.g. to point at self-hosted runners. ## Architecture matrix -`checks.yml`, `smoke.yml`, and `release.yml`'s `build` job each run as a +`checks.yml` and `release.yml`'s `build` job each run as a two-leg matrix over `amd64`/`arm64`, both legs building/testing natively — `ubuntu-24.04-arm` is a real (non-emulated) GitHub-hosted Linux arm64 runner, free on this public repo and billed as ordinary Actions minutes diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc177e3..74b3c30 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -143,14 +143,16 @@ repos: name: check-dockerfile-versions (ARG defaults match across Dockerfiles) entry: bash scripts/check-dockerfile-versions.sh DEBIAN_VERSION NODE_VERSION language: system - files: ^(\.devcontainer/)?Dockerfile$ + # Also when the check itself, or this entry (the ARG list), changes. + files: ^((\.devcontainer/)?Dockerfile|scripts/check-dockerfile-versions\.sh|\.pre-commit-config\.yaml)$ pass_filenames: false stages: [pre-commit, manual] - # Vulnerability scan of the built image tar(s) in dist/ with Trivy - # (ADR 0001; the Trivy pin and why are in scripts/image-scan.sh). - # Manual stage only: it needs `make build` to have produced dist/*.tar - # and network for the vulnerability DB. Skips cleanly with no tar. + # Vulnerability scan of the built image tar for the current + # RELEASE_VERSION/RELEASE_ARCH in dist/ with Trivy (ADR 0001; the Trivy + # pin and why are in scripts/image-scan.sh). Manual stage only: it needs + # `make build` to have produced the tar and network for the + # vulnerability DB. Skips cleanly with no tar. - id: image-scan name: image-scan (Trivy, HIGH/CRITICAL with a fix) entry: bash scripts/image-scan.sh @@ -158,3 +160,14 @@ repos: pass_filenames: false always_run: true stages: [manual] + + # Offline tests of the scripts and Makefile logic above (repo + # derivation, version-check edge cases, smoke-test failure on a missing + # tar); needs no Docker. + - id: image-scripts-test + name: image-scripts-test (Makefile and scripts/ behaviour) + entry: bash scripts/test-image-scripts.sh + language: system + files: ^(Makefile|scripts/(check-dockerfile-versions|image-scan|image-smoke|skopeo|test-image-scripts)\.sh)$ + pass_filenames: false + stages: [pre-commit, manual] diff --git a/CLAUDE.md b/CLAUDE.md index 516d260..8c13110 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,9 +92,11 @@ Practices below are distilled from Anthropic's own Claude Code guidance - **Execute multi-item work as an orchestrator.** When executing a `docs/plans/` plan, or any task that splits into several independent items (closing multiple test coverage gaps, for example), the main - session orchestrates rather than implementing, through - `plan-executor` subagents (`.claude/agents/plan-executor.md`). Use the - `plan-execution` skill (`.claude/skills/plan-execution/`) for how. + session only orchestrates, deciding and raising user gates; executors + and landers (`plan-executor` subagents, + `.claude/agents/plan-executor.md`) implement, review, merge, verify + and release. Use the `plan-execution` skill + (`.claude/skills/plan-execution/`) for how. - **Flag devcontainer changes as needing a rebuild.** Editing anything under `.devcontainer/` (compose files, `stack/`, Dockerfiles) doesn't take effect in the running container — tell the user a rebuild is diff --git a/Makefile b/Makefile index c648ce3..3632a62 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,16 @@ # # What gets published is the tar `build` wrote to dist/ -- the one `sbom` # described and `image-smoke` ran -- copied to the registry with skopeo -# rather than rebuilt, so the registry digest is the tested artifact's. +# rather than rebuilt, so the registry digest is the tested artifact's. The +# copy is `--all`: the tar's whole index (the image for RELEASE_ARCH plus any +# buildx attestation manifests: provenance and SBOM) is pushed, whatever the +# host's arch, so a non-native RELEASE_ARCH works and attestations survive. # `created` is the commit time, so rebuilding the same commit yields the same # labels; layer contents are not guaranteed bit-for-bit reproducible. -include image.mk -.PHONY: release-arches build sbom release-assets publish release-check finalize image-smoke check-arch +.PHONY: repo-check release-arches build sbom release-assets publish release-check finalize image-smoke check-arch .DEFAULT_GOAL := release-arches DOCKERFILE ?= Dockerfile @@ -39,9 +42,23 @@ SKOPEO_IMAGE := quay.io/skopeo/stable:v1.22.3-immutable@sha256:c0ee1f4edca5c01cb # owner/repo: GITHUB_REPOSITORY in CI; inside the devcontainer (where # release-check runs) it is unset, so derive it from the origin remote -# (ssh, scp-like and https forms, with or without credentials and .git); -# the directory name is the last resort. -REPO := $(or $(GITHUB_REPOSITORY),$(shell git remote get-url origin 2>/dev/null | sed -E 's|^[a-z+]+://([^@/]*@)?([^/:]+):[0-9]+/|\2/|; s|^[a-z+]+://||; s|^[^@/]*@||; s|^[^/:]+[:/]||; s|/$$||; s|\.git$$||'),$(notdir $(CURDIR))) +# (ssh://[user@]host[:PORT]/o/r, scp-like and https forms, with or without +# credentials and .git; scripts/test-image-scripts.sh checks them). If git +# fails (no origin, "dubious ownership", ...) there is no guess: REPO stays +# empty, git's own error is kept in REPO_ERROR, and every target that needs +# REPO (via repo-check) prints it to stderr and fails. +ifeq ($(GITHUB_REPOSITORY),) +GIT_REMOTE := $(shell url=$$(git remote get-url origin 2>&1) && { printf 'ok '; printf '%s' "$$url" | sed -E 's|^[A-Za-z][A-Za-z0-9+.-]*://([^@/]*@)?([^/:]+):[0-9]+/|\2/|; s|^[A-Za-z][A-Za-z0-9+.-]*://||; s|^[^@/]*@||; s|^[^/:]+[:/]||; s|/$$||; s|\.git$$||'; } || printf 'err %s' "$$url") +ifeq ($(firstword $(GIT_REMOTE)),ok) +REPO := $(word 2,$(GIT_REMOTE)) +else +REPO := +REPO_ERROR := make: cannot derive the repository from the origin remote: $(wordlist 2,$(words $(GIT_REMOTE)),$(GIT_REMOTE)) (set GITHUB_REPOSITORY=owner/repo to override) +export REPO_ERROR +endif +else +REPO := $(GITHUB_REPOSITORY) +endif OWNER := $(firstword $(subst /, ,$(REPO))) IMAGE := $(notdir $(REPO)) ASSET_PREFIX := dist/$(IMAGE)-$(RELEASE_VERSION) @@ -54,7 +71,7 @@ OCI_NAME = $(shell printf '%s' '$(or $(OCI_IMAGE_NAME),$(REPO))' | tr '[:upper:] export OCI_DESCRIPTION export IMAGE_SMOKE_RUN_ARGS IMAGE_SMOKE_PORT IMAGE_SMOKE_PATH IMAGE_SMOKE_TIMEOUT RELEASE_VERSION export IMAGE_TAR_PREFIX = $(ASSET_PREFIX) -export SKOPEO_IMAGE +export SKOPEO_IMAGE ARCHES # Only set in CI, by crazy-max/ghaction-github-runtime in release.yml. ifdef ACTIONS_RUNTIME_TOKEN @@ -82,6 +99,12 @@ LABELS += $(EXTRA_LABELS) BUILD = docker buildx build -f $(DOCKERFILE) --target $(TARGET) \ --platform linux/$(RELEASE_ARCH) $(BUILD_ARGS) $(LABELS) $(CACHE) +REPO_GUARD = test -n "$(REPO)" || { printf '%s\n' "$$REPO_ERROR" >&2; exit 1; } + +# Prerequisite of every target whose output names the repo. +repo-check: + @$(REPO_GUARD) + check-arch: @test -n "$(RELEASE_ARCH)" || { echo "RELEASE_ARCH is not set." >&2; exit 1; } @@ -89,38 +112,41 @@ release-arches: @test -n "$(strip $(ARCHES))" || { echo "ARCHES is empty; list at least one arch in image.mk." >&2; exit 1; } @printf '[%s]\n' "$$(printf '"%s",' $(ARCHES) | sed 's/,$$//')" -build: check-arch +build: check-arch repo-check mkdir -p dist $(BUILD) --output type=oci,dest=$(ASSET).tar . -sbom: check-arch +sbom: check-arch repo-check docker run --rm -v "$(CURDIR)/dist:/dist" anchore/syft:$(SYFT_VERSION) \ oci-archive:/$(ASSET).tar -o spdx-json=/$(ASSET).spdx.json # Each arch is smoke-tested here, natively on its own runner, before publish. -release-assets: check-arch image-smoke +release-assets: check-arch repo-check image-smoke test -f $(ASSET).tar && test -f $(ASSET).spdx.json -# Copies the tested tar from dist/ to the registry (skopeo copy -# oci-archive:... docker://...), records the pushed manifest digest in +# Copies the tested tar from dist/ to the registry (skopeo copy --all +# oci-archive:... docker://...), records the pushed index digest in # dist/.digest for finalize, and logs in through a throwaway authfile -# so the password never appears in a command line. +# so the password never appears in a command line. Without --all skopeo +# copies one image chosen by the host's platform, which can fail or pick the +# wrong image for a non-native RELEASE_ARCH, and it drops the attestations. publish: check-arch ifeq ($(OCI_REGISTRY),) @echo "OCI_REGISTRY not set -- skipping publish." else + @$(REPO_GUARD) @test -f $(ASSET).tar || { echo "$(ASSET).tar not found; run 'make build' first." >&2; exit 1; } - @auth="$$(mktemp -d)" && trap 'rm -rf "$$auth"' EXIT && \ + @auth="$$(mktemp -d)" && chmod 0777 "$$auth" && trap 'rm -rf "$$auth"' EXIT && \ printf '%s' "$$OCI_REGISTRY_PASSWORD" | bash scripts/skopeo.sh -i -v "$$auth:/auth" -- \ login --authfile /auth/auth.json -u "$$OCI_REGISTRY_USERNAME" --password-stdin "$(OCI_REGISTRY)" && \ bash scripts/skopeo.sh -v "$$auth:/auth:ro" -- \ - copy --authfile /auth/auth.json --digestfile /$(ASSET).digest \ + copy --all --authfile /auth/auth.json --digestfile /$(ASSET).digest \ oci-archive:/$(ASSET).tar docker://$(OCI_REGISTRY)/$(OCI_NAME):$(RELEASE_VERSION)-$(RELEASE_ARCH) endif # Smoke-tests the tar in dist/ for RELEASE_ARCH (default: the host's arch). # image.mk's IMAGE_SMOKE_* values reach the script through the environment. -image-smoke: +image-smoke: repo-check bash scripts/image-smoke.sh # No recipe by design: image.mk adds steps as prerequisites, so Make never @@ -128,18 +154,25 @@ image-smoke: # inside the devcontainer, with the instance's backing services up. release-check: image-smoke -# One manifest list over the per-arch digests publish recorded, so it -# references exactly the images that were tested and pushed. Logs in with a -# throwaway DOCKER_CONFIG so the password isn't left in ~/.docker/config.json. +# One manifest list over the per-arch digests publish recorded (each an +# index of image + attestations; imagetools create merges their manifests), +# so it references exactly the images that were tested and pushed. Logs in +# with a throwaway DOCKER_CONFIG so the password isn't left in +# ~/.docker/config.json. The Docker CLI looks for plugins in +# $DOCKER_CONFIG/cli-plugins, so a user-installed buildx would vanish with +# the swap: the user's cli-plugins directory is linked into the throwaway one. finalize: ifeq ($(OCI_REGISTRY),) @echo "OCI_REGISTRY not set -- no manifest list to create." else - @export DOCKER_CONFIG="$$(mktemp -d)" && trap 'rm -rf "$$DOCKER_CONFIG"' EXIT && \ + @$(REPO_GUARD) + @orig="$${DOCKER_CONFIG:-$$HOME/.docker}" && export DOCKER_CONFIG="$$(mktemp -d)" && trap 'rm -rf "$$DOCKER_CONFIG"' EXIT && \ + if [ -d "$$orig/cli-plugins" ]; then ln -s "$$orig/cli-plugins" "$$DOCKER_CONFIG/cli-plugins"; fi && \ printf '%s' "$$OCI_REGISTRY_PASSWORD" | docker login "$(OCI_REGISTRY)" -u "$$OCI_REGISTRY_USERNAME" --password-stdin && \ srcs="" && for a in $(ARCHES); do \ d="$$(cat dist/$(IMAGE)-$(RELEASE_VERSION)-$$a.digest)" || exit 1; \ srcs="$$srcs $(OCI_REGISTRY)/$(OCI_NAME)@$$d"; \ done && \ - docker buildx imagetools create --tag $(OCI_REGISTRY)/$(OCI_NAME):$(RELEASE_VERSION) $$srcs + docker buildx imagetools create --tag $(OCI_REGISTRY)/$(OCI_NAME):$(RELEASE_VERSION) $$srcs && \ + rm -f dist/$(IMAGE)-$(RELEASE_VERSION)-*.digest endif diff --git a/docs/TEMPLATE.md b/docs/TEMPLATE.md index 6e3e5f4..a209136 100644 --- a/docs/TEMPLATE.md +++ b/docs/TEMPLATE.md @@ -189,11 +189,12 @@ these files over base: | `Makefile` | `replace` | the image implementation of the release contract above | | `image.mk` | `ignore` | the instance's settings for the Makefile, every variable commented | | `Dockerfile` | `ignore` | skeleton to replace: `builder` and `runner` stages, `ARG DEBIAN_VERSION`, `ARG APP_UID`, SSL ARGs, `APP_VERSION`/`APP_COMMIT` | -| `.dockerignore` | `merge` | baseline build-context excludes; the instance appends its own | +| `.dockerignore` | `merge` | baseline build-context excludes for the runtime image (including `node_modules`, `coverage`, `target` and `dist`); the instance appends its own | | `scripts/image-smoke.sh` | `replace` | loads the image tar from `dist/`, runs it and polls a URL | -| `scripts/runner-setup.sh` | `replace` | creates the group-0 runtime user and its writable directories for the `runner` stage | +| `scripts/runner-setup.sh` | `replace` | optional: creates the group-0 runtime user and its writable directories for the `runner` stage | | `scripts/image-scan.sh` | `replace` | the Trivy scan the `image-scan` hook runs | -| `scripts/runner-setup.d/*.sh` | `ignore` | the instance's runner-stage hooks | +| `scripts/test-image-scripts.sh` | `replace` | offline tests of the Makefile and `scripts/` logic, run by the `image-scripts-test` hook | +| `scripts/runner-setup.d/*.sh` | `ignore` | optional: the instance's runner-stage hooks | | `scripts/check-dockerfile-versions.sh` | `replace` | the version-ARG consistency check | ### `image.mk` variables @@ -217,20 +218,34 @@ defaults) and every variable is `?=`, so set them in `image.mk`: digest) are pinned in the Makefile for Renovate; don't override them. The image name comes from `GITHUB_REPOSITORY` (the repository name), never a hardcoded one. Inside the devcontainer, where `release-check` runs and -`GITHUB_REPOSITORY` is unset, it is derived from the `origin` remote (ssh -or https form), and from the directory name as a last resort; +`GITHUB_REPOSITORY` is unset, it is derived from the `origin` remote +(`ssh://[user@]host[:PORT]/owner/repo`, scp-like or https form; tested by +`scripts/test-image-scripts.sh`). If git fails (no `origin`, "dubious +ownership"), there is no guess: `build`, `sbom`, `release-assets`, `publish` +and `finalize` print git's error to stderr and fail, and +`GITHUB_REPOSITORY=owner/repo` overrides; `image-smoke` also accepts the single `dist/*--.tar` when the expected name is absent. `OCI_IMAGE_NAME` overrides the registry path. `build`, `sbom`, `release-assets` and `publish` need `RELEASE_ARCH` and fail through the `check-arch` prerequisite when it is empty; `release-arches`, `release-check`/`image-smoke` (host arch when unset) and `finalize` don't. `publish` copies the tar from `dist/` that was SBOM'd -and smoke-tested to the registry with skopeo (`oci-archive:` to -`docker://`, Apache-2.0, pinned image; `crane push` only reads +and smoke-tested to the registry with skopeo (`copy --all`, `oci-archive:` +to `docker://`, Apache-2.0, pinned image; `crane push` only reads docker-format tarballs, and `build` writes an OCI layout), so the pushed -digest is the tested one. It records the digest in -`dist/--.digest`, and `finalize` builds the manifest -list from those digests. Registry login uses a throwaway authfile fed the +digest is the tested one. `--all` pushes the tar's whole index as +`-`: the image for `RELEASE_ARCH` plus the buildx attestation +manifests (provenance, SBOM) when the build wrote any. Without it skopeo +picks one image by the *host's* platform, which can fail or pick the wrong +image when `RELEASE_ARCH` isn't native, and it drops the attestations. It +records that index's digest in `dist/--.digest` (which +`finalize` deletes after use, so release.yml never attaches digest files to +the GitHub release), and +`finalize` builds the manifest list from those digests (`imagetools create` +merges the per-arch indexes, so the attestations carry over). `finalize` +logs in with a throwaway `DOCKER_CONFIG`, into which it links the user's +`cli-plugins` directory (Docker looks up plugins in +`$DOCKER_CONFIG/cli-plugins`), so a user-installed buildx is still found. Registry login uses a throwaway authfile fed the password on stdin. The labels are listed in `.github/workflows/README.md`'s "Image labels". Registry auth stays the opt-in `OCI_REGISTRY*` secrets contract. @@ -254,25 +269,38 @@ image in the OCI-layout tar with the pinned skopeo (through `scripts/skopeo.sh`, which `publish` uses too; `--override-arch`, to a docker-archive under `dist/`) and `docker load`s that, because `docker load` can't read a buildx OCI tar on the classic image store, -with or without attestations. It reads `IMAGE_TAR_PREFIX` and -`SKOPEO_IMAGE` (set by the Makefile), the arch -from `RELEASE_ARCH` (else the host's; if `RELEASE_ARCH` is unset and -there is no tar for the host arch, it prints a notice and exits 0, since -the per-arch run already covered that arch), maps a random host port to -`IMAGE_SMOKE_PORT`, prints the container logs on failure and always +with or without attestations. It reads `IMAGE_TAR_PREFIX`, +`SKOPEO_IMAGE` and `ARCHES` (set by the Makefile), the arch from +`RELEASE_ARCH` (else the host's). A missing tar for that arch fails, so a +run never passes without having tested an image; the one exception is +`RELEASE_ARCH` unset with a host arch outside `ARCHES` (the instance +doesn't build it, and its own build leg tested each arch it does). It maps +a random host port to `IMAGE_SMOKE_PORT`, prints the container logs on failure and always removes the container and the private `image-smoke-*` tag it created (never an image that existed before). ### Runner setup hook +Optional. It serves a runtime image that needs the group-0 user and +writable directories; an instance whose `runner` stage doesn't call it +(e.g. `template-react`'s nginx runner, whose base image already has its own +non-root user) simply never runs it. Both files stay in the tree, since +they are template-owned, and nothing checks or requires them. + `scripts/runner-setup.sh` creates the user (UID `APP_UID`, group 0), makes its home and `/app/data` group-writable, and leaves `/app` root-owned, so the image also runs under an arbitrary UID (OpenShift's restricted SCC). Run it **before** copying app code, then copy the code after it with -`COPY --chown=:0 --chmod=u=rX,g=rX,o=` (0550 directories, 0440 -files): only data and cache directories are writable, the runtime user +`COPY --chown=:0` of content whose modes are already 0550 +directories and 0440 files: only data and cache directories are writable, the runtime user can't modify what it runs, and no recursive `chgrp`/`chmod` duplicates -the files into another layer. It then runs every +the files into another layer. Set those modes numerically, per kind +(`find . -type d -exec chmod 0550 {} +`, `find . -type f -exec chmod 0440 +{} +`, e.g. in the builder stage, or `COPY --chmod=0550`/`0440` for sources +that are all one kind), not with the symbolic `--chmod=u=rX,g=rX,o=`: that +form left files unreadable to the app user on GitHub's stock-runner Docker +(template-fastapi's CI smoke, even with `# syntax=docker/dockerfile:1.28`), +though it worked on a current local daemon. It then runs every `scripts/runner-setup.d/*.sh` in glob order as `bash NN-name.sh APP_UID`, still as root and in the same `RUN` layer. The instance puts extra packages and writable directories there instead of editing the script. @@ -281,9 +309,12 @@ packages and writable directories there instead of editing the script. The `check-dockerfile-versions` hook runs `scripts/check-dockerfile-versions.sh DEBIAN_VERSION` whenever either -Dockerfile changes: each named ARG must have the same default in +Dockerfile, the script or `.pre-commit-config.yaml` (where the ARG list +lives) changes: each named ARG must have the same default in `.devcontainer/Dockerfile` and `Dockerfile` (the runtime must match the -toolchain the devcontainer tests against). An instance appends the ARG +toolchain the devcontainer tests against). An ARG may be declared several +times in one file (a bare `ARG NAME` re-declares it inside a stage); bare +ones are ignored and every default given must agree. An instance appends the ARG names it keeps in both files (e.g. `RUST_VERSION`) to the hook's `entry:` in its `.pre-commit-config.yaml`; ARGs not listed aren't checked. @@ -295,10 +326,17 @@ in its `.pre-commit-config.yaml`; ARGs not listed aren't checked. install an unpinned apt package adds a per-line `# hadolint ignore=DL3008` with a reason (as the skeleton `Dockerfile` does: Debian drops superseded versions, so pins break rebuilds). +- `image-scripts-test` runs `scripts/test-image-scripts.sh`: offline tests + of the Makefile's repo derivation, the version check, the smoke test's + failure on a missing tar and `skopeo.sh`'s argument handling. It needs no + Docker. - `image-scan` runs only at the `manual` stage, because it needs - `dist/*.tar` from `make build` and network access for the vulnerability - database. With no tar it skips. Otherwise `scripts/image-scan.sh` runs a - pinned Trivy image on each tar and fails on HIGH or CRITICAL + the tar from `make build` and network access for the vulnerability + database. It scans only `dist/*--.tar` (arch from + `RELEASE_ARCH`, else the host's; without `RELEASE_VERSION`, the newest + `dist/*-.tar`), never stale tars of other versions. With no such tar + it skips. Otherwise `scripts/image-scan.sh` runs a + pinned Trivy image on that tar and fails on HIGH or CRITICAL findings that have a fix (`--severity HIGH,CRITICAL --ignore-unfixed --exit-code 1`). Trivy can't read the OCI-layout tar `make build` writes, so the script unpacks it and scans the directory, selecting diff --git a/docs/plans/README.md b/docs/plans/README.md index e2311a1..7b1e516 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -11,7 +11,8 @@ made. A plan can reference an ADR it depends on or that it prompts. ## Execution section A plan with more than one independent work item is executed by an -orchestrating session through `plan-executor` subagents (see +orchestrating session that only decides and raises gates, through +`plan-executor` subagents as executors and landers (see `../../CLAUDE.md`, "Execute multi-item work as an orchestrator", and the `plan-execution` skill; plans themselves are written with the `plan-creation` skill, both in `../../.claude/skills/`). It @@ -33,11 +34,17 @@ each item straight into a brief: plan, so two items never pick the same one. - **Serialized lanes:** anything sharing devcontainer services (integration and e2e suites, load tests and benchmarks) runs one at a - time, outside the parallel waves. Measurements also need a quiet - window, with no parallel agents competing for CPU. + time, outside the parallel waves, by a lander (never the orchestrator). + Measurements also need a quiet window, with no parallel agents + competing for CPU. +- **Standing approvals:** which merges and releases landers may do + without asking (for example, template releases and merges in repos the + plan creates); a PR the user merges is never among them. - **User gates:** a table of every outward-facing or hard-to-reverse step (creating repos, pushing, opening PRs, publishing, tagging) that - waits for the user's explicit yes. + waits for the user's explicit yes, except what the plan's kickoff + gate (G0) or its standing approvals grant landers (pushing and + opening the wave PRs). - **Decision log:** a dated table of settled decisions. Executors treat it as final, and the orchestrator appends to it as it resolves escalations. diff --git a/scripts/README.md b/scripts/README.md index 5c5530f..42b3d51 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -11,18 +11,22 @@ - `develop.d/`, `post-create.d/`, `runner-setup.d/` — instance hook directories, empty apart from their own `README.md`; see `docs/TEMPLATE.md`'s "Instance extension points". -- `runner-setup.sh` — the top-level `Dockerfile`'s `runner` stage setup: - group-0 runtime user for arbitrary-UID platforms (run before copying +- `runner-setup.sh` — optional; the top-level `Dockerfile`'s `runner` stage + setup: group-0 runtime user for arbitrary-UID platforms (run before copying app code, which is then copied read-only), then every - `runner-setup.d/*.sh`. + `runner-setup.d/*.sh`. An instance whose runner image doesn't call it + (an nginx runner, say) ignores it and `runner-setup.d/`. - `image-scan.sh` — the manual-stage `image-scan` hook: Trivy over the - tars in `dist/`. + tar for the current `RELEASE_VERSION`/arch in `dist/`. +- `test-image-scripts.sh` — the `image-scripts-test` hook: offline tests of + the Makefile's repo derivation and these scripts. - `image-smoke.sh` — `make image-smoke` (a `release-check` prerequisite): converts the arch's image in the release OCI tar from `dist/` to a docker-archive with the pinned skopeo (`docker load` can't read a buildx OCI tar on the classic image store), loads and runs it, and polls - `IMAGE_SMOKE_PATH` until 2xx. Needs `SKOPEO_IMAGE`, which the Makefile - exports; usage in the script's header. + `IMAGE_SMOKE_PATH` until 2xx. Fails when there is no tar for the arch under + test. Needs `SKOPEO_IMAGE`, which the Makefile exports; usage in the + script's header. - `skopeo.sh` — runs the pinned skopeo image as the caller's user with `dist/` mounted at `/dist`; used by `make publish` and `image-smoke.sh`. - `check-dockerfile-versions.sh ARG...` — fails when a named `ARG` default diff --git a/scripts/check-dockerfile-versions.sh b/scripts/check-dockerfile-versions.sh index 1ff9139..61f6cbc 100755 --- a/scripts/check-dockerfile-versions.sh +++ b/scripts/check-dockerfile-versions.sh @@ -4,9 +4,10 @@ # Each named ARG must be declared (`ARG NAME=value`) with the same default # in both the base-owned .devcontainer/Dockerfile (develop stage) and the # top-level Dockerfile (builder/runner stages): the runtime image must match -# the toolchain the devcontainer tests against. Values are compared after stripping trailing comments, whitespace and -# surrounding quotes. Exits 1 if one differs, is missing from either file or -# is declared without a default. ARGs not named are not checked. +# the toolchain the devcontainer tests against. Values are compared after +# stripping trailing comments, whitespace and surrounding quotes. Exits 1 if +# one differs, is missing from either file, is declared without a default or +# has defaults that disagree within a file. ARGs not named are not checked. set -euo pipefail if [[ $# -eq 0 ]]; then @@ -16,15 +17,28 @@ fi # Prints the normalized default of `ARG name=...` in file $2, or exits: # 0 with the value, 3 when the ARG is not declared, 4 when it is declared -# without a default. Normalizing strips a trailing comment, surrounding -# whitespace and one pair of surrounding quotes. +# without a default, 5 when its defaults within the file disagree. A file +# may declare an ARG several times (a bare `ARG name` re-declares it inside +# a stage, and a stage may repeat the default): bare re-declarations are +# ignored and every default must agree. Normalizing strips a trailing +# comment, surrounding whitespace and one pair of surrounding quotes. default_of() { - local line - line="$(grep -m1 -E "^ARG[[:space:]]+$1([=[:space:]]|\$)" "$2")" || return 3 - [[ "$line" == *=* ]] || return 4 - line="${line#*=}" - line="$(sed -E 's/[[:space:]]+#.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/' <<<"$line")" - printf '%s\n' "$line" + local line value found=0 first="" have=0 + while IFS= read -r line; do + found=1 + [[ "$line" == *=* ]] || continue + line="${line#*=}" + value="$(sed -E 's/[[:space:]]+#.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/' <<<"$line")" + if [[ $have -eq 0 ]]; then + first="$value" + have=1 + elif [[ "$value" != "$first" ]]; then + return 5 + fi + done < <(grep -E "^ARG[[:space:]]+$1([=[:space:]]|\$)" "$2" || true) + [[ $found -eq 1 ]] || return 3 + [[ $have -eq 1 ]] || return 4 + printf '%s\n' "$first" } status=0 @@ -37,7 +51,8 @@ for arg in "$@"; do case $rc in 0) values+=("$v") ;; 3) echo "${arg} missing: not declared in $f" >&2; ok=0 ;; - *) echo "${arg} declared without a default in $f" >&2; ok=0 ;; + 4) echo "${arg} declared without a default in $f" >&2; ok=0 ;; + *) echo "${arg} declared with conflicting defaults in $f" >&2; ok=0 ;; esac done if [[ $ok -eq 1 && "${values[0]}" != "${values[1]}" ]]; then diff --git a/scripts/develop.d/README.md b/scripts/develop.d/README.md index facf126..15865b8 100644 --- a/scripts/develop.d/README.md +++ b/scripts/develop.d/README.md @@ -1,8 +1,11 @@ # scripts/develop.d/ Instance-owned hook: extra devcontainer tooling an instance layers on top -of `../develop.sh`. This directory ships only this file; an instance adds -its scripts here (see `docs/TEMPLATE.md`'s "Instance extension points"). +of `../develop.sh`. This directory ships no scripts in base; a mid-layer template or an instance +adds `NN-name.sh` scripts here (see `docs/TEMPLATE.md`'s "Instance extension +points"). A script a template ships is `replace` tier in its sync manifest, so +a sync overwrites it; an instance's own script is listed in +`.github/template-sync-manifest.local.yml` so a sync leaves it alone. - Every `*.sh` here runs as root, in lexical filename order (use an `NN-` prefix, e.g. `10-apt.sh`), after `scripts/develop.sh`, during the diff --git a/scripts/image-scan.sh b/scripts/image-scan.sh index 6353ded..1a7bf49 100755 --- a/scripts/image-scan.sh +++ b/scripts/image-scan.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash -# Scans every image tar `make build` left in dist/ for HIGH/CRITICAL -# vulnerabilities that have a fix, with Trivy. Skips when dist/ has no tar. +# Scans the image tar `make build` left in dist/ for the current release +# version and arch for HIGH/CRITICAL vulnerabilities that have a fix, with +# Trivy. Skips when there is none. Which tar: RELEASE_ARCH (default: the +# host's) selects the arch; RELEASE_VERSION selects dist/*--.tar, +# and when unset the newest dist/*-.tar is used, so tars of older +# versions or other arches left in dist/ are never scanned. # Run by the manual-stage `image-scan` pre-commit hook; needs Docker and # network access (the vulnerability DB is cached in the trivy-cache volume). # @@ -17,10 +21,25 @@ set -euo pipefail trivy=aquasec/trivy:0.76.0@sha256:2416d1f74045b57274df1a340eee6b510cacd73c66a6ccb8da092b117eb7d222 +case "$(uname -m)" in + aarch64 | arm64) host_arch=arm64 ;; + *) host_arch=amd64 ;; +esac +arch="${RELEASE_ARCH:-$host_arch}" + shopt -s nullglob -tars=(dist/*.tar) +tars=() +if [[ -n "${RELEASE_VERSION:-}" ]]; then + tars=(dist/*-"$RELEASE_VERSION-$arch".tar) +else + newest="" + for t in dist/*-"$arch".tar; do + if [[ -z "$newest" || "$t" -nt "$newest" ]]; then newest="$t"; fi + done + if [[ -n "$newest" ]]; then tars=("$newest"); fi +fi if [[ ${#tars[@]} -eq 0 ]]; then - echo "image-scan: no image tar in dist/, skipping" + echo "image-scan: no linux/$arch image tar in dist/${RELEASE_VERSION:+ for $RELEASE_VERSION}, skipping" exit 0 fi diff --git a/scripts/image-smoke.sh b/scripts/image-smoke.sh index 8b4d837..a67110c 100755 --- a/scripts/image-smoke.sh +++ b/scripts/image-smoke.sh @@ -11,9 +11,13 @@ # RELEASE_VERSION if that tar is missing (the image name may differ # when the repository name can't be derived), the one # dist/*-$RELEASE_VERSION-.tar is used instead -# RELEASE_ARCH arch to test; default: the host's. Unset and no tar -# for that arch: print a notice and exit 0 (the -# per-arch run in the build job already covered it) +# RELEASE_ARCH arch to test; default: the host's. A missing tar +# for that arch is an error, so a run never passes +# without having tested an image. The one exception: +# RELEASE_ARCH unset and the host's arch not in +# ARCHES, i.e. this instance doesn't build it +# ARCHES space-separated arches the instance builds (the +# Makefile sets it); only used for that exception # IMAGE_SMOKE_PORT container port (default 8000) # IMAGE_SMOKE_PATH URL path (default /health/live) # IMAGE_SMOKE_RUN_ARGS extra `docker run` words, whitespace-separated and @@ -42,8 +46,8 @@ if [[ ! -f "$tar" && -n "${RELEASE_VERSION:-}" ]]; then fi fi if [[ ! -f "$tar" ]]; then - if [[ -z "${RELEASE_ARCH:-}" ]]; then - echo "image-smoke: no $arch image in $(dirname "$prefix") for this host; the per-arch smoke already ran in the build job. Skipping." + if [[ -z "${RELEASE_ARCH:-}" && -n "${ARCHES:-}" && " ${ARCHES} " != *" $host_arch "* ]]; then + echo "image-smoke: this instance doesn't build $host_arch (ARCHES=$ARCHES), so there is no image to run on this host; each arch is smoke-tested by its own build leg. Skipping." exit 0 fi echo "image-smoke: $tar not found; run 'make build' first." >&2 @@ -92,8 +96,17 @@ SKOPEO_DIST_DIR="$tar_dir" bash "$(dirname "${BASH_SOURCE[0]}")/skopeo.sh" -- \ "docker-archive:/dist/$(basename "$work")/image.tar:$image" docker load -i "$work/image.tar" -cid="$(docker run -d -p "127.0.0.1::${port}" "${run_args[@]}" "$image")" -mapped="$(docker port "$cid" "${port}/tcp" | head -n1 | sed 's/.*://')" +# ${run_args[@]+...}: an empty array is unbound under `set -u` on bash < 4.4. +cid="$(docker run -d -p "127.0.0.1::${port}" ${run_args[@]+"${run_args[@]}"} "$image")" +# No `| head -n1`: head closing the pipe early can SIGPIPE `docker port`, +# which fails the pipeline under pipefail. Take the first line in the shell. +ports="$(docker port "$cid" "${port}/tcp")" +mapped="${ports%%$'\n'*}" +mapped="${mapped##*:}" +if [[ -z "$mapped" ]]; then + echo "image-smoke: container publishes no host port for ${port}/tcp" >&2 + exit 1 +fi url="http://127.0.0.1:${mapped}${path}" echo "image-smoke: waiting up to ${timeout}s for $url" diff --git a/scripts/post-create.d/README.md b/scripts/post-create.d/README.md index 626fb03..c566a9b 100644 --- a/scripts/post-create.d/README.md +++ b/scripts/post-create.d/README.md @@ -1,8 +1,11 @@ # scripts/post-create.d/ Instance-owned hook: extra steps an instance runs when the devcontainer -is created. This directory ships only this file; an instance adds its -scripts here (see `docs/TEMPLATE.md`'s "Instance extension points"). +is created. This directory ships no scripts in base; a mid-layer template or an instance +adds `NN-name.sh` scripts here (see `docs/TEMPLATE.md`'s "Instance extension +points"). A script a template ships is `replace` tier in its sync manifest, so +a sync overwrites it; an instance's own script is listed in +`.github/template-sync-manifest.local.yml` so a sync leaves it alone. - Every `*.sh` here runs as `vscode` inside the running devcontainer, with the compose stack up, in lexical filename order (use an `NN-` prefix, diff --git a/scripts/runner-setup.d/README.md b/scripts/runner-setup.d/README.md index 814a87a..df9ce57 100644 --- a/scripts/runner-setup.d/README.md +++ b/scripts/runner-setup.d/README.md @@ -1,7 +1,8 @@ # scripts/runner-setup.d/ -Instance hook directory for the runner stage of the top-level -`Dockerfile`. `../runner-setup.sh` (template-owned, `replace` tier) creates +Optional instance hook directory for the runner stage of the top-level +`Dockerfile`; an instance whose runner image doesn't call `../runner-setup.sh` +(template-react's nginx runner, say) leaves it empty and unused. `../runner-setup.sh` (template-owned, `replace` tier) creates the group-0 runtime user, makes the home directory and `/app/data` group-writable (and nothing else under `/app`), then runs every `*.sh` here in glob order as `bash NN-name.sh APP_UID`, still as root, in the same `RUN` layer. @@ -16,7 +17,7 @@ directories) instead of editing `runner-setup.sh`. - Use `set -euo pipefail` and give any extra *writable* directory (cache, uploads) group-0 ownership and `g+rwX`, as `runner-setup.sh` does for `/app/data`. App code is not writable: the `Dockerfile` copies it after - this runs, with `--chown=:0` and a read-only `--chmod`. + this runs, with `--chown=:0` and numeric read-only modes (0550/0440). ## Don't diff --git a/scripts/runner-setup.sh b/scripts/runner-setup.sh index bcbeba6..b398071 100755 --- a/scripts/runner-setup.sh +++ b/scripts/runner-setup.sh @@ -6,12 +6,18 @@ # Run it BEFORE copying app content. It creates the user and group 0 # ownership only where the app writes at runtime: the home directory and # /app/data (group read/write/execute). /app itself stays root-owned and not -# writable. Then COPY app code with `--chown=:0 --chmod=...` as -# read-only (e.g. `--chmod=u=rX,g=rX,o=`: 0550 directories, 0440 files), so it -# needs no recursive chgrp/chmod (which would copy every file into a new +# writable. Then COPY app code with `--chown=:0` as +# read-only: 0550 directories, 0440 files, set with numeric modes per kind +# (`find -type d/-type f ... chmod`, or `--chmod=0550`/`0440` per source). Not +# the symbolic `--chmod=u=rX,g=rX,o=`: it left files unreadable on GitHub's +# stock-runner Docker even with dockerfile:1.28. Either way +# it needs no recursive chgrp/chmod (which would copy every file into a new # layer) and the runtime user can't modify it. Other writable directories # (caches, upload dirs) are created by runner-setup.d hooks. # +# Optional: an instance whose runner image needs no such user (an nginx runner +# with its own non-root user) never calls this script. +# # Usage: runner-setup.sh APP_UID # # Afterwards runs every runner-setup.d/*.sh next to this script, in glob diff --git a/scripts/skopeo.sh b/scripts/skopeo.sh index 74edcbc..2749821 100755 --- a/scripts/skopeo.sh +++ b/scripts/skopeo.sh @@ -20,7 +20,9 @@ done [[ $# -gt 0 ]] || { echo "usage: $0 [DOCKER_RUN_ARGS...] -- SKOPEO_ARGS..." >&2; exit 2; } shift +# `${arr[@]+"${arr[@]}"}` below: an empty array is an unbound variable under +# `set -u` on bash older than 4.4 (macOS ships 3.2). dist="$(cd "${SKOPEO_DIST_DIR:-dist}" && pwd)" exec docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp \ - -v "$dist:/dist" "${docker_args[@]}" "$SKOPEO_IMAGE" "$@" + -v "$dist:/dist" ${docker_args[@]+"${docker_args[@]}"} "$SKOPEO_IMAGE" "$@" diff --git a/scripts/test-image-scripts.sh b/scripts/test-image-scripts.sh new file mode 100755 index 0000000..989db97 --- /dev/null +++ b/scripts/test-image-scripts.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Offline tests for the template-owned image logic that has no other check: +# the Makefile's repo derivation, check-dockerfile-versions.sh, image-smoke.sh's +# missing-tar failure and skopeo.sh's argument handling. Needs no Docker daemon +# (docker is stubbed) and writes only to a temp directory. Run by the +# `image-scripts-test` hook. +set -euo pipefail + +# Hermetic: a pre-commit run exports GIT_* (the test inits scratch repos and +# must never touch the real one), and a release run exports RELEASE_ARCH etc. +unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_COMMON_DIR RELEASE_ARCH RELEASE_VERSION ARCHES GITHUB_REPOSITORY +unset IMAGE_SMOKE_PORT IMAGE_SMOKE_PATH IMAGE_SMOKE_TIMEOUT IMAGE_SMOKE_RUN_ARGS IMAGE_TAR_PREFIX SKOPEO_IMAGE + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +failures=0 + +fail() { + echo "FAIL: $*" >&2 + failures=$((failures + 1)) +} + +# expect_eq NAME EXPECTED ACTUAL +expect_eq() { + [[ "$2" == "$3" ]] || fail "$1: expected '$2', got '$3'" +} + +# Makefile: REPO from the origin remote, in every URL form we claim to parse. +repo_of() { + local dir="$tmp/repo-$RANDOM" + mkdir -p "$dir" + git -C "$dir" init -q + git -C "$dir" remote add origin "$1" + (cd "$dir" && env -u GITHUB_REPOSITORY make -s -f "$root/Makefile" \ + -f <(printf 'show-repo:\n\t@echo $(REPO)\n') show-repo) +} +while IFS='|' read -r url want; do + expect_eq "repo from $url" "$want" "$(repo_of "$url")" +done <<'EOF_URLS' +https://github.com/o/r.git|o/r +https://github.com/o/r|o/r +https://user:token@github.com/o/r.git/|o/r +git@github.com:o/r.git|o/r +ssh://git@github.com/o/r.git|o/r +ssh://git@github.com:22/o/r.git|o/r +ssh://git@ssh.github.com:443/o/r|o/r +ssh://github.com:2222/o/r.git|o/r +git+ssh://git@host:22/o/r.git|o/r +SSH://git@host:22/o/r.git|o/r +EOF_URLS + +# GITHUB_REPOSITORY wins over the remote; a failing git (no origin here) is an +# error for targets that need REPO, with git's message on stderr, never a guess. +dir="$tmp/repo-pref" +mkdir -p "$dir" +git -C "$dir" init -q +git -C "$dir" remote add origin https://github.com/o/r.git +out="$(cd "$dir" && GITHUB_REPOSITORY=x/y make -s -f "$root/Makefile" -f <(printf 'show-repo:\n\t@echo $(REPO)\n') show-repo)" +expect_eq "GITHUB_REPOSITORY preferred" "x/y" "$out" +dir="$tmp/repo-norem" +mkdir -p "$dir" +git -C "$dir" init -q +out="$(cd "$dir" && make -s -f "$root/Makefile" repo-check 2>&1)" && fail "repo-check: git failure should fail" +[[ "$out" == *"cannot derive the repository"* && "$out" == *"origin"* ]] || fail "repo-check: wrong message: $out" +out="$(cd "$dir" && make -s -f "$root/Makefile" publish RELEASE_ARCH=amd64 OCI_REGISTRY=r 2>&1)" && fail "publish: git failure should fail" +[[ "$out" == *"cannot derive the repository"* ]] || fail "publish: wrong message: $out" +out="$(cd "$dir" && make -s -f "$root/Makefile" publish RELEASE_ARCH=amd64 2>&1)" || fail "publish: skipped publish should not need REPO: $out" +(cd "$dir" && GITHUB_REPOSITORY=o/r make -s -f "$root/Makefile" repo-check) || fail "repo-check: GITHUB_REPOSITORY should pass" + +# check-dockerfile-versions.sh in a scratch repo layout. +dv="$tmp/dv" +mkdir -p "$dv/.devcontainer" +run_dv() { (cd "$dv" && bash "$root/scripts/check-dockerfile-versions.sh" "$@" 2>&1); } +printf 'ARG A=1\nARG B="x" # c\nFROM scratch\nARG A\n' >"$dv/.devcontainer/Dockerfile" +printf 'ARG A\nARG A=1\nARG B=x\nFROM scratch\nARG A=1\n' >"$dv/Dockerfile" +run_dv A B >/dev/null || fail "check-dockerfile-versions: matching ARGs (bare re-declaration first) should pass" +printf 'ARG A=1\nFROM scratch\nARG A=2\n' >"$dv/Dockerfile" +out="$(run_dv A)" && fail "check-dockerfile-versions: conflicting defaults in one file should fail" +[[ "$out" == *"conflicting defaults"* ]] || fail "check-dockerfile-versions: wrong message: $out" +printf 'ARG A=2\n' >"$dv/Dockerfile" +out="$(run_dv A)" && fail "check-dockerfile-versions: mismatch should fail" +[[ "$out" == *"mismatch"* ]] || fail "check-dockerfile-versions: wrong message: $out" +printf 'ARG A\n' >"$dv/Dockerfile" +run_dv A >/dev/null && fail "check-dockerfile-versions: no default should fail" +printf 'ARG B=1\n' >"$dv/Dockerfile" +run_dv A >/dev/null && fail "check-dockerfile-versions: missing ARG should fail" + +# image-smoke.sh must fail when the tar for the arch under test is missing. +case "$(uname -m)" in + aarch64 | arm64) host=arm64 other=amd64 ;; + *) host=amd64 other=arm64 ;; +esac +mkdir -p "$tmp/dist" +smoke() { + env IMAGE_TAR_PREFIX="$tmp/dist/app-1.0.0" SKOPEO_IMAGE=unused RELEASE_VERSION=1.0.0 "$@" \ + bash "$root/scripts/image-smoke.sh" 2>&1 +} +out="$(smoke RELEASE_ARCH="$host")" && fail "image-smoke: missing tar with RELEASE_ARCH should fail" +[[ "$out" == *"not found"* ]] || fail "image-smoke: wrong message: $out" +out="$(smoke ARCHES="$host")" && fail "image-smoke: missing host-arch tar should fail" +[[ "$out" == *"not found"* ]] || fail "image-smoke: wrong message: $out" +smoke ARCHES="$other" >/dev/null || fail "image-smoke: host arch not built by the instance should skip" + +# skopeo.sh: no extra docker args (empty array under set -u) and with some. +mkdir -p "$tmp/bin" "$tmp/dist-s" +printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$*"\n' >"$tmp/bin/docker" +chmod +x "$tmp/bin/docker" +sk() { PATH="$tmp/bin:$PATH" SKOPEO_IMAGE=img SKOPEO_DIST_DIR="$tmp/dist-s" bash "$root/scripts/skopeo.sh" "$@"; } +out="$(sk -- copy a b)" || fail "skopeo.sh: no docker args should work" +[[ "$out" == *"img copy a b" ]] || fail "skopeo.sh: unexpected command: $out" +out="$(sk -i -- login)" || fail "skopeo.sh: docker args should work" +[[ "$out" == *" -i img login" ]] || fail "skopeo.sh: unexpected command: $out" + +if [[ $failures -ne 0 ]]; then + echo "$failures failure(s)" >&2 + exit 1 +fi +echo "image scripts: all tests passed"