From 8b1ae01c9bdcc799ebb38f348168d8e92c9aa15a Mon Sep 17 00:00:00 2001 From: Xinyi Lin Date: Wed, 1 Jul 2026 19:11:08 -0400 Subject: [PATCH] feat: add section nav and docker deploy support --- .dockerignore | 14 ++ .gitignore | 6 + AGENTS.md | 253 ++++++++++++++++++++++++++++++++++ CLAUDE.md | 55 ++++++++ Dockerfile | 39 ++++++ README.md | 46 ++++++- server.mjs | 44 +++++- src/App.tsx | 6 +- src/components/SectionNav.tsx | 159 +++++++++++++++++++++ src/styles/index.css | 119 ++++++++++++++++ src/styles/resume-editor.css | 1 + 11 files changed, 730 insertions(+), 12 deletions(-) create mode 100644 .dockerignore create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 src/components/SectionNav.tsx diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f651dc4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +node_modules +dist +.vite +.DS_Store +*.local +.env +.env.* +*.pem +CONTINUITY.md +.claude/ +.git +.github +.playwright-cli +output/playwright diff --git a/.gitignore b/.gitignore index 8ea980e..c064a70 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,11 @@ dist .vite .DS_Store *.local +.env +.env.* +!.env.example +*.pem CONTINUITY.md .claude/ +.playwright-cli/ +output/playwright/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..16cb796 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,253 @@ +# jakeforge Agent Guide + +Generic working agreements for coding agents on this project. `AGENTS.md` is the +provider-agnostic source of truth; `CLAUDE.md` adds Claude-specific overrides. +A more specific or deeper doc (a nested `AGENTS.md`, README, or engineering doc) +wins over this file when it is current. + +jakeforge is a local-first, single-template resume editor: fill in a form-driven +on-page resume and export it in the Jake Gutierrez LaTeX style. A React 19 + Vite ++ TypeScript frontend renders the editable document; a small Node `server.mjs` +exposes only the LaTeX endpoints (template list, resume → `.tex`/PDF render via +Tectonic, `.tex` import). The editor surface, structured model, and LaTeX +pipeline were ported from the `role-fit-ai` sibling, with its AI, job-tracker, +applications, and DOCX surfaces removed. + +## Instruction Precedence + +1. User instructions for the current task. +2. Safety, data integrity, and secret handling — before stylistic preferences. +3. The nearest, most specific guidance file, when current. +4. Durable facts in the nearest `CONTINUITY.md`, over older chat context. +5. Existing architecture and conventions. + +Do not preserve stale rules. If the project shape changes, update the relevant +docs and the continuity ledger together. + +Keep this file a router: state agent behavior and high-level conventions here, +and keep detailed rules in the narrowest relevant document (a nested `AGENTS.md`, +README, or engineering doc). When content overlaps, point to the deeper doc +instead of duplicating it. + +## Project Shape + +- Stack: React 19 + TypeScript + Vite 7; Node `server.mjs` for the LaTeX API. + npm. Tectonic (system binary) compiles `.tex` → PDF when installed. +- Layout: + - `src/lib/` — resume data model + parse/serialize/LaTeX-extract helpers. + - `src/hooks/` — `useResumeEditor` (structured reducer), `useDocStyle` + (typography, localStorage), `useTemplates` (API client), `useResumeExport`. + - `src/sections/editor/` — the editable on-page resume (sections, entries, + bullets, skills, drag-reorder via `@dnd-kit`). + - `src/sections/` — read-only document + off-screen print layer. + - `src/components/` — reusable `Modal` shell, `ImportModal`, and `SectionNav`. + - `src/styles/` — `tokens.css`/`base.css` (design tokens), `resume-*.css` + (document + editor), `index.css` (imports + app shell). + - `server/latex/` — Jake's template renderer, plain-text/LaTeX parser, Tectonic + wrapper. `server/docx.mjs` — DOCX text extractor (zero-dep, shells to `unzip`). +- Entry points: `src/main.tsx` → `src/App.tsx` (UI); `server.mjs` (serves the + Vite app in dev / `dist` in prod, plus `/api/templates`, + `/api/render-resume-latex`, `/api/import-resume-tex`, `/api/import-resume-docx`). +- External services: none; local-only. Optional deps: a local Tectonic install + for LaTeX PDF output, and the system `unzip` (standard) for DOCX import. +- Deployment: optional self-hosted Docker image (see `Dockerfile` and README's + "Deploying" section). The image bundles Tectonic + `unzip` and requires + `ALLOWED_HOSTS` (comma-separated public hostnames) at runtime — the server + refuses to start without it when bound beyond loopback. + +Treat the project as local-first. The resume is the user's personal data — keep +it in `localStorage` only; never send it anywhere but the app's own LaTeX +endpoints (the local server in dev, the user's own self-hosted instance when +deployed). No third-party services either way. + +Viewport support is desktop/tablet first. The editor does not need to provide a +full phone-sized authoring experience; if mobile maintenance starts creating +awkward compromises, it is acceptable to use the same kind of small-screen +restriction/gate already used in the `role-fit-ai` sibling instead of polishing +every mobile layout. + +## Commands + +Run from the project root. + +- Install: `npm install` +- Run / dev: `npm run dev` (starts `server.mjs` on `PORT` or 5186; Vite in + middleware mode) +- Build: `npm run build` (`tsc` typecheck + `vite build` → `dist/`) +- Preview prod: `npm run preview` (`NODE_ENV=production node server.mjs`) +- Server syntax check: `node --check server.mjs` + +There is no test harness yet. Verify with the build, a server syntax check, and a +browser check of the editor + a Tectonic PDF render when LaTeX paths change. + +### Port reservations + +Sibling projects in this workspace use fixed, non-overlapping dev-server ranges +so a bound port means "the app is already running," not "pick another." The port +is pinned with Vite `strictPort: true`; when 5186 is bound, connect to the +running app instead of starting a second server or switching ports. + +- careflow: `5173-5180` +- role-fit-ai: `5181-5183` +- portfolio: `5184-5185` +- jakeforge: `5186` (Vite HMR socket on `24686`) + +## Start-Of-Task Checklist + +Before changing code or project files: + +1. Read `CONTINUITY.md` if it exists. +2. Read the nearest `AGENTS.md`, `CLAUDE.md`, README, or docs that apply. +3. Identify the goal, acceptance criteria, scope, and constraints. +4. Inspect the files you will touch before choosing an implementation. +5. If the request depends on current or recency-sensitive facts, establish the + date/time and prefer authoritative sources. +6. For non-trivial tasks, state a compact plan with concrete verification checks. +7. Ask one targeted clarifying question only when ambiguity could cause + user-facing confusion or irreversible work. Otherwise make a reasonable + assumption and proceed. + +## Accuracy, Recency, And Sourcing + +When a request depends on "latest", "current", "today", recent APIs, pricing, +release notes, security advisories, or compatibility: + +- Establish the current date/time (e.g. `date -Is`; on macOS, + `date '+%Y-%m-%dT%H:%M:%S%z'`) and state it when it affects the answer. +- Prefer official or primary sources: vendor docs, upstream repositories, + changelogs, release notes, standards, or maintainer announcements. +- For safety-, compatibility-, legal-, medical-, or financial-sensitive details, + cross-check reputable sources and call out source dates when relevant. +- Use library/API documentation tools when available. Pin the library and version + when known, fetch only the focused docs needed, and summarize rather than + dumping large source text. +- Use web search when it materially improves correctness; prefer official docs + before secondary explainers. + +## Agent Operating Principles + +- Think before coding. State important assumptions, surface tradeoffs, and ask + when confusion would change the solution. +- Keep it simple. Write the minimum durable code that solves the request; do not + add speculative features, knobs, abstractions, or future-proofing. +- Make surgical changes. Every changed line should trace to the request, a + cleanup caused by it, or a verification fix. +- Match the codebase. Prefer existing style, naming, patterns, framework choices, + and helper APIs over personal preference. +- Clean up only your own wake. Remove imports, state, helpers, files, or docs + made obsolete by your change; mention unrelated dead code instead of deleting + it. +- Define success in verifiable terms: reproduce the issue, make the change, run + the relevant test/build, and inspect the result. +- Loop until verified. If a check fails, use the failure as evidence, adjust, and + rerun the smallest meaningful check before broader ones. +- Use judgment on tiny tasks. A typo or one-line answer does not need ceremony. +- Push back when the requested path is riskier, broader, or more brittle than a + simpler way to satisfy the same goal. + +## Development And Editing + +- Default to read-only exploration before edits. +- Keep changes scoped and reviewable. +- Prefer patch-style edits over full rewrites unless a clean replacement is + requested or the file is no longer relevant. +- Preserve existing style and conventions. +- Keep hand-written source files modular. Treat files over ~300 lines as a prompt + to check boundaries; split when it improves readability or future change. Do + not cap necessary scope just to hit a line count. +- Keep public entrypoints stable where practical; isolate volatile logic behind + smaller helpers. +- Do not add default fallbacks during development just to hide failures. If a + required value is missing, fail visibly enough to fix the real cause. +- Do not leave empty `catch` blocks or silently swallow errors. +- Do not reinvent the wheel. When a mature library would reduce risk, ask before + adding it and help qualify the choice. +- Design UI for the end user and workflow, not for the database schema. +- Verify major UI changes in a real browser when feasible, rather than relying + only on static inspection. The Claude-specific tool choice (Chrome vs Preview) + lives in `CLAUDE.md`. + +## Secrets And Safety + +- Never print secrets, tokens, private keys, credentials, or broad environment + dumps. Do not ask the user to paste secrets. +- Never commit secrets or `.env` files; keep them git-ignored. +- Avoid commands that may expose secrets (dumping shell environments, reading + private key files). Redact sensitive strings in shared output. +- Remote API calls must be read-only unless the user explicitly requests a write; + dry-run requested writes first when possible. +- Pause and confirm before irreversible or destructive actions: bulk deletes, + history rewrites, schema or data drops, production/remote writes, or adding + paid or vendor dependencies. + +## Containers And Tooling + +- Never install system packages on the host unless the user explicitly asks. +- Prefer the project's existing workflow when one exists (`Dockerfile`, compose + files, Make targets, or documented scripts). +- If no workflow exists and dependencies are needed, discuss a minimal, + project-scoped setup before adding one. + +## Reading Documents And Data + +For PDFs, uploads, long documents, spreadsheets, or CSVs: + +- Read the full source before drafting. +- Draft the requested output. +- Before finalizing, re-check the source for factual accuracy, invented details, + and wording/style constraints. +- Label paraphrases explicitly when source-faithful handling matters. + +## Continuity Ledger + +Maintain one compact `CONTINUITY.md` for the project. It is the durable handoff +memory; keep it factual and bounded — no transcripts, raw logs, or chat dumps. + +- Read it at the start of each task before acting. +- Update it only for meaningful deltas: goal, constraints, durable decisions, + state, open questions, working set, or important tool outcomes. +- Tag every entry with an ISO date and a provenance tag: `[USER]`, `[CODE]`, + `[TOOL]`, or `[ASSUMPTION]`. Write `UNCONFIRMED` rather than guessing. +- Supersede changed facts explicitly instead of silently rewriting history. +- Keep `Snapshot` to ~25 lines, `Done (recent)` to ~7 bullets, and `Working set` + to ~12 paths. Compress older noise into milestone bullets that point to a + commit, PR, doc, or log. +- Record durable choices as ADR-lite entries, e.g. + `D001 ACTIVE: chosen stack is ...`. +- In replies after material work, include a brief snapshot: Goal, Now, Next, and + Open Questions. Print the full ledger only when it changed materially or the + user asks. + +## Verification And Definition Of Done + +A task is done when: + +- The requested change is implemented or the question is answered. +- Relevant verification was attempted — build, lint, tests, typecheck, document + rendering, or browser checks (see Commands). UI changes that alter layout, + styling, animation, or other visible surfaces get a real-browser check. +- Errors and warnings are fixed or explicitly listed as out of scope. +- Impact is explained: what changed, where, and why. +- Docs are updated for impacted behavior, setup, or workflow. +- `CONTINUITY.md` is updated when the change materially affects state, decisions, + risks, or next steps. +- If no build or test harness exists, say so and verify by the strongest + available lightweight check. + +## Git And Existing Work + +- The working tree may contain user edits or generated output. +- Run git commands from the relevant repository root; use non-interactive flags. +- Do not stage, commit, push, amend, reset, rebase, or switch branches unless the + user asks. +- Stage and commit `AGENTS.md` and `CLAUDE.md` like any other tracked file when + they're part of the change; do not single them out to exclude. `CONTINUITY.md` + and `.claude/` are gitignored, so they never appear as staging candidates. +- Never revert, delete, or overwrite changes you did not make unless explicitly + asked. +- Never force-push a shared branch or rewrite published history without an + explicit request. +- Avoid broad cleanup, drive-by refactors, and formatting churn. +- When asked to commit, prefer one coherent commit per reviewable unit. Follow + project-specific commit rules when present; otherwise use Conventional Commit + subjects such as `fix(scope): preserve calendar scroll`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e3f1ce5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# jakeforge — Claude Overrides + +`AGENTS.md` is the canonical guide. It is imported below, so its rules load +into context every session — no separate read step. `CONTINUITY.md` is **not** +imported (it changes constantly); read it fresh before acting. This file adds +Claude-specific behavior; when it conflicts with `AGENTS.md`, this file wins. + +@AGENTS.md + +## Tool Use + +- `Read` before `Edit`/`Write`. Never `Write` without reading first. +- Prefer `Edit` for targeted changes; use `Write` only for new files or + intentional full-file replacements. +- Prefer `Glob`/`Grep` for codebase searches; otherwise use `rg`. +- Use `Bash` for project commands, tests, builds, and git. Do not use shell write + tricks to overwrite files when `Edit`/`Write` is safer. +- Keep command output focused. Do not dump broad environments, secrets, or large + generated logs into chat. + +- Run all commands from the project root. `npm run dev` runs `server.mjs` + (frontend + LaTeX API together); there is no separate Vite process to start. +- A bound port 5186 means the app is already running — connect to it, do not + start a second server. +- LaTeX PDF (`/api/render-resume-latex` with `wantsPdf`) needs a local Tectonic + binary; when it is absent the server returns a `pdfError` and the UI falls back + to browser-print "PDF · clean". Don't treat a missing-Tectonic failure as a + code bug. + +## Visual QA + +Verify major UI changes in a browser when feasible (`AGENTS.md` default). +Pick the tool by what you're verifying: + +- **Layout / responsive / visual fidelity** → **Claude in Chrome** + (`mcp__Claude_in_Chrome`): real window, accurate at any width + (`resize_window`, e.g. 1440 / 768 / 375), faithful screenshots. +- **Content / computed styles / tokens / console** → **Claude Preview** + (`mcp__Claude_Preview`): `preview_snapshot` / `preview_inspect` are + deterministic (no pixel-guessing); `preview_screenshot` for a glance, fall + back to snapshot/inspect if blank. +- If the chosen tool's bridge isn't connected, use the other and note the gap. + +Default: **Claude in Chrome** — `npm run dev`, then navigate to +`http://localhost:5186`. The editor is a layout-and-typography surface (an +on-page resume), so prioritize a real-window screenshot; for a LaTeX change, also +open "Preview PDF" and confirm the Tectonic render. Expect a harmless Vite HMR +websocket line only if a sibling Vite already holds port 24686. + +## Communication + +Think privately; do not print raw reasoning. Report actions, blockers, +verification, skipped checks, and final outputs, and skip preambles unless they +help the user act. After material work, open with a brief ledger snapshot +(Goal, Now, Next, Open Questions). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d855d68 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM node:20-slim AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:20-slim +WORKDIR /app + +# unzip: server/docx.mjs shells out to it for DOCX import. +# Tectonic (LaTeX -> PDF): pinned static binary matching the image arch. +ARG TARGETARCH +RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates unzip && \ + case "${TARGETARCH}" in \ + arm64) TECTONIC_ARCH="aarch64-unknown-linux-musl" ;; \ + amd64|*) TECTONIC_ARCH="x86_64-unknown-linux-musl" ;; \ + esac && \ + curl -fsSL "https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%400.15.0/tectonic-0.15.0-${TECTONIC_ARCH}.tar.gz" \ + -o /tmp/tectonic.tar.gz && \ + tar -xzf /tmp/tectonic.tar.gz -C /usr/local/bin && \ + rm /tmp/tectonic.tar.gz && \ + apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev +COPY --from=build /app/dist ./dist +COPY server.mjs ./ +COPY server ./server + +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=5186 +# Required at runtime: comma-separated hostnames the app is reached by, e.g. +# ALLOWED_HOSTS=resume.example.com,203.0.113.7 — the server refuses to start +# without it when bound beyond loopback. Loopback names are always allowed. + +EXPOSE 5186 +CMD ["node", "server.mjs"] diff --git a/README.md b/README.md index ae41b04..b675934 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ tailoring, job tracker, and applications stripped out. intentionally unsupported — it extracts too poorly to be useful. - **In-app PDF preview** — compile and view the LaTeX PDF without downloading. - **Autosave** — the structured resume is persisted to `localStorage`, so a - reload keeps your work. Nothing leaves your machine. + reload keeps your work. Rendering only calls the app's own LaTeX endpoints. ## Getting started @@ -71,13 +71,48 @@ sidebar shows the clean-print hint. | `npm run build` | Typecheck (`tsc`) + production build to `dist/` | | `npm run preview` | Serve the production build (`NODE_ENV=production`) | +## Deploying (Docker) + +The `Dockerfile` builds a self-contained image: the production bundle, +`server.mjs`, Tectonic (arch-matched, so LaTeX PDF works in the container), and +`unzip` for DOCX import. + +```bash +docker build -t jakeforge . +docker run -p 5186:5186 -e ALLOWED_HOSTS=resume.example.com,203.0.113.7 jakeforge +``` + +`ALLOWED_HOSTS` is required: a comma-separated list of every hostname or IP the +app is reached by. It backs the same-origin/Host guard on the API, and the +server refuses to start without it when bound beyond loopback. Loopback names +(`localhost`, `127.0.0.1`) are always allowed, so on-box smoke tests and +container health checks work regardless. + +A single small instance (e.g. an EC2 `t3.micro`) is plenty — the server is one +Node process with no database. + +For a small EC2 deployment, publish container port `5186` on host port `80` and +include both the public DNS name and public IP in `ALLOWED_HOSTS`: + +```bash +docker run -d \ + --name jakeforge \ + --restart unless-stopped \ + -p 80:5186 \ + -e ALLOWED_HOSTS=ec2-example.compute-1.amazonaws.com,203.0.113.7 \ + jakeforge +``` + +Use plain HTTP only for smoke testing. Put HTTPS in front of the app before +entering real resume content on a hosted instance. + ## Architecture ``` src/ lib/ resume data model + parse/serialize/LaTeX-extract helpers hooks/ useResumeEditor · useDocStyle · useTemplates · useResumeExport - components/ reusable Modal shell + ImportModal + components/ reusable Modal shell, ImportModal, and SectionNav sections/ editor/ the editable on-page resume (sections, entries, bullets, skills) Resume*.tsx read-only document + off-screen print layer @@ -99,6 +134,7 @@ clean-print mirror. ## Privacy -Local-only and personal. The resume lives in `localStorage` and is sent solely to -the local LaTeX endpoints on your own machine for rendering. There is no account, -network upload, or third-party service. +Local-first and personal. The resume lives in `localStorage` and is sent solely +to the app's own LaTeX endpoints for rendering — on your machine when running +locally, or on your own server when self-hosting the Docker image. There is no +account, no database, and no third-party service. diff --git a/server.mjs b/server.mjs index 8a2d521..4dbbdd1 100644 --- a/server.mjs +++ b/server.mjs @@ -5,7 +5,6 @@ import { createServer } from "node:http"; import { readFile } from "node:fs/promises"; import { extname, join, resolve, sep } from "node:path"; -import { createServer as createViteServer } from "vite"; import { listTemplates, renderResumeTex, @@ -22,6 +21,38 @@ const root = process.cwd(); const isProduction = process.env.NODE_ENV === "production"; const port = Number(process.env.PORT ?? 5186); const host = process.env.HOST ?? "127.0.0.1"; +// Hostnames the app is served from when bound beyond loopback (e.g. an EC2 +// deployment reached by public IP during bring-up and by domain afterwards). +// Comma-separated; loopback names are always allowed so on-box smoke tests and +// container health checks keep working. +const allowedHostList = (process.env.ALLOWED_HOSTS ?? process.env.ALLOWED_HOST ?? "") + .split(",") + .map((h) => h.trim()) + .filter(Boolean); +const isLoopbackBind = host === "127.0.0.1" || host === "localhost" || host === "::1"; + +if (!isLoopbackBind && allowedHostList.length === 0) { + console.error( + `HOST=${host} binds beyond loopback but ALLOWED_HOSTS is not set — every /api request would be rejected.\n` + + `Set ALLOWED_HOSTS to the hostname(s) the app is reached by, e.g. ALLOWED_HOSTS=resume.example.com,203.0.113.7` + ); + process.exit(1); +} + +const allowedHosts = new Set([ + "localhost", + "127.0.0.1", + "[::1]", + `localhost:${port}`, + `127.0.0.1:${port}`, + `[::1]:${port}` +]); +for (const name of allowedHostList) { + allowedHosts.add(name); + allowedHosts.add(`${name}:${port}`); + allowedHosts.add(`${name}:80`); + allowedHosts.add(`${name}:443`); +} async function handleListTemplates(req, res) { if (req.method !== "GET") { @@ -172,9 +203,11 @@ async function serveStatic(req, res) { } } +// Vite is a devDependency; import it lazily so a production install +// (`npm ci --omit=dev`) can boot without it. const vite = isProduction ? null - : await createViteServer({ + : await (await import("vite")).createServer({ root, appType: "spa", server: { middlewareMode: true } @@ -183,10 +216,9 @@ const vite = isProduction const server = createServer((req, res) => { const pathname = new URL(req.url ?? "/", `http://${req.headers.host}`).pathname; - // Same-origin/Host guard for the local API: a website the user visits must not - // be able to drive this server cross-origin or read the resume via DNS rebind. - if (pathname.startsWith("/api/") && host === "127.0.0.1") { - const allowedHosts = new Set([`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`]); + // Same-origin/Host guard for the API: a website the user visits must not be + // able to drive this server cross-origin or read the resume via DNS rebind. + if (pathname.startsWith("/api/")) { if (!allowedHosts.has(req.headers.host ?? "")) { sendJson(res, 403, { error: "Forbidden host." }); return; diff --git a/src/App.tsx b/src/App.tsx index 83661c7..bb3ca73 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { useResumeExport } from "./hooks/useResumeExport"; import { ResumeEditor } from "./sections/editor/ResumeEditor"; import { ResumePrintLayer } from "./sections/ResumePrintLayer"; import { Modal } from "./components/Modal"; +import { SectionNav } from "./components/SectionNav"; import { buildStarterResume, reidResume } from "./sampleResume"; import { fileToText } from "./lib/importResume"; import type { ResumeData } from "./lib/resumeData"; @@ -200,6 +201,7 @@ export default function App() { const docStyle = useDocStyle(); const templates = useTemplates(); const [texStatus, setTexStatus] = useState(""); + const canvasRef = useRef(null); // The 11 fine-grained spacing sliders live behind a disclosure — most people // pick a preset and never open them. Closed by default; choice is remembered. @@ -400,6 +402,8 @@ export default function App() { + +