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 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-14 03:11:58 +02:00
1 parent 6c66b812b1
commit 0234478db0
5 files changed
+296 -4

No files matched your search

+3 -1
View File
@@ -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
+4
View File
@@ -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.
+17 -3
View File
@@ -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.
@@ -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<T extends {id:string;label:string;hint:string}>({ 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</`; `with a delay` → `Comes back after a wait`).
- Add form shows the example row; edit form does not.
- [ ] **Step 2:** `bun test test/custom-ui.test.tsx test/templates.test.ts` → FAIL.
- [ ] **Step 3: Implement** in the spec's order with messages beside their fields.
- [ ] **Step 4:** same → PASS; full `bun test` and `bun run typecheck` green.
### Task 4: Browser check and docs
- [ ] Build (`bun run build:css && bun run build:js && bun run build:static`), serve on a fresh port, puppeteer (scratchpad) at 360px and 1280px: open Settings → Your own → Add an event, tap each example, check no horizontal scroll, fill the banner and save, see the row; same for Add a task.
- [ ] PRD F13 and F17: a sentence each on examples and their bar. DESIGN.md chips: the example row. TODO.md: tick the item.
- [ ] Final `bun test`, `bun run typecheck`.
- [ ] Final whole-change review (fresh reviewer); then report. Changelog entry follows the feature commit when the owner asks to commit.
@@ -0,0 +1,161 @@
# Examples to start from, and a pass over the add forms — design
Date: 2026-10-14. Status: approved in conversation (approach A, both design sections).
## Problem
A reader who already knows what they want — "a banner that ends on the 28th", "1300 points each
week" — cannot see which combination of fields in *Add an event* or *Add a task* produces it. The
owner's words: "in the add event/task, its confusing to figure out how to create something you want.
Having examples you can start from would be very useful."
Both forms also grew field by field (TODO.md), and read that way: enum values as copy, every note
and error stacked at the bottom, and Save disabled with no reason given.
## Outcome
- Each add form opens with a row of examples. One tap shapes the form for a common kind of entry;
the reader fills in the name, dates and figures, then saves.
- Both forms read as a reader thinks: plain words, choices in view, a message beside the field it is
about, and Save that says what it still needs.
- Nothing that is stored changes: no schema, store, id or export change.
## Non-negotiables
- **An example sets a shape, never a fact.** It may set kind, cadence, repeat mode, whether the end
is known, the period unit, the measure kind and the unit word. It never sets a title (the title
gets the example's text as a *placeholder* only), a date, a time, a target, or a game. The single
exception is *Monthly shop buy-out*'s days-needed of 1, which is the shape "tick it off once", not
a figure about any game.
- **Nothing is saved until the reader saves.** Save stays disabled until every date and figure the
shape needs has been typed, so nothing an example wrote can be saved unread.
- **No date is guessed.** No "today", no "the 1st": games reset on different days, and a guessed
date that looks deliberate is the failure this product exists to avoid.
- **"I don't know when it ends" stays first-class**, and every existing rule of the event form
(preset windows, measured cycles, delays, `comesRoundEarly`, the stranded-ticks notice, `until`
surviving a save) behaves exactly as before.
## Part 1 — examples
### Data
`src/shared/templates.ts`, pure data with no React and no clock:
```ts
export interface EventTemplate {
id: string; // kebab-case, unique within the list
label: string; // the chip text
placeholder: string; // the title field's placeholder while this example is chosen
hint: string; // one line: what is left for the reader
sets: { type: EventType; cadence: Cadence; repeatMode: RepeatMode; endKnown: boolean };
}
export interface TaskTemplate {
id: string;
label: string;
placeholder: string;
hint: string;
sets: {
unit: "week" | "month";
kind: "days" | "amount";
required?: string; // as typed; "" empties the field
amountUnit?: string;
};
}
export const EVENT_TEMPLATES: readonly EventTemplate[];
export const TASK_TEMPLATES: readonly TaskTemplate[];
```
| Event example | type | cadence | repeatMode | endKnown | hint |
|---|---|---|---|---|---|
| Banner with a known end | banner | one-off | never | true | Fill in when it starts and when it ends. |
| Login event | login | one-off | never | true | Fill in when it starts and when it ends, or tick that you don't know. |
| Monthly shop reset | shop | monthly | never | true | Fill in a day it resets on. Each one runs until the next. |
| Recurring endgame cycle | challenge | custom | forever | true | Fill in one cycle's start and end; how often it comes back is worked out from them. |
| Maintenance | maintenance | one-off | never | true | Fill in when it starts and ends, with times. |
| Task example | unit | kind | required | amountUnit | hint |
|---|---|---|---|---|---|
| Weekly points target | week | amount | — | points | Fill in the target, and the day the week rolls over if it isn't Monday. |
| A few days each week | week | days | "" | — | Fill in how many days a week it needs. |
| Monthly shop buy-out | month | days | "1" | — | Fill in the day the month rolls over. |
| Monthly target | month | amount | — | — | Fill in the target and what it counts. |
Placeholders are generic ("Limited banner", "Login bonus", "Shop reset", "Endgame cycle",
"Maintenance", "Weekly points", "Weekly chore", "Monthly shop", "Monthly target"), never a real
event's name.
### Behaviour
- The row ("Start from an example") renders only on add: `EventForm` and `TaskForm` without
`initial`. Editing never shows it.
- A tap applies `sets` and nothing else. Game, title, dates, times, note, target, weekday and month
day keep whatever the reader typed. On the event form it also clears `ownCadence`, so a measured
cycle is shown rather than a stale hand-stated one.
- The last example tapped is pressed (`aria-pressed="true"`); its `placeholder` replaces the title
placeholder and its `hint` shows under the row. Editing a field afterwards does not unpress it —
it is where the reader started, not a lock.
- Applying a shape is a pure function per form (`withEventTemplate`, `withTaskTemplate`) so it is
provable without a click: the suite renders statically and cannot click.
## Part 2 — the pass
### EventForm
1. **Order:** Game → Name → How often → Starts → Ends → repeat details → Kind → Note.
2. **Words:**
- How often (was *Cadence*): *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*. Stored values are unchanged.
- Name (was *What is it*).
3. **Pills, not selects, for How often and Repeat**, in the task form's pill style, wrapping at phone
width. *Comes back after a wait* is a disabled pill while there is no end to measure from, exactly
as the option is disabled today. Choosing *On its own cycle* while the repeat is *Doesn't repeat*
moves it to *Comes back as it ends*, because a cycle that never comes back is a contradiction on
screen; a stored custom-never record still reopens as it is.
4. **Messages beside their field:** the no-time note under Starts; *ends before it starts* and *isn't
a real date* under Ends; the unknown-end note under its checkbox; *comes round before it ends*
under the repeat details.
### Both forms
5. **Save says why it is disabled.** A muted line beside the buttons: "Still needed: a name and a
start date." It comes from a pure `eventStillNeeded` / `taskStillNeeded`, and Save's disabled
state is derived from the same function, so the line and the button cannot disagree. Errors
(backwards dates, comes round early, an unstatable wait) stay where they are, beside their field,
in critical.
- Event needs, in order: a name; a start date; an end date (a dated window whose end is stated
as known but is empty, or a wait with no end); a cycle length (comes back as it ends, not
measured, no valid length); a wait (comes back after a wait, no valid length).
- Task needs: a name; a day of the month (monthly, unreadable day); how many days (tick it off,
not a whole number from 1 to 7 a week or 31 a month); the target (reach a number, unreadable),
or a target of at least 1 / a smaller target (outside `AmountTarget`'s 1–1,000,000,000).
6. **One field and pill style.** Both forms use the name *Name* and DESIGN.md's input spec (8px
corners, 8×12 padding, ground fill, hairline border, faint on focus). The pill and field classes
live once in `src/client/components/formParts.tsx`, with the example row and the still-needed line.
7. **Phone width.** Checked in a browser at 360px and desktop: no horizontal scroll, and every
control usable.
### Out of scope
The entry point (Settings → Your own), `GameForm`, and the commit-button padding mismatch with
DESIGN.md (app-wide; noted, not fixed here).
## Testing
- `test/templates.test.ts`: ids are unique; no template sets a title, date or target; every event
template, given a name and dates, yields a valid draft through the same functions the form uses;
every task template, given a name and a target or day count, yields a `taskDraftFrom` draft.
- `test/custom-ui.test.tsx`: the row renders on add and not on edit; pill copy and pressed states
replace the `selected` option assertions; `eventStillNeeded` and `taskStillNeeded` cases; Save is
disabled exactly when the list is non-empty or an error stands.
- Browser check (puppeteer, scratchpad only): tap each example, fill it, save, see the row; 360px
width with no horizontal scroll.
## Docs
PRD F13 and F17 gain a sentence on the examples and the bar they are held to; DESIGN.md's chips
section names the example row; TODO.md item ticked; a changelog entry (reader-visible) in a
follow-up `chore(changelog)` commit.