From 0234478db0dea385b7eb1f6f33cb72216e5fba9e Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Wed, 14 Oct 2026 03:11:58 +0200 Subject: [PATCH] docs: record examples to start from, and the pass over the add forms PRD F13 said the form asked how often after Kind and had no word on examples; F17 and DESIGN.md had no example row. Adds the spec and plan, and ticks the TODO item. Co-Authored-By: Claude Opus 5.5 --- TODO.md | 4 +- docs/DESIGN.md | 4 + docs/PRD.md | 20 ++- .../plans/2026-10-14-form-examples.md | 111 ++++++++++++ .../specs/2026-10-14-form-examples-design.md | 161 ++++++++++++++++++ 5 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 docs/superpowers/plans/2026-10-14-form-examples.md create mode 100644 docs/superpowers/specs/2026-10-14-form-examples-design.md diff --git a/TODO.md b/TODO.md index 5cc4f25..7272238 100644 --- a/TODO.md +++ b/TODO.md @@ -45,7 +45,9 @@ `src/ingest/parsers/holodori.ts` against a new fixture, keeping the old one. The wiki stopped being maintained (`holodori.wiki` is a Miraheze wiki) → find another source; assess it in `docs/SOURCES.md` as the others are. -- [ ] Give *Add an event* and *Add a task* easy-to-use templates, and give both forms a UI/UX pass. +- [x] Give *Add an event* and *Add a task* easy-to-use templates, and give both forms a UI/UX pass. + Done 2026-10-14 as *examples to start from* — see PRD F13 and + `docs/superpowers/specs/2026-10-14-form-examples-design.md`. - Templates: one tap fills the common shapes so the reader only adjusts the details — for example a weekly points task, a monthly shop reset, a login event, a banner with a known end. A template only pre-fills fields; nothing is saved until the reader saves, and no figure is diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 65a89b2..707611c 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -415,6 +415,10 @@ selected tab fills with `raised` and turns ink; unselected tabs are faint and go label, a small numeric field on `ground`, `/ 2,100 points`, and a near-pill Save. Weekly and monthly counts sit on their own faint line under the strip's heading, so the reset countdown never wraps. +- **Example row** (top of *Add an event* and *Add a task*, never on an edit): a label-type "Start + from an example", then neutral pills — `near` border and wash when pressed, the form's own pill + rather than a game chip, because an example belongs to no game — and the pressed example's + one-line hint in faint. The form's choices (how often, repeat, done when) use the same pill. - **What's new card** (top of the main column, never the rail): the eyebrow, up to five lines of kind (faint, fixed width) · title (muted) · date (faint, hidden on phones), then a near-pill **Got it** and a faint **See all**. Shown once per batch of changes; never modal. diff --git a/docs/PRD.md b/docs/PRD.md index f784ae6..a2be4ae 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -355,8 +355,10 @@ Four constraints, each protecting something that already exists: no server to restore from. A reader's event may also state how it comes round again. The form asks that -directly after **Kind**, before it asks for any date, because the answer -decides which dates are even questions: +(**How often**) directly after the name, before it asks for any date, because +the answer decides which dates are even questions. Its five answers read *Once · +Every day · Every week · Every month · On its own cycle* on screen; the table +names them by what is stored: | Cadence | What it asks for | What it stores | |---|---|---| @@ -379,6 +381,17 @@ Which of the five a saved event opens in is derived from the rule itself, so an event made before this control existed, or one that arrived by import, opens in whichever answer actually describes it. +**Examples to start from.** Readers who knew what they wanted could not see which answers produced +it, so *Add an event* and *Add a task* open with a row of examples — a banner with a known end, a +login event, a monthly shop reset, a recurring endgame cycle, maintenance; a weekly points target, a +few days each week, a monthly shop buy-out, a monthly target (`src/shared/templates.ts`). One tap +sets the form's **shape** — kind, how often, how it repeats, how it is measured — and nothing else: +never a name (the example's text is only a placeholder), a date, a time, a target or a game, and no +date is guessed, not even today. A figure the reader did not type would read as one we sourced, the +bar built-in tasks are held to. Save stays disabled until every date and figure the shape needs is +typed, and says what is still needed rather than greying out in silence. Editing never shows the +examples. + The rule is stored; its occurrences are derived, and each one is an ordinary event everywhere in the app: its own countdown, its own completion, its own daily checklist. @@ -560,7 +573,8 @@ Some chores roll weekly or monthly rather than daily — earning ZZZ's weekly ac finishing Endfield's weekly routine — and no source publishes them, so they were nowhere in the app. A reader can now set them up per game in *Your own*: a title, every week (from a weekday they pick) or every month (from a day of the month), and either **tick it off** (N days in the period, one by -default) or **reach a number** (a target such as 2,100, with an optional unit). +default) or **reach a number** (a target such as 2,100, with an optional unit). The form opens with +examples to start from, held to the same bar as the event form's (F13, *Examples to start from*). - **They roll on the game's clock.** A week starts at the game's daily reset on the chosen day, on the reader's server — the same clock every daily tick is cut on, so nothing new to get wrong. diff --git a/docs/superpowers/plans/2026-10-14-form-examples.md b/docs/superpowers/plans/2026-10-14-form-examples.md new file mode 100644 index 0000000..a7db4f7 --- /dev/null +++ b/docs/superpowers/plans/2026-10-14-form-examples.md @@ -0,0 +1,111 @@ +# Examples to start from, and a pass over the add forms — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Give *Add an event* and *Add a task* a row of examples that shape the form, and make both +forms read as a reader thinks. + +**Architecture:** Examples are pure data in `src/shared/templates.ts`. Each form applies one through a +pure function on its state, so the suite (static render, no clicks) can prove what a tap does. The +event form's derived state moves into one exported pure function, `eventFormState`, so validity, the +draft and the "still needed" line all come from one place. Shared field/pill classes, the example row +and the "still needed" sentence live in `src/client/components/formParts.tsx`. + +**Tech Stack:** Bun, React 19 (static render in tests via `react-dom/server`), Tailwind, zod. + +**Spec:** `docs/superpowers/specs/2026-10-14-form-examples-design.md` + +## Global Constraints + +- An example sets a shape, never a fact: never a title (placeholder only), date, time, target or game. Only exception: *Monthly shop buy-out* sets `required: "1"`. +- Nothing is saved until the reader saves; Save stays disabled until every date and figure the shape needs is typed. +- No date is guessed — no "today", no "the 1st". +- "I don't know when it ends" stays first-class; preset windows, measured cycles, delays, `comesRoundEarly`, the stranded notice and `until` survival behave exactly as before. +- No schema, store, id or export change. +- The examples row renders only on add (no `initial`), never on edit. +- Copy: How often → *Once · Every day · Every week · Every month · On its own cycle*; Repeat → *Doesn't repeat · Comes back as it ends · Comes back after a wait*; Kind → *Banner · Story · Rerun · Challenge / endgame · Login rewards · Shop · Maintenance · Other*; the name field is *Name* in both forms. +- Commits wait for the owner's go-ahead (AGENTS.md); work stays uncommitted until asked. + +## Review Focus + +1. A reader types a title and dates, then taps an example — the title and dates must survive (Task 1/2/3 tests on `withEventTemplate` / `withTaskTemplate`). +2. Switching from *Recurring endgame cycle* to *Banner with a known end* must leave no repeat on the draft (Task 3 test). +3. *A few days each week* leaves the day count empty: Save disabled and the line says "how many days" (Task 2 test). +4. Choosing *On its own cycle* from *Doesn't repeat* must land on *Comes back as it ends*; a stored custom-never record must still reopen as never (Task 3 test). +5. At 360px the example row and the pills wrap without horizontal scroll (Task 4 browser check). + +--- + +### Task 1: The examples, as data + +**Files:** +- Create: `src/shared/templates.ts` +- Test: `test/templates.test.ts` + +**Interfaces:** +- Produces: `EventTemplate`, `TaskTemplate`, `EVENT_TEMPLATES`, `TASK_TEMPLATES` (shapes in the spec). + +- [ ] **Step 1: Write the failing test** — ids unique per list; `sets` keys limited to the shape keys; + no `sets` carries a title/date/target; only `monthly-shop` sets `required`, to `"1"` (and + `few-days` to `""`); placeholders non-empty and none equals a real event name in the list (smoke). +- [ ] **Step 2:** `bun test test/templates.test.ts` → FAIL (module not found). +- [ ] **Step 3: Implement** `src/shared/templates.ts` with the two tables from the spec verbatim. +- [ ] **Step 4:** `bun test test/templates.test.ts` → PASS. + +### Task 2: Task form — shared parts, examples, still-needed + +**Files:** +- Create: `src/client/components/formParts.tsx` — `field`, `pill(on)`, `labelClass`, `stillNeededLine(needs: string[]): string | null`, `ExampleRow`. +- Modify: `src/client/components/TaskForm.tsx` +- Test: `test/custom-ui.test.tsx`, `test/templates.test.ts` + +**Interfaces:** +- Consumes: `TASK_TEMPLATES`, `TaskTemplate`. +- Produces: + - `stillNeededLine(["a name","a start date"]) === "Still needed: a name and a start date."`; three → `"a, b and c"`; `[]` → `null`. + - `ExampleRow({ examples: readonly T[]; chosen: string | null; onChoose: (t: T) => void })` — renders "Start from an example", one pill per example with `aria-pressed`, and the chosen one's hint. + - `withTaskTemplate(f: TaskFields, t: TaskTemplate): TaskFields` — `{...f, unit, kind, required?, amountUnit?}` only. + - `taskStillNeeded(f: TaskFields): string[]` — in order "a name", "a day of the month", "how many days", "the target". + +- [ ] **Step 1: Failing tests:** + - `stillNeededLine` cases above. + - `withTaskTemplate` keeps `game`, `title`, `target`, `weekday`, `monthDay`; sets `unit`/`kind`; *A few days each week* empties `required`. + - `taskStillNeeded`: blank title → "a name"; `kind:"days", required:""` → "how many days"; `unit:"week", required:"9"` → "how many days"; amount with `target:""` → "the target"; complete → `[]`. + - Every task template, applied to `{title:"x", target:"100", required kept or "2"}` fields, gives `taskStillNeeded === []` and a non-null `taskDraftFrom`. + - Render: add form contains "Start from an example" and each label; edit form (`initial`) does not; label "Name" present. +- [ ] **Step 2:** `bun test test/custom-ui.test.tsx test/templates.test.ts` → FAIL. +- [ ] **Step 3: Implement** — formParts.tsx; TaskForm imports `field`/`pill`, labels "Name", inputs `px-3 py-2`, `chosen` state, `ExampleRow` only when `initial === undefined`, title placeholder from the chosen template, Save `disabled={draft === null || needs.length > 0}`, the still-needed line beside the buttons in `text-faint`. +- [ ] **Step 4:** same command → PASS; `bun run typecheck` clean. + +### Task 3: Event form — order, words, pills, messages, examples + +**Files:** +- Modify: `src/client/components/CustomForms.tsx` +- Test: `test/custom-ui.test.tsx`, `test/templates.test.ts` + +**Interfaces:** +- Consumes: `EVENT_TEMPLATES`, `ExampleRow`, `pill`, `stillNeededLine`. +- Produces: + - `EventShape = { type: EventType; cadence: Cadence; repeatMode: RepeatMode; endKnown: boolean; ownCadence: boolean }`, held as one `useState`. + - `withEventTemplate(shape: EventShape, t: EventTemplate): EventShape` → `{...shape, ...t.sets, ownCadence: false}`. + - `withCadence(shape: EventShape, cadence: Cadence): EventShape` — choosing `custom` while `repeatMode === "never"` moves to `"forever"`. + - `eventStillNeeded(s: { title; startDate; datedWindow; endKnown; endDate; delayNeedsEnd; needsCycle }): string[]` — "a name", "a start date", "an end date", "a cycle length". + - `eventFormState(values: EventFormValues, existingUntil: string | null)` — returns `{ startsAt, endsAt, endMs, preset, datedWindow, measured, measuring, delayNeedsEnd, repeat, backwards, endMissing, earlyReturn, needs, valid, draft }`, the block that is inline today, moved verbatim, plus `needs`. `valid = needs.length === 0 && !backwards && !endMissing && !earlyReturn && !repeatIncomplete`. + +- [ ] **Step 1: Failing tests:** + - `withEventTemplate` keeps nothing it shouldn't touch (shape only) and clears `ownCadence`. + - `withCadence(never-shape, "custom").repeatMode === "forever"`; a stored custom-never event reopens with *Doesn't repeat* pressed. + - Endgame → Banner: `eventFormState` with the banner shape and dates has `repeat === null`. + - Each event template, with title "x", start `2026-10-01`, end `2026-10-15`, is `valid` with a draft; a fresh banner with no dates lists "a name", "a start date", "an end date". + - Copy: update existing assertions (`Cadence` → `How often`; answers list; `value="daily" selected=""` → `/aria-pressed="true"[^>]*>Every day