mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 21:37:45 +00:00
plans: names webapp redesign spec and plans
This commit is contained in:
1 parent
e10ae435b3
commit
367ca8ac6e
5 files changed
+2583
No files matched your search
@@ -0,0 +1,672 @@
|
||||
# Names webapp: pages that change in place — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Every in-page state change of the names webapp updates only the nodes whose state changed; only a move to another screen is a page transition.
|
||||
|
||||
**Architecture:** Today every state change rebuilds the whole page (`page()` with fresh DOM) and `motion.ts` compares old and new pages (signatures, slots, runtime view-transition names, Firefox overlays) to make the swap look partial. That comparison is the source of every flicker reported. The new model: a view computes its state and calls `screen(kind, state, build)`; the screen of the same kind already shown gets `update(state)` and changes its own nodes (class changes animated by CSS transitions, `live()` cross-fades, `rollTo`, keyed list sync with `addTo`/`removeFrom`); another kind is built once and shown by `page()` as a page change. Page changes use view transitions with statically named shared elements (header, footer, step bar, order card, order name) in Chromium and WebKit, and a fade-through in Firefox. The comparison machinery is then deleted.
|
||||
|
||||
**Tech Stack:** Vite + TypeScript (vanilla DOM), Web Animations API, CSS transitions, View Transitions API, Playwright e2e (Chromium, Firefox, WebKit), vitest (node, no DOM).
|
||||
|
||||
**Spec:** This document (the owner approved the approach in conversation on 2026-10-17: "Proceed with your plan please, make the architecture correct and modern"). Context: `plans/names-codes/2026-10-09-names-web-store-plan.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Code lives in `/project/git/simplex.domains/webapp`; never commit unless the owner asks.
|
||||
- Follow CLAUDE.md: comments rare, short, no plan references; named constants; no dead code; fix every warning; English only.
|
||||
- In-page change = same route token family (a `refresh()` redraw or a poll answer) and same screen kind. It MUST NOT call `page()`, MUST NOT start a view transition, and MUST keep the DOM nodes of everything that did not change (same object identity).
|
||||
- Screen change (another kind, or another route) = one page transition: view transition with types `forward` / `back` / `screen` / `fill` / `theme` in browsers where `transitions()` is true; elsewhere (Firefox, `motion.ts` `VIEW_TRANSITIONS` false) the fade-through in `swapPage(..., "through")`.
|
||||
- View transitions name only static shared elements: `page-top`, `page-foot`, `offline-note`, `page` (the main content), `page-title` (`.page-head h1`), `page-lead` (`.page-head .lead`), `crumbs`, `step-ring`, transient `step-line-*` / `step-done-*` from `crumbs()`, `order-card` (`.summary`), `page-panel` (the main column's first `.panel`), `order-name`. No runtime pairing, no `part-N` / `gone-N` / `fresh-N` names.
|
||||
- Reduced motion: every change applies at once (`moving()` false), as today.
|
||||
- Timers and listeners that belong to a screen stop when the screen is replaced or the route is left (the screen's `AbortSignal`), not when a redraw of the same screen happens.
|
||||
- Focus: an update keeps focus on the node that had it (the node survives); a screen change focuses the new heading, as today (`focus.focusHeading`).
|
||||
- Determinism (owner, 2026-10-17: "implement all three, everything should be deterministic"): each view's state is one object changed only by `dispatch(event)` through a pure `reduce(state, event)`; a pure `screenOf(state)` picks the screen; a per-view transition table (`moves`) gives each screen-to-screen move its motion. Reducers and `screenOf` never read the clock, randomness, storage, the network or the DOM: time and outside data arrive inside events. Effects (API calls, storage writes, timers, focus) run in the view's controller, and their results come back as events. A move missing from the table is logged with `console.warn("[screens] <view>: unexpected move <from> → <to>")` and still shown as a page change. The pure modules live in `src/screens/` and are covered by vitest: every `screenOf` branch and every table row.
|
||||
- e2e must pass in Chromium, Firefox and WebKit (`npx playwright test --project=<browser>`), with the motion checks of `e2e/cuts.ts` (no flash, cut-in, cut-out, spike, jump, shift, scroll-jump).
|
||||
|
||||
- Every page-change animation that exists today is preserved (owner, 2026-10-17): header, footer and ground stay still and the footer moves to the new height; the order's name travels from the search result into the order card (`order-name`); the step bar's pills stay while the ring moves on (`step-ring`), the line to the reached step fills (`step-line-*`) and the step left turns to a growing tick (`step-done-*`); the order card stays and morphs across checkout → name code → payment → registering; headings and leads change in place; the main panel morphs; content that differs slides out and in the way the reader goes (`forward`/`back`) or fades within a page; the theme is one cross-fade.
|
||||
|
||||
**User decisions (already made):**
|
||||
- "Proceed with your plan please, make the architecture correct and modern" (2026-10-17): views update in place; page transitions only between screens; delete the redraw/diff machinery.
|
||||
- Firefox before 158 does not use view transitions (bug 2066829, fixed in 158); it changes pages by the fallback (fade through, shared parts moved by FLIP). Firefox 158 and newer use view transitions like Chromium (owner updated to 158, 2026-10-17: "Yes, proceed").
|
||||
- Order/registering page first, then checkout, Your names, search.
|
||||
- Pure screen functions, a transition table per view, and one reducer per view, all unit-tested; everything deterministic (2026-10-17).
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
| File | Responsibility after the change |
|
||||
|---|---|
|
||||
| `src/views/shell.ts` | Router glue, header/footer, popups, `page()` (screen change only), new `screen()` primitive and the shown screen's lifetime |
|
||||
| `src/motion.ts` | In-place motion (`live`, `crossFade`, `rollTo`, `addTo`, `removeFrom`, height easing, new `syncList`), page change (`transition`, `swapPage`), no comparison code |
|
||||
| `src/views/order.ts` | Order page: `follow()` loop computes state and calls `screen()` with kinds `pay-crypto`, `pay-card`, `confirming`, `slow`, `registering`, `registered`, `taken`, `code-issued`, `failed`, `not-on-device`, `status:<id>`; each kind has `build` and, where its state changes in place, `update` |
|
||||
| `src/views/checkout.ts` | Checkout and name-code screens with `update` for code applied/removed, method, waiting order, link previews |
|
||||
| `src/views/names.ts` | Your names screen with keyed row sync; edit and restore screens |
|
||||
| `src/views/search.ts` | Search screen with `update` for applied code, retarget banner, typed name kept |
|
||||
| `src/styles.css` | Page transition rules for the static names; CSS transitions for step states; no part/box/swap/gone/fresh rules |
|
||||
| `e2e/motion.spec.ts`, `e2e/cuts.ts` | Motion checks for the new model: in-place updates keep nodes and run no view transition |
|
||||
| `e2e/inplace.spec.ts` (new) | One test per screen: an in-page state change keeps unchanged nodes, runs no transition, shows no cut |
|
||||
| `src/screens/machine.ts` (new) | Pure core: `Machine`, `Motion`, `step()` (reduce, pick screen, look up the move, warn on an unexpected one) |
|
||||
| `src/screens/order.ts`, `checkout.ts`, `names.ts`, `search.ts`, `routes.ts` (new) | Pure per-view state, events, `reduce`, `screenOf`, `moves`; `routes.ts` gives moves between views (search → checkout → name code → order, Your names) |
|
||||
| `test/machine.test.ts`, `test/screens-*.test.ts` (new) | vitest: `step()`, every `screenOf` branch and every table row per view |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: The screen primitive
|
||||
|
||||
**Goal:** `screen()` in `shell.ts` builds a screen once and routes later states of the same kind to its `update`, with a per-screen `AbortSignal` for its timers.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/shell.ts` (add `screen`, `Built`, the shown-screen state; reset it in `nextRoute` when the route is not a redraw)
|
||||
- Modify: `src/views/order.ts:27-38` (`every()` takes a signal instead of pushing to `cleanups`)
|
||||
- Test: `e2e/inplace.spec.ts` (new; first test drives Task 2, so this task's test is the harness helper)
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] `screen(active, kind, state, build)` calls `build(state, signal)` and `page()` when no screen of `kind` is shown on this route family; otherwise calls the shown screen's `update(state)` and nothing else.
|
||||
- [ ] A screen without `update` is rebuilt (`page()`) when its kind is called again with a state whose `JSON.stringify` differs from the shown one; with an equal state it is left alone.
|
||||
- [ ] Leaving the route (`nextRoute` without `redrawing`) aborts the shown screen's signal and forgets it; a `refresh()` keeps it.
|
||||
- [ ] `e2e/inplace.spec.ts` exists with helpers `markNodes(page, selectors)` (sets `__kept = true` on each matched node) and `keptNodes(page, selectors)` (returns which still carry it), and `takeTransitions` from `e2e/cuts.ts`.
|
||||
- [ ] `npx tsc --noEmit -p .` clean.
|
||||
|
||||
**Verify:** `npx tsc --noEmit -p . && npm test` → no errors, `77 passed`
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 1: Add the primitive to `src/views/shell.ts`** (after `refresh()`):
|
||||
|
||||
```ts
|
||||
// A screen of a view is built once and then told each new state of what it shows, changing only what differs; a state
|
||||
// of another kind is another screen, shown as a page change. What a screen runs, as a countdown, stops with it.
|
||||
export interface Built<S> { sections: Node[]; update?: (state: S) => void }
|
||||
interface Shown { kind: string; state: string; update?: (state: unknown) => void; stop: AbortController }
|
||||
let shown: Shown | null = null
|
||||
|
||||
export function screen<S>(active: Active, kind: string, state: S, build: (state: S, signal: AbortSignal) => Built<S>): void {
|
||||
const same = shown !== null && shown.kind === kind && isRedraw() && document.querySelector(SHOWN_PAGE) !== null
|
||||
if (same && shown!.update) return shown!.update(state)
|
||||
if (same && shown!.state === JSON.stringify(state)) return
|
||||
shown?.stop.abort()
|
||||
const stop = new AbortController()
|
||||
const built = build(state, stop.signal)
|
||||
shown = { kind, state: JSON.stringify(state), update: built.update as Shown["update"], stop }
|
||||
page(active, built.sections)
|
||||
}
|
||||
```
|
||||
|
||||
In `nextRoute()`, where `focus.drop()` runs for a new page, also run:
|
||||
|
||||
```ts
|
||||
shown?.stop.abort()
|
||||
shown = null
|
||||
```
|
||||
|
||||
`state` passed to screens must be JSON-serialisable data (API views, records, flags), never DOM nodes or functions.
|
||||
|
||||
- [ ] **Step 2: Signal-owned timers in `src/views/order.ts`**: replace `cleanups`/`cleanUp`/`every` with
|
||||
|
||||
```ts
|
||||
function every(ms: number, f: () => void, signal: AbortSignal): void {
|
||||
const id = setInterval(f, ms)
|
||||
signal.addEventListener("abort", () => clearInterval(id), { once: true })
|
||||
}
|
||||
```
|
||||
|
||||
and give `onTheSecond(left, f, signal)` the same signal (clear its timeout on abort). `cleanups.push(card.destroy)` becomes `signal.addEventListener("abort", card.destroy, { once: true })`.
|
||||
|
||||
- [ ] **Step 3: Create `e2e/inplace.spec.ts` helpers**
|
||||
|
||||
```ts
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { recordCuts, takeCuts, takeTransitions } from "./cuts.ts"
|
||||
|
||||
test.use({ reducedMotion: "no-preference" })
|
||||
test.beforeEach(async ({ page }) => { await page.addInitScript(recordCuts) })
|
||||
|
||||
// marks the nodes a state change must keep, to tell afterwards which are still the same nodes
|
||||
export const markNodes = (page: Page, selectors: string[]) => page.evaluate((list) => {
|
||||
for (const s of list) for (const n of document.querySelectorAll("body > main " + s)) (n as unknown as { __kept: boolean }).__kept = true
|
||||
}, selectors)
|
||||
export const keptNodes = (page: Page, selectors: string[]) => page.evaluate((list) => list.filter((s) => {
|
||||
const n = document.querySelector("body > main " + s) as unknown as { __kept?: boolean } | null
|
||||
return n?.__kept === true
|
||||
}), selectors)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Type-check and unit tests**
|
||||
|
||||
Run: `npx tsc --noEmit -p . && npm test`
|
||||
Expected: no type errors, `Tests 77 passed`
|
||||
|
||||
```json:metadata
|
||||
{"files": ["src/views/shell.ts", "src/views/order.ts", "e2e/inplace.spec.ts"], "verifyCommand": "npx tsc --noEmit -p . && npm test", "acceptanceCriteria": ["screen() builds once and routes same-kind states to update", "screen without update rebuilds only on a changed state", "leaving the route aborts the screen signal; refresh keeps it", "inplace.spec.ts helpers exist", "tsc clean"], "modelTier": "frontier"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Order page as screens, registering changes in place
|
||||
|
||||
**Goal:** The order page's `follow()` loop calls `screen()` with a kind per screen; the registering screen's phases (committing → committed → wait → registering), its countdown, transactions and problem note change in place.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/order.ts` (`draw`, `payment`, `confirming`, `registering`, `track`, `txRows`, `registered`, `taken`, `codeIssued`, `failed`, status screens; remove `carry(note)` uses)
|
||||
- Modify: `src/styles.css` (`.stage` / `.node` state transitions)
|
||||
- Test: `e2e/inplace.spec.ts`
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Kinds: `pay-crypto`, `pay-card`, `confirming`, `slow`, `registering`, `registered`, `taken`, `code-issued`, `failed`, `not-on-device`, and `status:<name>` for each status page (`paid-late`, `paid-by-invoice`, `still-open`, `card-failed`, `expired-cancelled`, `expired-received`, `expired`, `price-changed`, `unreadable`, `unknown-link`).
|
||||
- [ ] `registering` update, for a new `RegistrationView` of the same order: each `.stage` gets its new state class (`done`/`active`/`""`), its node text crosses over (live), its title crosses over (live); the wait line fill starts on entering `committed`; the countdown clock and note keep their live behaviour; each tx row's value and chip cross over (live); the problem slot crosses over; heading/lead/crumbs/card are untouched unless their text changes (then live).
|
||||
- [ ] `pay-crypto` update: partial-payment alert slot, amount label and amount, copy fields, QR (only when the URI changed), "Waiting for (the rest of) payment" and the cancel link slot cross over in place; the rate countdown restarts only if `expiresAt` changed.
|
||||
- [ ] `confirming` update: the confirmations line crosses over; the slow timer keeps running from the same `cardSubmittedAt`.
|
||||
- [ ] Stage colours change by CSS transition (`transition: background-color, border-color, color, box-shadow` on `.stage .node` and the line drawn by `.stage::before`), not by a view transition.
|
||||
- [ ] e2e `registering changes in place`: on the code-order journey, nodes `.track`, `.stage:nth-child(1)`, `.countdown`, `.txs`, `.summary`, `h1` are marked during committing; after the commit is mined and again after the wait ends, all are still kept, `takeTransitions` is `[]`, `takeCuts` is `[]`, and the stage classes are `done active ""` then `done done active`.
|
||||
- [ ] e2e `a new screen is a page change`: registering → registered runs exactly one transition (or, in Firefox, `.page-leaving` appears once) and the heading takes focus.
|
||||
|
||||
**Verify:** `npx playwright test e2e/inplace.spec.ts e2e/paid.spec.ts e2e/taken.spec.ts --project=chromium` → all passed
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 1: Write the failing e2e test** in `e2e/inplace.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { control, REGISTRATION_MS, uniqueLabel } from "./helpers.ts"
|
||||
test("registering changes in place: the steps, countdown and transactions keep their nodes through each phase", async ({ page, request }) => {
|
||||
const { code } = await control(request, "code", { minLength: 6 }) as { code: string }
|
||||
await page.goto("/")
|
||||
await page.getByRole("button", { name: "Check a code" }).click()
|
||||
await page.getByRole("textbox", { name: "Code", exact: true }).fill(code)
|
||||
await page.getByRole("button", { name: "Use this code" }).click()
|
||||
await page.getByRole("textbox", { name: "Name", exact: true }).fill(uniqueLabel("inplace"))
|
||||
await page.getByRole("textbox", { name: "Name", exact: true }).press("Enter")
|
||||
await page.getByRole("button", { name: "Register with code" }).click()
|
||||
await page.getByRole("button", { name: "Continue" }).click()
|
||||
await page.getByText("I have saved my code").click()
|
||||
await page.getByRole("button", { name: "Register", exact: true }).click()
|
||||
await page.locator(".stage.active", { hasText: "Committing" }).waitFor({ timeout: REGISTRATION_MS })
|
||||
const kept = [".track", ".stage:nth-child(1)", ".countdown", ".txs", ".summary", "h1"]
|
||||
await markNodes(page, kept)
|
||||
await takeTransitions(page)
|
||||
await page.locator(".stage.active", { hasText: "Waiting" }).waitFor({ timeout: REGISTRATION_MS })
|
||||
expect(await keptNodes(page, kept)).toEqual(kept)
|
||||
expect(await page.evaluate(() => [...document.querySelectorAll("body > main .stage")].map((s) => s.className))).toEqual(["stage done", "stage active", "stage"])
|
||||
await page.locator(".stage.active", { hasText: "Registering" }).waitFor({ timeout: REGISTRATION_MS })
|
||||
expect(await keptNodes(page, kept)).toEqual(kept)
|
||||
expect(await takeTransitions(page)).toEqual([])
|
||||
expect(await takeCuts(page)).toEqual([])
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it to verify it fails**
|
||||
|
||||
Run: `npx playwright test e2e/inplace.spec.ts --project=chromium`
|
||||
Expected: FAIL at the first `keptNodes` (nodes were rebuilt).
|
||||
|
||||
- [ ] **Step 3: Kinds in `draw()`**: `draw(invoice, view, record)` builds the state `{ invoice, view, record }` and calls `screen(null, kindOf(invoice, view, record), state, buildOf(kind))`, where `kindOf` returns the kind strings above using exactly the branch conditions now in `draw()`, `payment()`, `confirming()` (slow when `Date.now() > slowAt`) and `expired()`. Status pages become `status:<name>` kinds with `build` returning the same `status(...)` sections they return today and no `update`.
|
||||
|
||||
- [ ] **Step 4: Registering screen** — rewrite `registering()` as `buildRegistering(state, signal): Built<OrderState>`:
|
||||
|
||||
```ts
|
||||
function buildRegistering({ invoice, view, record }: OrderState, signal: AbortSignal): Built<OrderState> {
|
||||
const steps = track()
|
||||
const clockNode = live(el("b", {}))
|
||||
const note = live(el("p", {}))
|
||||
const problem = live(el("div", {}))
|
||||
const txs = txRows()
|
||||
const lead = live(el("span", {}))
|
||||
let current = view
|
||||
const show = (next: OrderState) => {
|
||||
current = next.view
|
||||
const states = stagesOf(next.view)
|
||||
steps.set(states)
|
||||
txs.set(next.view, states, refusedOf(next.view, next.record))
|
||||
lead.replaceChildren(leadOf(next.view, next.record))
|
||||
problem.replaceChildren(...problemOf(next.view, next.record))
|
||||
waitFor(next, signal)
|
||||
}
|
||||
show({ invoice, view, record })
|
||||
onTheSecond(() => waitLeft(current), () => tick(current, record, clockNode, note), signal)
|
||||
const sections = [pageHead(headingOf(view, record), lead, crumbs("Registration", { fromApp: openedFromApp(view, record), prepaid: !invoice })),
|
||||
twoCols(el("div", { class: "panel" }, el("div", { class: "panel-sec" }, steps.node, el("div", { class: "countdown" }, clockNode, note), problem), txs.node),
|
||||
orderCard(labelOf(view, record), view.tld, paidRows(view, invoice)))]
|
||||
return { sections, update: show }
|
||||
}
|
||||
```
|
||||
|
||||
where `track()` returns `{ node, set(states) }`: it builds the three `.stage` elements once (each with a live `.node` span and live `b` title) and `set` assigns `stage.className = \`stage ${st}\`.trim()`, `node.replaceChildren(st === "done" ? "✓" : st === "failed" ? "✕" : n)` only when the text differs, and the title (`Committing`/`Committed`, `Registering`/`Registered`) only when it differs. `txRows()` returns `{ node, set(view, states, refused) }` with live value and chip cells per row, replacing children only when their text differs. `waitFor` starts `fillWait` the first time the phase is `committed` (once per screen). The reveal retry (`revealing`, `revealFailures`) moves into `tick` unchanged.
|
||||
|
||||
- [ ] **Step 5: Stage transitions in `src/styles.css`** (after `.stage.failed .node`):
|
||||
|
||||
```css
|
||||
/* a step turning active or done eases to its colours, its node and line changing in place */
|
||||
.stage::before, .stage .node { transition: var(--moves); }
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Payment and confirming screens** — `buildPayCrypto` keeps references to: the partial-alert slot (`live(el("div", {}))`), amount label, amount, USD line, the three copy fields (each a live value cell), the QR slot (live; replaced only when `paymentUri(...)` differs from the last), the awaiting text (live) and the cancel slot (live). Its `update` recomputes each from the new invoice and replaces only what differs. `buildConfirming` keeps the confirmations line (live) and the slow timer on `signal`.
|
||||
|
||||
- [ ] **Step 7: Run the tests**
|
||||
|
||||
Run: `npx playwright test e2e/inplace.spec.ts e2e/paid.spec.ts e2e/taken.spec.ts e2e/focus.spec.ts --project=chromium`
|
||||
Expected: all passed
|
||||
|
||||
```json:metadata
|
||||
{"files": ["src/views/order.ts", "src/styles.css", "e2e/inplace.spec.ts"], "verifyCommand": "npx playwright test e2e/inplace.spec.ts e2e/paid.spec.ts e2e/taken.spec.ts e2e/focus.spec.ts --project=chromium", "acceptanceCriteria": ["order kinds as listed", "registering update keeps nodes and changes only differing ones", "pay-crypto update in place", "confirming update in place", "stage colours by CSS transition", "e2e registering changes in place passes", "e2e new screen is one page change"], "modelTier": "frontier"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Checkout and name-code screens change in place
|
||||
|
||||
**Goal:** Applying or removing a code, choosing a method, a waiting order and link previews change the checkout's nodes in place; the name-code page's states do too.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/checkout.ts` (`checkoutPage`, `codeEntry`, `ownerCodePage`, `settleWaiting`, `showOpenOrder`; remove `carry(linksSec)`, `linksDraw`)
|
||||
- Modify: `src/views/codes.ts:169` (calls `refresh()` instead of `checkoutPage()` for the checkout)
|
||||
- Test: `e2e/inplace.spec.ts`
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Kind `checkout`: `update(c)` crosses over the rows (`Paying with` ↔ `Code`), the total, the code area, the link slot (`I have a code` ↔ `Remove the code`), the crumbs (`prepaid` changes the steps: the step bar crosses over as one live part) and the Continue button state; the links section (fields being typed, previews loading) is the same node throughout.
|
||||
- [ ] Kind `owner-code`: the "I have saved my code" check and Register button keep their nodes when the order is refused for its method and drawn again.
|
||||
- [ ] Error pages (`price-changed`, `too-many`, `failed`) are `status:<name>` kinds.
|
||||
- [ ] e2e `checkout changes in place`: mark `.summary`, `.panel`, `.link-field`, `h1`; apply a code, remove it; all kept, no transition, no cut, rows read `Code` then `Paying with`.
|
||||
- [ ] `src/screens/checkout.ts`: state, events (code applied/removed, method chosen, waiting order seen/settled, hold started/ended, order refused, price changed), pure `reduce`, `screenOf` (`checkout`, `owner-code`, `status:<name>`), `moves`; the views dispatch events and render from state; `test/screens-checkout.test.ts` covers every `screenOf` branch and every `moves` row.
|
||||
|
||||
**Verify:** `npx playwright test e2e/inplace.spec.ts e2e/checkout.spec.ts e2e/codes.spec.ts --project=chromium` → all passed
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 1: Failing e2e test**
|
||||
|
||||
```ts
|
||||
test("the checkout changes in place: a code applied and removed keeps the card, panel and link fields", async ({ page, request }) => {
|
||||
const { code } = await control(request, "code", { minLength: 6 }) as { code: string }
|
||||
await page.goto(`/#/?name=${uniqueLabel("inco")}`)
|
||||
await page.getByRole("button", { name: "Register" }).click()
|
||||
await page.getByRole("heading", { name: "Check your order" }).waitFor()
|
||||
const kept = [".summary", ".panel", ".link-field", "h1"]
|
||||
await markNodes(page, kept)
|
||||
await takeTransitions(page)
|
||||
await page.getByText("I have a code").click()
|
||||
await page.locator(".code-entry input").fill(code)
|
||||
await expect(page.getByText("Remove the code")).toBeVisible()
|
||||
expect(await page.locator(".summary .r").allInnerTexts()).toContainEqual(expect.stringContaining("Code"))
|
||||
await page.getByText("Remove the code").click()
|
||||
await expect(page.getByText("I have a code")).toBeVisible()
|
||||
expect(await keptNodes(page, kept)).toEqual(kept)
|
||||
expect(await takeTransitions(page)).toEqual([])
|
||||
expect(await takeCuts(page)).toEqual([])
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run, expect FAIL** (`npx playwright test e2e/inplace.spec.ts -g checkout --project=chromium`).
|
||||
|
||||
- [ ] **Step 3: Restructure `checkoutPage()`** into state (`session.checkout()`, waiting record, held flag, applied code) → `screen("search", "checkout", state, buildCheckout)`. `buildCheckout` creates the links section, method radios, card rows (each value a live cell keyed by row label), total (live), code area (live), link slot (live), the crumbs slot (live, `crumbs("Details", ...)` replaced only when `prepaid` changes) and Continue; `update` recomputes rows/total/slots and replaces only differing ones. The code entry's success path calls `refresh()`; removing the code calls `refresh()`.
|
||||
|
||||
- [ ] **Step 4: Owner-code page** as kind `owner-code` with `update` that keeps the check and button and replaces the hold note (live).
|
||||
|
||||
- [ ] **Step 5: Run tests**
|
||||
|
||||
Run: `npx playwright test e2e/inplace.spec.ts e2e/checkout.spec.ts e2e/codes.spec.ts e2e/focus.spec.ts --project=chromium`
|
||||
Expected: all passed
|
||||
|
||||
```json:metadata
|
||||
{"files": ["src/views/checkout.ts", "src/views/codes.ts", "e2e/inplace.spec.ts"], "verifyCommand": "npx playwright test e2e/inplace.spec.ts e2e/checkout.spec.ts e2e/codes.spec.ts e2e/focus.spec.ts --project=chromium", "acceptanceCriteria": ["checkout update in place", "owner-code update in place", "error pages are status kinds", "e2e checkout changes in place passes"], "modelTier": "frontier"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Your names, edit and restore screens change in place
|
||||
|
||||
**Goal:** The Your names list syncs its rows by key (kept, changed, added, removed) instead of being rebuilt; the edit and restore pages' states change in place.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/motion.ts` (add `syncList`)
|
||||
- Modify: `src/views/names.ts` (`historyPage`, `confirmForget`, `pendingRow`, `orderRow`, `codeRow`, `editPage`, `restorePage`)
|
||||
- Test: `e2e/inplace.spec.ts`
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] `syncList(part, rows)` in `motion.ts`: for each new row with `data-key`, an existing child with the same key is kept when `isEqualNode` holds, else replaced by `removeFrom(part, old, row)` (fades across); keys only in the old list leave by `removeFrom(part, old)`; keys only in the new list come in by `addTo(part, row, nextSibling)`; order follows `rows`.
|
||||
- [ ] Kind `names`: `update` calls `syncList` for the Names and Codes lists and crosses over the empty-list notes and the forget area; rows that did not change keep their nodes; the entering rise runs only when the screen is built.
|
||||
- [ ] Kinds `edit`, `edit-unreadable`, `restore`: retry and refusal states cross over in place; `editPage`'s `setTimeout` retry uses the screen signal.
|
||||
- [ ] e2e `Your names changes in place`: with two orders, mark both rows; add a code in another tab (`storage` event); both rows kept, the code row added, no transition, no cut.
|
||||
- [ ] `src/screens/names.ts`: state (records, codes, pending, forget question, edit and restore states), events (storage changed, forget asked/kept/confirmed, edit answer, restore answer), pure `reduce`, `screenOf`, `moves`; `test/screens-names.test.ts` covers every branch and row.
|
||||
|
||||
**Verify:** `npx playwright test e2e/inplace.spec.ts e2e/names.spec.ts e2e/history.spec.ts --project=chromium` → all passed
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 1: Failing e2e test**
|
||||
|
||||
```ts
|
||||
test("Your names changes in place: rows another tab leaves alone keep their nodes", async ({ page, context, request }) => {
|
||||
const codeOf = async () => (await control(request, "code", { minLength: 6 }) as { code: string }).code
|
||||
const [first, second] = [await codeOf(), await codeOf()]
|
||||
const store = (code: string) => localStorage.setItem(`simplex-names:code:${code}`, JSON.stringify({ code, minLength: 6, years: 2, addedAt: new Date().toISOString() }))
|
||||
await page.goto("/#/names")
|
||||
await page.evaluate(store, first)
|
||||
await page.reload()
|
||||
await page.locator(".row-item").first().waitFor()
|
||||
await expect(page.locator(".row-item.entering")).toHaveCount(0)
|
||||
await markNodes(page, [".row-item", "h1"])
|
||||
await takeTransitions(page)
|
||||
const other = await context.newPage()
|
||||
await other.goto("/#/names")
|
||||
await other.evaluate(store, second)
|
||||
await expect(page.locator(".row-item")).toHaveCount(2)
|
||||
expect(await keptNodes(page, [".row-item", "h1"])).toEqual([".row-item", "h1"])
|
||||
expect(await takeTransitions(page)).toEqual([])
|
||||
expect(await takeCuts(page)).toEqual([])
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run, expect FAIL.**
|
||||
|
||||
- [ ] **Step 3: `syncList` in `src/motion.ts`** (after `addTo`):
|
||||
|
||||
```ts
|
||||
// A list drawn again from its rows, each keyed by data-key: a row alike stays as it is, a changed one crosses over in
|
||||
// its place, one gone fades as what follows slides up, and a new one fades in as what follows slides aside.
|
||||
export function syncList(part: HTMLElement, rows: HTMLElement[]): void {
|
||||
const byKey = new Map([...part.children].filter((n) => !n.classList.contains("outgoing")).map((n) => [(n as HTMLElement).dataset.key, n as HTMLElement]))
|
||||
const wanted = new Set(rows.map((r) => r.dataset.key))
|
||||
for (const [key, node] of byKey) if (!wanted.has(key)) removeFrom(part, node)
|
||||
let before: Element | null = part.firstElementChild
|
||||
for (const row of rows) {
|
||||
while (before?.classList.contains("outgoing")) before = before.nextElementSibling
|
||||
const old = byKey.get(row.dataset.key)
|
||||
if (!old) addTo(part, row, before)
|
||||
else if (old.isEqualNode(row)) {
|
||||
if (old !== before) part.insertBefore(old, before)
|
||||
before = old.nextElementSibling
|
||||
continue
|
||||
} else removeFrom(part, old, row)
|
||||
before = row.nextElementSibling
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Your names screen** — `historyPage()` computes `{ orders, codes, pending, canPersist }` and calls `screen("names", "names", state, buildNames)`; `buildNames` keeps the two list parts (live) and the forget area; `update` builds rows (`pendingRow`, `orderRow`, `codeRow`, each with `data-key`) and calls `syncList`. The storage listener calls `refresh()` (batched in one frame as now).
|
||||
|
||||
- [ ] **Step 5: Edit and restore** — `editPage` state `{ registrationId, view | error }` → kinds; `update` crosses over the result line and fields' notes; the retry `setTimeout` is cleared on `signal` abort.
|
||||
|
||||
- [ ] **Step 6: Run tests**
|
||||
|
||||
Run: `npx playwright test e2e/inplace.spec.ts e2e/names.spec.ts e2e/history.spec.ts e2e/focus.spec.ts --project=chromium`
|
||||
Expected: all passed
|
||||
|
||||
```json:metadata
|
||||
{"files": ["src/motion.ts", "src/views/names.ts", "e2e/inplace.spec.ts"], "verifyCommand": "npx playwright test e2e/inplace.spec.ts e2e/names.spec.ts e2e/history.spec.ts e2e/focus.spec.ts --project=chromium", "acceptanceCriteria": ["syncList keeps, crosses, adds and removes rows by key", "names update syncs lists in place", "edit and restore states in place", "e2e Your names changes in place passes"], "modelTier": "frontier"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Search screen changes in place
|
||||
|
||||
**Goal:** A code applied or removed, a retarget banner corrected and a redraw keep the search box, typed name and results nodes.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/search.ts` (`searchPage`, `removeCode`, `checkRetarget`, the applied-code note; remove `shownInput`, `shownResults`, `carry(results)`)
|
||||
- Modify: `src/views/codes.ts:109,179` (keep `refresh()`)
|
||||
- Test: `e2e/inplace.spec.ts`
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Kind `search:<fromApp|retarget|plain>`: `update` crosses over the banner slot and the applied-code note; the input, its typed value, the search button and the results part keep their nodes; a new `name` parameter that differs from the searched one runs the search in place.
|
||||
- [ ] Kind `status:bad-app-link` for the damaged wallet link.
|
||||
- [ ] e2e `search changes in place`: type a name, apply a code from the popup; `.search`, `.search input`, `.results`, `h1` kept, input value unchanged, no transition, no cut.
|
||||
- [ ] `src/screens/search.ts`: state (name, typed, answer, applied code, retarget, app wallet, pause), events (typed, searched, answered, code applied/removed, retarget read), pure `reduce`, `screenOf`, `moves`; `test/screens-search.test.ts` covers every branch and row.
|
||||
|
||||
**Verify:** `npx playwright test e2e/inplace.spec.ts e2e/search.spec.ts e2e/codes.spec.ts --project=chromium` → all passed
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 1: Failing e2e test**
|
||||
|
||||
```ts
|
||||
test("the search changes in place: a code applied from the popup keeps the box, the typed name and the results", async ({ page, request }) => {
|
||||
const { code } = await control(request, "code", { minLength: 6 }) as { code: string }
|
||||
await page.goto("/")
|
||||
await page.getByRole("textbox", { name: "Name", exact: true }).fill("typedname")
|
||||
const kept = [".search", ".search input", ".results", "h1"]
|
||||
await markNodes(page, kept)
|
||||
await takeTransitions(page)
|
||||
await page.getByRole("button", { name: "Check a code" }).click()
|
||||
await page.getByRole("textbox", { name: "Code", exact: true }).fill(code)
|
||||
await page.getByRole("button", { name: "Use this code" }).click()
|
||||
await expect(page.getByText(/^Code applied/)).toBeVisible()
|
||||
expect(await keptNodes(page, kept)).toEqual(kept)
|
||||
await expect(page.getByRole("textbox", { name: "Name", exact: true })).toHaveValue("typedname")
|
||||
expect(await takeTransitions(page)).toEqual([])
|
||||
expect(await takeCuts(page)).toEqual([])
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run, expect FAIL.**
|
||||
|
||||
- [ ] **Step 3: Restructure `searchPage()`** into state `{ name, fromApp, retargetId, applied }` → `screen("search", kind, state, buildSearch)`; `buildSearch` creates hero, input, box, results (live), banner slot (live), applied-note slot (live), bento; `update` updates the banner and applied note, and runs `submit()` when `state.name` differs from the last searched name. `removeCode` calls `refresh()` then focuses the same input (it survives).
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `npx playwright test e2e/inplace.spec.ts e2e/search.spec.ts e2e/codes.spec.ts e2e/focus.spec.ts --project=chromium`
|
||||
Expected: all passed
|
||||
|
||||
```json:metadata
|
||||
{"files": ["src/views/search.ts", "src/views/codes.ts", "e2e/inplace.spec.ts"], "verifyCommand": "npx playwright test e2e/inplace.spec.ts e2e/search.spec.ts e2e/codes.spec.ts e2e/focus.spec.ts --project=chromium", "acceptanceCriteria": ["search update in place keeps box, input, results", "bad app link is a status kind", "e2e search changes in place passes"], "modelTier": "frontier"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Page changes with static names; delete the comparison machinery
|
||||
|
||||
**Goal:** `page()` shows a new screen with one transition and no comparison; every runtime pairing path and its CSS is deleted.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/shell.ts` (`page()`: no `compare`, no identical-redraw path, no `carry`; change type `forward`/`back`/`screen`/`fill`)
|
||||
- Modify: `src/motion.ts` (delete `compare`, `Match`, `Comparison`, `signatures`, `keysOf`, `slotsOf`, `pairsOf`, `freshOf`, `paints`, `leaf`, `empty`, `carry`, `placeCarried`, stand-ins, `MOST_MORPHED`, `kindOf`, `anchorOf`, `WHOLE`, `MOVING`, `changeOnly`, `leaveOver`'s "over" mode, the pair-naming loop in `begin`, `unname`'s pair names; keep `transition`, `transitionsIdle`, `dropWaitingPage`, `finishMotion`, `settled`, transient names, `swapPage` with `"at-once" | "through"`)
|
||||
- Modify: `src/styles.css` (delete `.part`, `.box`, `.swap`, `.gone`, `.fresh`, `.step`, `.tick`, `.name` pair rules and `vt-moving`; add page rules; static names `order-card`)
|
||||
- Modify: `src/views/order.ts`, `src/views/checkout.ts` (`.summary` gets `view-transition-name: order-card` via CSS only), remove `data-whole`
|
||||
- Test: `e2e/motion.spec.ts`, `e2e/cuts.ts`
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] `rg -n "compare\(|pairsOf|freshOf|carry\(|data-whole|part-\$\{|gone-|fresh-|vt-moving" src` returns nothing.
|
||||
- [ ] A page change in Chromium/WebKit runs one view transition whose groups are a subset of `root, page, page-top, page-foot, offline-note, step-ring, step-line-*, step-done-*, order-card, order-name`.
|
||||
- [ ] `page` group: old fades out in `--axis-out-ms`, new fades in over `--axis-in-ms` after it, both sliding `--axis` the way the reader goes for `forward`/`back`; `screen` and `fill` without the slide.
|
||||
- [ ] Firefox: page changes use `swapPage(old, main, ease, "through")`; no `.page-leaving` remains after `PAGE_OUT` time.
|
||||
- [ ] `e2e/motion.spec.ts` updated: tests asserting `part-`/`gone-`/`fresh-` groups now assert the static groups above; in-place assertions moved to `e2e/inplace.spec.ts`.
|
||||
- [ ] Each preserved animation has an e2e check in `e2e/motion.spec.ts` (groups and pseudo animations from `takeTransitions` in Chromium/WebKit): search → checkout has `order-name`; checkout → name code has `order-card`, `page-title`, `crumbs`, `step-ring`, `step-line-name-code` (`fill-across`), `step-done-details` (`tick-in`); forward/back page changes slide `page` (`axis-out`/`axis-in`); the theme change is one root cross-fade.
|
||||
- [ ] `motion.ts` `VIEW_TRANSITIONS` admits Gecko from major version 158 (`FIREFOX_FIXED = 158`, read from the user agent's `Firefox/<n>`), with the bug cited in one short comment; older Gecko and any browser without transition types take the fallback.
|
||||
- [ ] Firefox before 158 (no view transitions) keeps the shared parts' motion by FLIP: on a page change, each element of `order-name`, `order-card`, `crumbs`, `page-title`, `page-panel` present on both pages is hidden in the leaving page and the new one moves (translate, and height for `order-card`/`page-panel`) from the old one's place over `--axis-ms`; the rest fades through. e2e in Firefox: the name's new element starts at the result's place (its first animation's `translate` keyframe equals the old offset) and the card does not fade.
|
||||
- [ ] `src/screens/routes.ts`: moves between views (search, checkout, name code, order, Your names, restore, edit, trademarks) with their motion; `page()` takes the motion from `step()`/`routes.ts` instead of `wayTo()`'s crumb and nav heuristics (browser Back/Forward still give `back`/`forward`); `test/screens-routes.test.ts` covers every row.
|
||||
- [ ] `npx tsc --noEmit -p .` clean; `npm test` passes (77 plus the new screen tests).
|
||||
|
||||
**Verify:** `npx tsc --noEmit -p . && npm test && npx playwright test e2e/motion.spec.ts e2e/inplace.spec.ts --project=chromium` → all passed
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 1: Update `e2e/motion.spec.ts` expectations first** — in "a page opened slides the way the reader goes…", replace the group assertion with
|
||||
|
||||
```ts
|
||||
expect(opened.groups.filter((g) => !/^step-(line|done)-/.test(g)).sort()).toEqual(["page", "page-foot", "page-top", "root"])
|
||||
```
|
||||
|
||||
and delete the `redraws` part (in-place redraws no longer transition; covered in `inplace.spec.ts`). In "the next step of an order…", keep the `step-ring` / `step-line-name-code` / `step-done-details` assertions, replace the root and `gone-` assertions with
|
||||
|
||||
```ts
|
||||
expect(next.groups).toContain("page")
|
||||
expect(next.groups).toContain("order-card")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run, expect FAIL** (`npx playwright test e2e/motion.spec.ts --project=chromium`).
|
||||
|
||||
- [ ] **Step 3: CSS** — replace the view-transition section between `/* Every change of the page is compared…` and the theme rules with:
|
||||
|
||||
```css
|
||||
/* A page change moves only the shared parts, each by its name: the header and footer stay, the step bar's ring goes over,
|
||||
the order's card and name go to their new places; the rest of the page fades through, the way the reader goes. */
|
||||
.summary { view-transition-name: order-card; }
|
||||
body > main:not(.pending) { view-transition-name: page; }
|
||||
::view-transition-group(page), ::view-transition-group(order-card), ::view-transition-group(.name), ::view-transition-group(step-ring), ::view-transition-group(page-foot) { animation-duration: var(--axis-ms); animation-timing-function: var(--emphasized); }
|
||||
::view-transition-old(page) { animation: var(--axis-out-ms) linear both fade-out; }
|
||||
::view-transition-new(page) { animation: var(--axis-in-ms) var(--emphasized) var(--axis-out-ms) both appear; }
|
||||
html:active-view-transition-type(forward, back)::view-transition-old(page) { animation: var(--axis-ms) var(--emphasized) both axis-out, var(--axis-out-ms) linear both fade-out; }
|
||||
html:active-view-transition-type(forward, back)::view-transition-new(page) { animation: var(--axis-ms) var(--emphasized) both axis-in, var(--axis-in-ms) var(--emphasized) var(--axis-out-ms) both appear; }
|
||||
```
|
||||
|
||||
keeping the existing `page-top`/`page-foot`/`offline-note` rules (types now `forward, back, screen, fill`), the `step-*` rules, `.order-name` rules and keyframes.
|
||||
|
||||
- [ ] **Step 4: `page()`** — delete `compare`, `filled`/`arrived`/`unseen` comparison branches that depend on it, the `!comparison.differs` path; `change` is `samePage ? (filled ? "fill" : "screen") : wayTo(...)`; `transition(change, swap, () => finishMotion(old))`; `swap` calls `swapPage(old, main, !scrollTo, transitions() ? "at-once" : "through")`.
|
||||
|
||||
- [ ] **Step 5: `motion.ts`** — delete the listed functions and types; `transition(change, update, prepare)` drops the `comparison` parameter; `begin` becomes: `prepare(); unname(); start the view transition with the update; on finish unname and run the queue`.
|
||||
|
||||
- [ ] **Step 6: Run tests**
|
||||
|
||||
Run: `npx tsc --noEmit -p . && npm test && npx playwright test e2e/motion.spec.ts e2e/inplace.spec.ts --project=chromium`
|
||||
Expected: no type errors, 77 passed, all passed
|
||||
|
||||
```json:metadata
|
||||
{"files": ["src/views/shell.ts", "src/motion.ts", "src/styles.css", "src/views/order.ts", "src/views/checkout.ts", "e2e/motion.spec.ts", "e2e/cuts.ts"], "verifyCommand": "npx tsc --noEmit -p . && npm test && npx playwright test e2e/motion.spec.ts e2e/inplace.spec.ts --project=chromium", "acceptanceCriteria": ["no comparison code remains (rg empty)", "page change groups are static names only", "page group fade-through with axis slide", "Firefox fades through", "motion.spec updated", "tsc clean, unit tests pass"], "modelTier": "frontier"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Verify across browsers, recordings and review
|
||||
|
||||
**Goal:** The whole suite passes in three browsers, frame recordings show no flicker, and review subagents report no issues.
|
||||
|
||||
> **USER-ORDERED GATE — NON-SKIPPABLE.** This task was requested by the user in the current conversation. It MUST NOT be closed by walking around it, by declaring it "verified inline", or by substituting a cheaper check. Close only after every item in `acceptanceCriteria` has been re-validated independently, with output captured.
|
||||
|
||||
**Files:**
|
||||
- Test: whole `e2e/`, scratchpad recordings (`tools/journey/plain-journey.mjs`, `tools/static-spikes.mjs`)
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] `npx playwright test --project=chromium`, `--project=firefox`, `--project=webkit` each report 0 failed (2 offline tests skipped without `BASE_URL`).
|
||||
- [ ] Firefox and Chromium recordings of the `code-order` and `checkout-edits` journeys: `static-spikes.mjs` reports 0 one-frame spikes in header and ground; frame sheets of each in-page change show only the changed parts moving.
|
||||
- [ ] A review round of subagents (code, motion with frame recordings in Chromium and Firefox, design desktop and phone) reports no issues; findings are fixed and the round repeated until it does.
|
||||
|
||||
**Verify:** `for b in chromium firefox webkit; do npx playwright test --project=$b; done` → `0 failed` each
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 1: Full e2e in each browser** (one at a time; Firefox and WebKit with the scratchpad env scripts).
|
||||
- [ ] **Step 2: Recordings** — `node plain-journey.mjs firefox code-order <dir>` and `chromium`, then `node static-spikes.mjs <dir>`; cut and read frame sheets of each change.
|
||||
- [ ] **Step 3: Review round** — dispatch reviewers (no edits to the repos, scratchpad only, no polling waits), fix findings, repeat until none.
|
||||
|
||||
```json:metadata
|
||||
{"files": [], "verifyCommand": "for b in chromium firefox webkit; do npx playwright test --project=$b; done", "acceptanceCriteria": ["0 failed in chromium, firefox, webkit", "0 one-frame spikes in recordings; only changed parts move", "review subagents report no issues"], "modelTier": "frontier", "userGate": true, "tags": ["user-gate"]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: The state machine core
|
||||
|
||||
**Goal:** A pure `src/screens/machine.ts` with `Machine`, `Motion` and `step()`, unit-tested, for every view to run its state through.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/screens/machine.ts`
|
||||
- Test: `test/machine.test.ts`
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] `step(machine, state, shown, event, warn)` returns `{ state, screen, motion, expected }`: `state = machine.reduce(state, event)`, `screen = machine.screenOf(state)`; `shown === null` → motion `"screen"`, expected; `screen === shown` → `"in-place"`, expected; a move listed in `machine.moves[shown][screen]` → that motion, expected; otherwise `warn("[screens] <name>: unexpected move <shown> → <screen>")`, motion `"screen"`, `expected: false`.
|
||||
- [ ] No DOM, clock, storage or network use in `src/screens/` (`rg -n "document|window|Date\.now|localStorage|fetch" src/screens` empty).
|
||||
- [ ] vitest covers the four branches with a small test machine (counter-like states and two screens).
|
||||
|
||||
**Verify:** `npx tsc --noEmit -p . && npx vitest run test/machine.test.ts` → passed
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 1: Failing test** `test/machine.test.ts`:
|
||||
|
||||
```ts
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { type Machine, step } from "../src/screens/machine.ts"
|
||||
|
||||
type S = { n: number }
|
||||
type E = { kind: "add"; by: number }
|
||||
type K = "low" | "high" | "odd"
|
||||
const machine: Machine<S, E, K> = {
|
||||
name: "test",
|
||||
reduce: (s, e) => ({ n: s.n + e.by }),
|
||||
screenOf: (s) => (s.n % 2 ? "odd" : s.n < 10 ? "low" : "high"),
|
||||
moves: { low: { high: "forward" }, high: { low: "back" }, odd: {} },
|
||||
}
|
||||
|
||||
describe("step", () => {
|
||||
it("draws the first screen as a page change", () => {
|
||||
expect(step(machine, { n: 0 }, null, { kind: "add", by: 2 }, vi.fn())).toEqual({ state: { n: 2 }, screen: "low", motion: "screen", expected: true })
|
||||
})
|
||||
it("keeps a screen of the same kind in place", () => {
|
||||
expect(step(machine, { n: 2 }, "low", { kind: "add", by: 2 }, vi.fn()).motion).toBe("in-place")
|
||||
})
|
||||
it("takes a listed move's motion", () => {
|
||||
expect(step(machine, { n: 8 }, "low", { kind: "add", by: 2 }, vi.fn())).toMatchObject({ screen: "high", motion: "forward", expected: true })
|
||||
})
|
||||
it("logs a move missing from the table and still shows it as a page change", () => {
|
||||
const warn = vi.fn()
|
||||
expect(step(machine, { n: 2 }, "low", { kind: "add", by: 1 }, warn)).toMatchObject({ screen: "odd", motion: "screen", expected: false })
|
||||
expect(warn).toHaveBeenCalledWith("[screens] test: unexpected move low → odd")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run, expect FAIL** (`npx vitest run test/machine.test.ts`: module not found).
|
||||
|
||||
- [ ] **Step 3: Implement** `src/screens/machine.ts`:
|
||||
|
||||
```ts
|
||||
// A view's behaviour as data: its state changes only by events through reduce, the screen shown follows from the state
|
||||
// alone, and each move from one screen to another has its motion; a move the table lacks is logged, and still shown.
|
||||
export type Motion = "in-place" | "forward" | "back" | "screen"
|
||||
export interface Machine<S, E, K extends string> {
|
||||
name: string
|
||||
reduce: (state: S, event: E) => S
|
||||
screenOf: (state: S) => K
|
||||
moves: { readonly [F in K]: Partial<Record<K, Exclude<Motion, "in-place">>> }
|
||||
}
|
||||
export interface Step<S, K> { state: S; screen: K; motion: Motion; expected: boolean }
|
||||
|
||||
export function step<S, E, K extends string>(machine: Machine<S, E, K>, state: S, shown: K | null, event: E, warn: (message: string) => void = console.warn): Step<S, K> {
|
||||
const next = machine.reduce(state, event)
|
||||
const screen = machine.screenOf(next)
|
||||
if (shown === null) return { state: next, screen, motion: "screen", expected: true }
|
||||
if (screen === shown) return { state: next, screen, motion: "in-place", expected: true }
|
||||
const motion = machine.moves[shown][screen]
|
||||
if (motion) return { state: next, screen, motion, expected: true }
|
||||
warn(`[screens] ${machine.name}: unexpected move ${shown} → ${screen}`)
|
||||
return { state: next, screen, motion: "screen", expected: false }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run, expect PASS**; `npx tsc --noEmit -p .` clean.
|
||||
|
||||
```json:metadata
|
||||
{"files": ["src/screens/machine.ts", "test/machine.test.ts"], "verifyCommand": "npx tsc --noEmit -p . && npx vitest run test/machine.test.ts", "acceptanceCriteria": ["step() covers first draw, in place, listed move, unexpected move with warn", "src/screens has no DOM/clock/storage/network use", "vitest covers the four branches"], "modelTier": "mechanical"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: The order page as a state machine
|
||||
|
||||
**Goal:** The order page's state lives in one object changed by events through a pure reducer in `src/screens/order.ts`; its screen comes from a pure `screenOf`, its moves from a table, all unit-tested; `order.ts` becomes the controller (polls, timers, API calls, storage) that dispatches events and renders.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/screens/order.ts`
|
||||
- Modify: `src/views/order.ts` (follow loop, timers and actions dispatch events; `kindOf` replaced by `screenOf`; screens render from state)
|
||||
- Test: `test/screens-order.test.ts`
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] `OrderState` holds everything the screens read: invoice, registration view, record, card submitted time, reveal attempts and refusals, action notes, hold, `now` (service time of the last event).
|
||||
- [ ] Events: `answered` (invoice and/or view with service time), `record-changed`, `tick` (service time), `card-submitted`, `card-failed`, `reveal-sent`, `reveal-refused`, `reveal-failed`, `cancel-started`, `cancel-answered`, `cancel-failed`, `reorder-started`, `reorder-failed`, `hold-started`, `hold-ended`; reducer pure (no clock, storage, network, DOM).
|
||||
- [ ] `screenOf` returns every kind of Task 2 and nothing else; `moves` lists the order lifecycle (pay-crypto → confirming → registering → registered | taken | failed; pay-crypto → expired | still-open | expired-cancelled; pay-card → confirming | card-failed | slow; confirming → slow | registering; registering → registered | taken | failed | not-on-device; etc.) with motion `screen`.
|
||||
- [ ] `test/screens-order.test.ts`: one test per `screenOf` branch (state → kind), one per `moves` row (a state of the from-screen and an event leading to the to-screen: `step()` returns the row's motion, `expected: true`), the reducer's handling of each event, and a race test: two `answered` events applied in either order (older service time second) end in the same state (an older answer never replaces a newer one).
|
||||
- [ ] The e2e of Task 2 still pass.
|
||||
|
||||
**Verify:** `npx tsc --noEmit -p . && npm test && npx playwright test e2e/inplace.spec.ts e2e/paid.spec.ts e2e/taken.spec.ts --project=chromium` → all passed
|
||||
|
||||
```json:metadata
|
||||
{"files": ["src/screens/order.ts", "src/views/order.ts", "test/screens-order.test.ts"], "verifyCommand": "npx tsc --noEmit -p . && npm test && npx playwright test e2e/inplace.spec.ts e2e/paid.spec.ts e2e/taken.spec.ts --project=chromium", "acceptanceCriteria": ["OrderState holds all screen inputs", "events as listed, reducer pure", "screenOf returns Task 2 kinds; moves list the lifecycle", "vitest covers every branch, row, event and the answer race", "Task 2 e2e still pass"], "modelTier": "frontier"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-review
|
||||
|
||||
- Spec coverage: in-place updates per view (Tasks 2–5), page transitions with static names and machinery removal (Task 6), verification (Task 7), primitive (Task 1).
|
||||
- Types: `Built<S>`, `screen()`, `syncList()`, `every(ms, f, signal)`, `onTheSecond(left, f, signal)` are defined in Tasks 1 and 4 and used with the same signatures later.
|
||||
- Order: Task 6 depends on Tasks 2–5 (no view may still rely on `compare`).
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"planPath": "plans/names-codes/2026-10-17-webapp-in-place-updates.md",
|
||||
"tasks": [
|
||||
{
|
||||
"id": 0,
|
||||
"subject": "Task 1: The screen primitive",
|
||||
"status": "completed",
|
||||
"description": "**Goal:** `screen()` in `shell.ts` builds a screen once and routes later states of the same kind to its `update`, with a per-screen `AbortSignal` for its timers.\n\n**Files:**\n- Modify: `src/views/shell.ts` (add `screen`, `Built`, the shown-screen state; reset it in `nextRoute` when the route is not a redraw)\n- Modify: `src/views/order.ts:27-38` (`every()` takes a signal instead of pushing to `cleanups`)\n- Test: `e2e/inplace.spec.ts` (new; first test drives Task 2, so this task's test is the harness helper)\n\n**Acceptance Criteria:**\n- [ ] `screen(active, kind, state, build)` calls `build(state, signal)` and `page()` when no screen of `kind` is shown on this route family; otherwise calls the shown screen's `update(state)` and nothing else.\n- [ ] A screen without `update` is rebuilt (`page()`) when its kind is called again with a state whose `JSON.stringify` differs from the shown one; with an equal state it is left alone.\n- [ ] Leaving the route (`nextRoute` without `redrawing`) aborts the shown screen's signal and forgets it; a `refresh()` keeps it.\n- [ ] `e2e/inplace.spec.ts` exists with helpers `markNodes(page, selectors)` (sets `__kept = true` on each matched node) and `keptNodes(page, selectors)` (returns which still carry it), and `takeTransitions` from `e2e/cuts.ts`.\n- [ ] `npx tsc --noEmit -p .` clean.\n\n**Verify:** `npx tsc --noEmit -p . && npm test` \u2192 no errors, `77 passed`\n\n**Steps:**\n\n- [ ] **Step 1: Add the primitive to `src/views/shell.ts`** (after `refresh()`):\n\n```ts\n// A screen of a view is built once and then told each new state of what it shows, changing only what differs; a state\n// of another kind is another screen, shown as a page change. What a screen runs, as a countdown, stops with it.\nexport interface Built<S> { sections: Node[]; update?: (state: S) => void }\ninterface Shown { kind: string; state: string; update?: (state: unknown) => void; stop: AbortController }\nlet shown: Shown | null = null\n\nexport function screen<S>(active: Active, kind: string, state: S, build: (state: S, signal: AbortSignal) => Built<S>): void {\n const same = shown !== null && shown.kind === kind && isRedraw() && document.querySelector(SHOWN_PAGE) !== null\n if (same && shown!.update) return shown!.update(state)\n if (same && shown!.state === JSON.stringify(state)) return\n shown?.stop.abort()\n const stop = new AbortController()\n const built = build(state, stop.signal)\n shown = { kind, state: JSON.stringify(state), update: built.update as Shown[\"update\"], stop }\n page(active, built.sections)\n}\n```\n\nIn `nextRoute()`, where `focus.drop()` runs for a new page, also run:\n\n```ts\n shown?.stop.abort()\n shown = null\n```\n\n`state` passed to screens must be JSON-serialisable data (API views, records, flags), never DOM nodes or functions.\n\n- [ ] **Step 2: Signal-owned timers in `src/views/order.ts`**: replace `cleanups`/`cleanUp`/`every` with\n\n```ts\nfunction every(ms: number, f: () => void, signal: AbortSignal): void {\n const id = setInterval(f, ms)\n signal.addEventListener(\"abort\", () => clearInterval(id), { once: true })\n}\n```\n\nand give `onTheSecond(left, f, signal)` the same signal (clear its timeout on abort). `cleanups.push(card.destroy)` becomes `signal.addEventListener(\"abort\", card.destroy, { once: true })`.\n\n- [ ] **Step 3: Create `e2e/inplace.spec.ts` helpers**\n\n```ts\nimport { expect, test, type Page } from \"@playwright/test\"\nimport { recordCuts, takeCuts, takeTransitions } from \"./cuts.ts\"\n\ntest.use({ reducedMotion: \"no-preference\" })\ntest.beforeEach(async ({ page }) => { await page.addInitScript(recordCuts) })\n\n// marks the nodes a state change must keep, to tell afterwards which are still the same nodes\nexport const markNodes = (page: Page, selectors: string[]) => page.evaluate((list) => {\n for (const s of list) for (const n of document.querySelectorAll(\"body > main \" + s)) (n as unknown as { __kept: boolean }).__kept = true\n}, selectors)\nexport const keptNodes = (page: Page, selectors: string[]) => page.evaluate((list) => list.filter((s) => {\n const n = document.querySelector(\"body > main \" + s) as unknown as { __kept?: boolean } | null\n return n?.__kept === true\n}), selectors)\n```\n\n- [ ] **Step 4: Type-check and unit tests**\n\nRun: `npx tsc --noEmit -p . && npm test`\nExpected: no type errors, `Tests 77 passed`\n\n```json:metadata\n{\"files\": [\"src/views/shell.ts\", \"src/views/order.ts\", \"e2e/inplace.spec.ts\"], \"verifyCommand\": \"npx tsc --noEmit -p . && npm test\", \"acceptanceCriteria\": [\"screen() builds once and routes same-kind states to update\", \"screen without update rebuilds only on a changed state\", \"leaving the route aborts the screen signal; refresh keeps it\", \"inplace.spec.ts helpers exist\", \"tsc clean\"], \"modelTier\": \"frontier\"}\n```"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"subject": "Task 2: Order page as screens, registering changes in place",
|
||||
"status": "in_progress",
|
||||
"description": "**Goal:** The order page's `follow()` loop calls `screen()` with a kind per screen; the registering screen's phases (committing \u2192 committed \u2192 wait \u2192 registering), its countdown, transactions and problem note change in place.\n\n**Files:**\n- Modify: `src/views/order.ts` (`draw`, `payment`, `confirming`, `registering`, `track`, `txRows`, `registered`, `taken`, `codeIssued`, `failed`, status screens; remove `carry(note)` uses)\n- Modify: `src/styles.css` (`.stage` / `.node` state transitions)\n- Test: `e2e/inplace.spec.ts`\n\n**Acceptance Criteria:**\n- [ ] Kinds: `pay-crypto`, `pay-card`, `confirming`, `slow`, `registering`, `registered`, `taken`, `code-issued`, `failed`, `not-on-device`, and `status:<name>` for each status page (`paid-late`, `paid-by-invoice`, `still-open`, `card-failed`, `expired-cancelled`, `expired-received`, `expired`, `price-changed`, `unreadable`, `unknown-link`).\n- [ ] `registering` update, for a new `RegistrationView` of the same order: each `.stage` gets its new state class (`done`/`active`/`\"\"`), its node text crosses over (live), its title crosses over (live); the wait line fill starts on entering `committed`; the countdown clock and note keep their live behaviour; each tx row's value and chip cross over (live); the problem slot crosses over; heading/lead/crumbs/card are untouched unless their text changes (then live).\n- [ ] `pay-crypto` update: partial-payment alert slot, amount label and amount, copy fields, QR (only when the URI changed), \"Waiting for (the rest of) payment\" and the cancel link slot cross over in place; the rate countdown restarts only if `expiresAt` changed.\n- [ ] `confirming` update: the confirmations line crosses over; the slow timer keeps running from the same `cardSubmittedAt`.\n- [ ] Stage colours change by CSS transition (`transition: background-color, border-color, color, box-shadow` on `.stage .node` and the line drawn by `.stage::before`), not by a view transition.\n- [ ] e2e `registering changes in place`: on the code-order journey, nodes `.track`, `.stage:nth-child(1)`, `.countdown`, `.txs`, `.summary`, `h1` are marked during committing; after the commit is mined and again after the wait ends, all are still kept, `takeTransitions` is `[]`, `takeCuts` is `[]`, and the stage classes are `done active \"\"` then `done done active`.\n- [ ] e2e `a new screen is a page change`: registering \u2192 registered runs exactly one transition (or, in Firefox, `.page-leaving` appears once) and the heading takes focus.\n\n**Verify:** `npx playwright test e2e/inplace.spec.ts e2e/paid.spec.ts e2e/taken.spec.ts --project=chromium` \u2192 all passed\n\n**Steps:**\n\n- [ ] **Step 1: Write the failing e2e test** in `e2e/inplace.spec.ts`:\n\n```ts\nimport { control, REGISTRATION_MS, uniqueLabel } from \"./helpers.ts\"\ntest(\"registering changes in place: the steps, countdown and transactions keep their nodes through each phase\", async ({ page, request }) => {\n const { code } = await control(request, \"code\", { minLength: 6 }) as { code: string }\n await page.goto(\"/\")\n await page.getByRole(\"button\", { name: \"Check a code\" }).click()\n await page.getByRole(\"textbox\", { name: \"Code\", exact: true }).fill(code)\n await page.getByRole(\"button\", { name: \"Use this code\" }).click()\n await page.getByRole(\"textbox\", { name: \"Name\", exact: true }).fill(uniqueLabel(\"inplace\"))\n await page.getByRole(\"textbox\", { name: \"Name\", exact: true }).press(\"Enter\")\n await page.getByRole(\"button\", { name: \"Register with code\" }).click()\n await page.getByRole(\"button\", { name: \"Continue\" }).click()\n await page.getByText(\"I have saved my code\").click()\n await page.getByRole(\"button\", { name: \"Register\", exact: true }).click()\n await page.locator(\".stage.active\", { hasText: \"Committing\" }).waitFor({ timeout: REGISTRATION_MS })\n const kept = [\".track\", \".stage:nth-child(1)\", \".countdown\", \".txs\", \".summary\", \"h1\"]\n await markNodes(page, kept)\n await takeTransitions(page)\n await page.locator(\".stage.active\", { hasText: \"Waiting\" }).waitFor({ timeout: REGISTRATION_MS })\n expect(await keptNodes(page, kept)).toEqual(kept)\n expect(await page.evaluate(() => [...document.querySelectorAll(\"body > main .stage\")].map((s) => s.className))).toEqual([\"stage done\", \"stage active\", \"stage\"])\n await page.locator(\".stage.active\", { hasText: \"Registering\" }).waitFor({ timeout: REGISTRATION_MS })\n expect(await keptNodes(page, kept)).toEqual(kept)\n expect(await takeTransitions(page)).toEqual([])\n expect(await takeCuts(page)).toEqual([])\n})\n```\n\n- [ ] **Step 2: Run it to verify it fails**\n\nRun: `npx playwright test e2e/inplace.spec.ts --project=chromium`\nExpected: FAIL at the first `keptNodes` (nodes were rebuilt).\n\n- [ ] **Step 3: Kinds in `draw()`**: `draw(invoice, view, record)` builds the state `{ invoice, view, record }` and calls `screen(null, kindOf(Line truncated
|
||||
"blockedBy": [
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"subject": "Task 3: Checkout and name-code screens change in place",
|
||||
"status": "pending",
|
||||
"description": "**Goal:** Applying or removing a code, choosing a method, a waiting order and link previews change the checkout's nodes in place; the name-code page's states do too.\n\n**Files:**\n- Modify: `src/views/checkout.ts` (`checkoutPage`, `codeEntry`, `ownerCodePage`, `settleWaiting`, `showOpenOrder`; remove `carry(linksSec)`, `linksDraw`)\n- Modify: `src/views/codes.ts:169` (calls `refresh()` instead of `checkoutPage()` for the checkout)\n- Test: `e2e/inplace.spec.ts`\n\n**Acceptance Criteria:**\n- [ ] Kind `checkout`: `update(c)` crosses over the rows (`Paying with` \u2194 `Code`), the total, the code area, the link slot (`I have a code` \u2194 `Remove the code`), the crumbs (`prepaid` changes the steps: the step bar crosses over as one live part) and the Continue button state; the links section (fields being typed, previews loading) is the same node throughout.\n- [ ] Kind `owner-code`: the \"I have saved my code\" check and Register button keep their nodes when the order is refused for its method and drawn again.\n- [ ] Error pages (`price-changed`, `too-many`, `failed`) are `status:<name>` kinds.\n- [ ] e2e `checkout changes in place`: mark `.summary`, `.panel`, `.link-field`, `h1`; apply a code, remove it; all kept, no transition, no cut, rows read `Code` then `Paying with`.\n- [ ] `src/screens/checkout.ts`: state, events (code applied/removed, method chosen, waiting order seen/settled, hold started/ended, order refused, price changed), pure `reduce`, `screenOf` (`checkout`, `owner-code`, `status:<name>`), `moves`; the views dispatch events and render from state; `test/screens-checkout.test.ts` covers every `screenOf` branch and every `moves` row.\n\n**Verify:** `npx playwright test e2e/inplace.spec.ts e2e/checkout.spec.ts e2e/codes.spec.ts --project=chromium` \u2192 all passed\n\n**Steps:**\n\n- [ ] **Step 1: Failing e2e test**\n\n```ts\ntest(\"the checkout changes in place: a code applied and removed keeps the card, panel and link fields\", async ({ page, request }) => {\n const { code } = await control(request, \"code\", { minLength: 6 }) as { code: string }\n await page.goto(`/#/?name=${uniqueLabel(\"inco\")}`)\n await page.getByRole(\"button\", { name: \"Register\" }).click()\n await page.getByRole(\"heading\", { name: \"Check your order\" }).waitFor()\n const kept = [\".summary\", \".panel\", \".link-field\", \"h1\"]\n await markNodes(page, kept)\n await takeTransitions(page)\n await page.getByText(\"I have a code\").click()\n await page.locator(\".code-entry input\").fill(code)\n await expect(page.getByText(\"Remove the code\")).toBeVisible()\n expect(await page.locator(\".summary .r\").allInnerTexts()).toContainEqual(expect.stringContaining(\"Code\"))\n await page.getByText(\"Remove the code\").click()\n await expect(page.getByText(\"I have a code\")).toBeVisible()\n expect(await keptNodes(page, kept)).toEqual(kept)\n expect(await takeTransitions(page)).toEqual([])\n expect(await takeCuts(page)).toEqual([])\n})\n```\n\n- [ ] **Step 2: Run, expect FAIL** (`npx playwright test e2e/inplace.spec.ts -g checkout --project=chromium`).\n\n- [ ] **Step 3: Restructure `checkoutPage()`** into state (`session.checkout()`, waiting record, held flag, applied code) \u2192 `screen(\"search\", \"checkout\", state, buildCheckout)`. `buildCheckout` creates the links section, method radios, card rows (each value a live cell keyed by row label), total (live), code area (live), link slot (live), the crumbs slot (live, `crumbs(\"Details\", ...)` replaced only when `prepaid` changes) and Continue; `update` recomputes rows/total/slots and replaces only differing ones. The code entry's success path calls `refresh()`; removing the code calls `refresh()`.\n\n- [ ] **Step 4: Owner-code page** as kind `owner-code` with `update` that keeps the check and button and replaces the hold note (live).\n\n- [ ] **Step 5: Run tests**\n\nRun: `npx playwright test e2e/inplace.spec.ts e2e/checkout.spec.ts e2e/codes.spec.ts e2e/focus.spec.ts --project=chromium`\nExpected: all passed\n\n```json:metadata\n{\"files\": [\"src/views/checkout.ts\", \"src/views/codes.ts\", \"e2e/inplace.spec.ts\"], \"verifyCommand\": \"npx playwright test e2e/inplace.spec.ts e2e/checkout.spec.ts e2e/codes.spec.ts e2e/focus.spec.ts --project=chromium\", \"acceptanceCriteria\": [\"checkout update in place\", \"owner-code update in place\", \"error pages are status kinds\", \"e2e checkout changes in place passes\"], \"modelTier\": \"frontier\"}\n```",
|
||||
"blockedBy": [
|
||||
0,
|
||||
7,
|
||||
8
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"subject": "Task 4: Your names, edit and restore screens change in place",
|
||||
"status": "pending",
|
||||
"description": "**Goal:** The Your names list syncs its rows by key (kept, changed, added, removed) instead of being rebuilt; the edit and restore pages' states change in place.\n\n**Files:**\n- Modify: `src/motion.ts` (add `syncList`)\n- Modify: `src/views/names.ts` (`historyPage`, `confirmForget`, `pendingRow`, `orderRow`, `codeRow`, `editPage`, `restorePage`)\n- Test: `e2e/inplace.spec.ts`\n\n**Acceptance Criteria:**\n- [ ] `syncList(part, rows)` in `motion.ts`: for each new row with `data-key`, an existing child with the same key is kept when `isEqualNode` holds, else replaced by `removeFrom(part, old, row)` (fades across); keys only in the old list leave by `removeFrom(part, old)`; keys only in the new list come in by `addTo(part, row, nextSibling)`; order follows `rows`.\n- [ ] Kind `names`: `update` calls `syncList` for the Names and Codes lists and crosses over the empty-list notes and the forget area; rows that did not change keep their nodes; the entering rise runs only when the screen is built.\n- [ ] Kinds `edit`, `edit-unreadable`, `restore`: retry and refusal states cross over in place; `editPage`'s `setTimeout` retry uses the screen signal.\n- [ ] e2e `Your names changes in place`: with two orders, mark both rows; add a code in another tab (`storage` event); both rows kept, the code row added, no transition, no cut.\n- [ ] `src/screens/names.ts`: state (records, codes, pending, forget question, edit and restore states), events (storage changed, forget asked/kept/confirmed, edit answer, restore answer), pure `reduce`, `screenOf`, `moves`; `test/screens-names.test.ts` covers every branch and row.\n\n**Verify:** `npx playwright test e2e/inplace.spec.ts e2e/names.spec.ts e2e/history.spec.ts --project=chromium` \u2192 all passed\n\n**Steps:**\n\n- [ ] **Step 1: Failing e2e test**\n\n```ts\ntest(\"Your names changes in place: rows another tab leaves alone keep their nodes\", async ({ page, context, request }) => {\n const codeOf = async () => (await control(request, \"code\", { minLength: 6 }) as { code: string }).code\n const [first, second] = [await codeOf(), await codeOf()]\n const store = (code: string) => localStorage.setItem(`simplex-names:code:${code}`, JSON.stringify({ code, minLength: 6, years: 2, addedAt: new Date().toISOString() }))\n await page.goto(\"/#/names\")\n await page.evaluate(store, first)\n await page.reload()\n await page.locator(\".row-item\").first().waitFor()\n await expect(page.locator(\".row-item.entering\")).toHaveCount(0)\n await markNodes(page, [\".row-item\", \"h1\"])\n await takeTransitions(page)\n const other = await context.newPage()\n await other.goto(\"/#/names\")\n await other.evaluate(store, second)\n await expect(page.locator(\".row-item\")).toHaveCount(2)\n expect(await keptNodes(page, [\".row-item\", \"h1\"])).toEqual([\".row-item\", \"h1\"])\n expect(await takeTransitions(page)).toEqual([])\n expect(await takeCuts(page)).toEqual([])\n})\n```\n\n- [ ] **Step 2: Run, expect FAIL.**\n\n- [ ] **Step 3: `syncList` in `src/motion.ts`** (after `addTo`):\n\n```ts\n// A list drawn again from its rows, each keyed by data-key: a row alike stays as it is, a changed one crosses over in\n// its place, one gone fades as what follows slides up, and a new one fades in as what follows slides aside.\nexport function syncList(part: HTMLElement, rows: HTMLElement[]): void {\n const byKey = new Map([...part.children].filter((n) => !n.classList.contains(\"outgoing\")).map((n) => [(n as HTMLElement).dataset.key, n as HTMLElement]))\n const wanted = new Set(rows.map((r) => r.dataset.key))\n for (const [key, node] of byKey) if (!wanted.has(key)) removeFrom(part, node)\n let before: Element | null = part.firstElementChild\n for (const row of rows) {\n while (before?.classList.contains(\"outgoing\")) before = before.nextElementSibling\n const old = byKey.get(row.dataset.key)\n if (!old) addTo(part, row, before)\n else if (old.isEqualNode(row)) {\n if (old !== before) part.insertBefore(old, before)\n before = old.nextElementSibling\n continue\n } else removeFrom(part, old, row)\n before = row.nextElementSibling\n }\n}\n```\n\n- [ ] **Step 4: Your names screen** \u2014 `historyPage()` computes `{ orders, codes, pending, canPersist }` and calls `screen(\"names\", \"names\", state, buildNames)`; `buildNames` keeps the two list parts (live) and the forget area; `update` builds rows (`pendingRow`, `orderRow`, `codeRow`, each with `data-key`) and calls `syncList`. The storage listener calls `refresh()` (batched in one frame as now).\n\n- [ ] **Step 5: Edit and restore** \u2014 `editPage` state `{ registrationId, view | error }` \u2192 kinds; `update` crosses over the result line and fields' notes; the retry `setTimeout` is cleared on `signal` abort.\n\n- [ ] **Step 6: Run tests**\n\nRun: `npx playwright test e2e/inplace.spec.ts e2e/names.spec.ts e2e/history.spec.ts e2e/focus.spec.ts --project=chromium`\nExpected: all passed\n\n```jLine truncated
|
||||
"blockedBy": [
|
||||
0,
|
||||
7,
|
||||
8
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"subject": "Task 5: Search screen changes in place",
|
||||
"status": "pending",
|
||||
"description": "**Goal:** A code applied or removed, a retarget banner corrected and a redraw keep the search box, typed name and results nodes.\n\n**Files:**\n- Modify: `src/views/search.ts` (`searchPage`, `removeCode`, `checkRetarget`, the applied-code note; remove `shownInput`, `shownResults`, `carry(results)`)\n- Modify: `src/views/codes.ts:109,179` (keep `refresh()`)\n- Test: `e2e/inplace.spec.ts`\n\n**Acceptance Criteria:**\n- [ ] Kind `search:<fromApp|retarget|plain>`: `update` crosses over the banner slot and the applied-code note; the input, its typed value, the search button and the results part keep their nodes; a new `name` parameter that differs from the searched one runs the search in place.\n- [ ] Kind `status:bad-app-link` for the damaged wallet link.\n- [ ] e2e `search changes in place`: type a name, apply a code from the popup; `.search`, `.search input`, `.results`, `h1` kept, input value unchanged, no transition, no cut.\n- [ ] `src/screens/search.ts`: state (name, typed, answer, applied code, retarget, app wallet, pause), events (typed, searched, answered, code applied/removed, retarget read), pure `reduce`, `screenOf`, `moves`; `test/screens-search.test.ts` covers every branch and row.\n\n**Verify:** `npx playwright test e2e/inplace.spec.ts e2e/search.spec.ts e2e/codes.spec.ts --project=chromium` \u2192 all passed\n\n**Steps:**\n\n- [ ] **Step 1: Failing e2e test**\n\n```ts\ntest(\"the search changes in place: a code applied from the popup keeps the box, the typed name and the results\", async ({ page, request }) => {\n const { code } = await control(request, \"code\", { minLength: 6 }) as { code: string }\n await page.goto(\"/\")\n await page.getByRole(\"textbox\", { name: \"Name\", exact: true }).fill(\"typedname\")\n const kept = [\".search\", \".search input\", \".results\", \"h1\"]\n await markNodes(page, kept)\n await takeTransitions(page)\n await page.getByRole(\"button\", { name: \"Check a code\" }).click()\n await page.getByRole(\"textbox\", { name: \"Code\", exact: true }).fill(code)\n await page.getByRole(\"button\", { name: \"Use this code\" }).click()\n await expect(page.getByText(/^Code applied/)).toBeVisible()\n expect(await keptNodes(page, kept)).toEqual(kept)\n await expect(page.getByRole(\"textbox\", { name: \"Name\", exact: true })).toHaveValue(\"typedname\")\n expect(await takeTransitions(page)).toEqual([])\n expect(await takeCuts(page)).toEqual([])\n})\n```\n\n- [ ] **Step 2: Run, expect FAIL.**\n\n- [ ] **Step 3: Restructure `searchPage()`** into state `{ name, fromApp, retargetId, applied }` \u2192 `screen(\"search\", kind, state, buildSearch)`; `buildSearch` creates hero, input, box, results (live), banner slot (live), applied-note slot (live), bento; `update` updates the banner and applied note, and runs `submit()` when `state.name` differs from the last searched name. `removeCode` calls `refresh()` then focuses the same input (it survives).\n\n- [ ] **Step 4: Run tests**\n\nRun: `npx playwright test e2e/inplace.spec.ts e2e/search.spec.ts e2e/codes.spec.ts e2e/focus.spec.ts --project=chromium`\nExpected: all passed\n\n```json:metadata\n{\"files\": [\"src/views/search.ts\", \"src/views/codes.ts\", \"e2e/inplace.spec.ts\"], \"verifyCommand\": \"npx playwright test e2e/inplace.spec.ts e2e/search.spec.ts e2e/codes.spec.ts e2e/focus.spec.ts --project=chromium\", \"acceptanceCriteria\": [\"search update in place keeps box, input, results\", \"bad app link is a status kind\", \"e2e search changes in place passes\"], \"modelTier\": \"frontier\"}\n```",
|
||||
"blockedBy": [
|
||||
0,
|
||||
7,
|
||||
8
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"subject": "Task 6: Page changes with static names; delete the comparison machinery",
|
||||
"status": "pending",
|
||||
"description": "**Goal:** `page()` shows a new screen with one transition and no comparison; every runtime pairing path and its CSS is deleted.\n\n**Files:**\n- Modify: `src/views/shell.ts` (`page()`: no `compare`, no identical-redraw path, no `carry`; change type `forward`/`back`/`screen`/`fill`)\n- Modify: `src/motion.ts` (delete `compare`, `Match`, `Comparison`, `signatures`, `keysOf`, `slotsOf`, `pairsOf`, `freshOf`, `paints`, `leaf`, `empty`, `carry`, `placeCarried`, stand-ins, `MOST_MORPHED`, `kindOf`, `anchorOf`, `WHOLE`, `MOVING`, `changeOnly`, `leaveOver`'s \"over\" mode, the pair-naming loop in `begin`, `unname`'s pair names; keep `transition`, `transitionsIdle`, `dropWaitingPage`, `finishMotion`, `settled`, transient names, `swapPage` with `\"at-once\" | \"through\"`)\n- Modify: `src/styles.css` (delete `.part`, `.box`, `.swap`, `.gone`, `.fresh`, `.step`, `.tick`, `.name` pair rules and `vt-moving`; add page rules; static names `order-card`)\n- Modify: `src/views/order.ts`, `src/views/checkout.ts` (`.summary` gets `view-transition-name: order-card` via CSS only), remove `data-whole`\n- Test: `e2e/motion.spec.ts`, `e2e/cuts.ts`\n\n**Acceptance Criteria:**\n- [ ] `rg -n \"compare\\(|pairsOf|freshOf|carry\\(|data-whole|part-\\$\\{|gone-|fresh-|vt-moving\" src` returns nothing.\n- [ ] A page change in Chromium/WebKit runs one view transition whose groups are a subset of `root, page, page-top, page-foot, offline-note, step-ring, step-line-*, step-done-*, order-card, order-name`.\n- [ ] `page` group: old fades out in `--axis-out-ms`, new fades in over `--axis-in-ms` after it, both sliding `--axis` the way the reader goes for `forward`/`back`; `screen` and `fill` without the slide.\n- [ ] Firefox: page changes use `swapPage(old, main, ease, \"through\")`; no `.page-leaving` remains after `PAGE_OUT` time.\n- [ ] `e2e/motion.spec.ts` updated: tests asserting `part-`/`gone-`/`fresh-` groups now assert the static groups above; in-place assertions moved to `e2e/inplace.spec.ts`.\n- [ ] `src/screens/routes.ts`: moves between views (search, checkout, name code, order, Your names, restore, edit, trademarks) with their motion; `page()` takes the motion from `step()`/`routes.ts` instead of `wayTo()`'s crumb and nav heuristics (browser Back/Forward still give `back`/`forward`); `test/screens-routes.test.ts` covers every row.\n- [ ] `npx tsc --noEmit -p .` clean; `npm test` passes (77 plus the new screen tests).\n\n**Verify:** `npx tsc --noEmit -p . && npm test && npx playwright test e2e/motion.spec.ts e2e/inplace.spec.ts --project=chromium` \u2192 all passed\n\n**Steps:**\n\n- [ ] **Step 1: Update `e2e/motion.spec.ts` expectations first** \u2014 in \"a page opened slides the way the reader goes\u2026\", replace the group assertion with\n\n```ts\n expect(opened.groups.filter((g) => !/^step-(line|done)-/.test(g)).sort()).toEqual([\"page\", \"page-foot\", \"page-top\", \"root\"])\n```\n\nand delete the `redraws` part (in-place redraws no longer transition; covered in `inplace.spec.ts`). In \"the next step of an order\u2026\", keep the `step-ring` / `step-line-name-code` / `step-done-details` assertions, replace the root and `gone-` assertions with\n\n```ts\n expect(next.groups).toContain(\"page\")\n expect(next.groups).toContain(\"order-card\")\n```\n\n- [ ] **Step 2: Run, expect FAIL** (`npx playwright test e2e/motion.spec.ts --project=chromium`).\n\n- [ ] **Step 3: CSS** \u2014 replace the view-transition section between `/* Every change of the page is compared\u2026` and the theme rules with:\n\n```css\n/* A page change moves only the shared parts, each by its name: the header and footer stay, the step bar's ring goes over,\n the order's card and name go to their new places; the rest of the page fades through, the way the reader goes. */\n.summary { view-transition-name: order-card; }\nbody > main:not(.pending) { view-transition-name: page; }\n::view-transition-group(page), ::view-transition-group(order-card), ::view-transition-group(.name), ::view-transition-group(step-ring), ::view-transition-group(page-foot) { animation-duration: var(--axis-ms); animation-timing-function: var(--emphasized); }\n::view-transition-old(page) { animation: var(--axis-out-ms) linear both fade-out; }\n::view-transition-new(page) { animation: var(--axis-in-ms) var(--emphasized) var(--axis-out-ms) both appear; }\nhtml:active-view-transition-type(forward, back)::view-transition-old(page) { animation: var(--axis-ms) var(--emphasized) both axis-out, var(--axis-out-ms) linear both fade-out; }\nhtml:active-view-transition-type(forward, back)::view-transition-new(page) { animation: var(--axis-ms) var(--emphasized) both axis-in, var(--axis-in-ms) var(--emphasized) var(--axis-out-ms) both appear; }\n```\n\nkeeping the existing `page-top`/`page-foot`/`offline-note` rules (types now `forward, back, screen, fill`), the `step-*` rules, `.order-name` rules and keyframes.\n\n- [ ] **Step 4: `page()`** \u2014 delete `compare`, `filled`/`arrLine truncated
|
||||
"blockedBy": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
8
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"subject": "Task 7: Verify across browsers, recordings and review",
|
||||
"status": "pending",
|
||||
"description": "**Goal:** The whole suite passes in three browsers, frame recordings show no flicker, and review subagents report no issues.\n\n> **USER-ORDERED GATE \u2014 NON-SKIPPABLE.** This task was requested by the user in the current conversation. It MUST NOT be closed by walking around it, by declaring it \"verified inline\", or by substituting a cheaper check. Close only after every item in `acceptanceCriteria` has been re-validated independently, with output captured.\n\n**Files:**\n- Test: whole `e2e/`, scratchpad recordings (`tools/journey/plain-journey.mjs`, `tools/static-spikes.mjs`)\n\n**Acceptance Criteria:**\n- [ ] `npx playwright test --project=chromium`, `--project=firefox`, `--project=webkit` each report 0 failed (2 offline tests skipped without `BASE_URL`).\n- [ ] Firefox and Chromium recordings of the `code-order` and `checkout-edits` journeys: `static-spikes.mjs` reports 0 one-frame spikes in header and ground; frame sheets of each in-page change show only the changed parts moving.\n- [ ] A review round of subagents (code, motion with frame recordings in Chromium and Firefox, design desktop and phone) reports no issues; findings are fixed and the round repeated until it does.\n\n**Verify:** `for b in chromium firefox webkit; do npx playwright test --project=$b; done` \u2192 `0 failed` each\n\n**Steps:**\n\n- [ ] **Step 1: Full e2e in each browser** (one at a time; Firefox and WebKit with the scratchpad env scripts).\n- [ ] **Step 2: Recordings** \u2014 `node plain-journey.mjs firefox code-order <dir>` and `chromium`, then `node static-spikes.mjs <dir>`; cut and read frame sheets of each change.\n- [ ] **Step 3: Review round** \u2014 dispatch reviewers (no edits to the repos, scratchpad only, no polling waits), fix findings, repeat until none.\n\n```json:metadata\n{\"files\": [], \"verifyCommand\": \"for b in chromium firefox webkit; do npx playwright test --project=$b; done\", \"acceptanceCriteria\": [\"0 failed in chromium, firefox, webkit\", \"0 one-frame spikes in recordings; only changed parts move\", \"review subagents report no issues\"], \"modelTier\": \"frontier\", \"userGate\": true, \"tags\": [\"user-gate\"]}\n```",
|
||||
"blockedBy": [
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"subject": "Task 8: The state machine core",
|
||||
"status": "in_progress",
|
||||
"description": "**Goal:** A pure `src/screens/machine.ts` with `Machine`, `Motion` and `step()`, unit-tested, for every view to run its state through.\n\n**Files:**\n- Create: `src/screens/machine.ts`\n- Test: `test/machine.test.ts`\n\n**Acceptance Criteria:**\n- [ ] `step(machine, state, shown, event, warn)` returns `{ state, screen, motion, expected }`: `state = machine.reduce(state, event)`, `screen = machine.screenOf(state)`; `shown === null` \u2192 motion `\"screen\"`, expected; `screen === shown` \u2192 `\"in-place\"`, expected; a move listed in `machine.moves[shown][screen]` \u2192 that motion, expected; otherwise `warn(\"[screens] <name>: unexpected move <shown> \u2192 <screen>\")`, motion `\"screen\"`, `expected: false`.\n- [ ] No DOM, clock, storage or network use in `src/screens/` (`rg -n \"document|window|Date\\.now|localStorage|fetch\" src/screens` empty).\n- [ ] vitest covers the four branches with a small test machine (counter-like states and two screens).\n\n**Verify:** `npx tsc --noEmit -p . && npx vitest run test/machine.test.ts` \u2192 passed\n\n**Steps:**\n\n- [ ] **Step 1: Failing test** `test/machine.test.ts`:\n\n```ts\nimport { describe, expect, it, vi } from \"vitest\"\nimport { type Machine, step } from \"../src/screens/machine.ts\"\n\ntype S = { n: number }\ntype E = { kind: \"add\"; by: number }\ntype K = \"low\" | \"high\" | \"odd\"\nconst machine: Machine<S, E, K> = {\n name: \"test\",\n reduce: (s, e) => ({ n: s.n + e.by }),\n screenOf: (s) => (s.n % 2 ? \"odd\" : s.n < 10 ? \"low\" : \"high\"),\n moves: { low: { high: \"forward\" }, high: { low: \"back\" }, odd: {} },\n}\n\ndescribe(\"step\", () => {\n it(\"draws the first screen as a page change\", () => {\n expect(step(machine, { n: 0 }, null, { kind: \"add\", by: 2 }, vi.fn())).toEqual({ state: { n: 2 }, screen: \"low\", motion: \"screen\", expected: true })\n })\n it(\"keeps a screen of the same kind in place\", () => {\n expect(step(machine, { n: 2 }, \"low\", { kind: \"add\", by: 2 }, vi.fn()).motion).toBe(\"in-place\")\n })\n it(\"takes a listed move's motion\", () => {\n expect(step(machine, { n: 8 }, \"low\", { kind: \"add\", by: 2 }, vi.fn())).toMatchObject({ screen: \"high\", motion: \"forward\", expected: true })\n })\n it(\"logs a move missing from the table and still shows it as a page change\", () => {\n const warn = vi.fn()\n expect(step(machine, { n: 2 }, \"low\", { kind: \"add\", by: 1 }, warn)).toMatchObject({ screen: \"odd\", motion: \"screen\", expected: false })\n expect(warn).toHaveBeenCalledWith(\"[screens] test: unexpected move low \u2192 odd\")\n })\n})\n```\n\n- [ ] **Step 2: Run, expect FAIL** (`npx vitest run test/machine.test.ts`: module not found).\n\n- [ ] **Step 3: Implement** `src/screens/machine.ts`:\n\n```ts\n// A view's behaviour as data: its state changes only by events through reduce, the screen shown follows from the state\n// alone, and each move from one screen to another has its motion; a move the table lacks is logged, and still shown.\nexport type Motion = \"in-place\" | \"forward\" | \"back\" | \"screen\"\nexport interface Machine<S, E, K extends string> {\n name: string\n reduce: (state: S, event: E) => S\n screenOf: (state: S) => K\n moves: { readonly [F in K]: Partial<Record<K, Exclude<Motion, \"in-place\">>> }\n}\nexport interface Step<S, K> { state: S; screen: K; motion: Motion; expected: boolean }\n\nexport function step<S, E, K extends string>(machine: Machine<S, E, K>, state: S, shown: K | null, event: E, warn: (message: string) => void = console.warn): Step<S, K> {\n const next = machine.reduce(state, event)\n const screen = machine.screenOf(next)\n if (shown === null) return { state: next, screen, motion: \"screen\", expected: true }\n if (screen === shown) return { state: next, screen, motion: \"in-place\", expected: true }\n const motion = machine.moves[shown][screen]\n if (motion) return { state: next, screen, motion, expected: true }\n warn(`[screens] ${machine.name}: unexpected move ${shown} \u2192 ${screen}`)\n return { state: next, screen, motion: \"screen\", expected: false }\n}\n```\n\n- [ ] **Step 4: Run, expect PASS**; `npx tsc --noEmit -p .` clean.\n\n```json:metadata\n{\"files\": [\"src/screens/machine.ts\", \"test/machine.test.ts\"], \"verifyCommand\": \"npx tsc --noEmit -p . && npx vitest run test/machine.test.ts\", \"acceptanceCriteria\": [\"step() covers first draw, in place, listed move, unexpected move with warn\", \"src/screens has no DOM/clock/storage/network use\", \"vitest covers the four branches\"], \"modelTier\": \"mechanical\"}\n```"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"subject": "Task 9: The order page as a state machine",
|
||||
"status": "pending",
|
||||
"description": "**Goal:** The order page's state lives in one object changed by events through a pure reducer in `src/screens/order.ts`; its screen comes from a pure `screenOf`, its moves from a table, all unit-tested; `order.ts` becomes the controller (polls, timers, API calls, storage) that dispatches events and renders.\n\n**Files:**\n- Create: `src/screens/order.ts`\n- Modify: `src/views/order.ts` (follow loop, timers and actions dispatch events; `kindOf` replaced by `screenOf`; screens render from state)\n- Test: `test/screens-order.test.ts`\n\n**Acceptance Criteria:**\n- [ ] `OrderState` holds everything the screens read: invoice, registration view, record, card submitted time, reveal attempts and refusals, action notes, hold, `now` (service time of the last event).\n- [ ] Events: `answered` (invoice and/or view with service time), `record-changed`, `tick` (service time), `card-submitted`, `card-failed`, `reveal-sent`, `reveal-refused`, `reveal-failed`, `cancel-started`, `cancel-answered`, `cancel-failed`, `reorder-started`, `reorder-failed`, `hold-started`, `hold-ended`; reducer pure (no clock, storage, network, DOM).\n- [ ] `screenOf` returns every kind of Task 2 and nothing else; `moves` lists the order lifecycle (pay-crypto \u2192 confirming \u2192 registering \u2192 registered | taken | failed; pay-crypto \u2192 expired | still-open | expired-cancelled; pay-card \u2192 confirming | card-failed | slow; confirming \u2192 slow | registering; registering \u2192 registered | taken | failed | not-on-device; etc.) with motion `screen`.\n- [ ] `test/screens-order.test.ts`: one test per `screenOf` branch (state \u2192 kind), one per `moves` row (a state of the from-screen and an event leading to the to-screen: `step()` returns the row's motion, `expected: true`), the reducer's handling of each event, and a race test: two `answered` events applied in either order (older service time second) end in the same state (an older answer never replaces a newer one).\n- [ ] The e2e of Task 2 still pass.\n\n**Verify:** `npx tsc --noEmit -p . && npm test && npx playwright test e2e/inplace.spec.ts e2e/paid.spec.ts e2e/taken.spec.ts --project=chromium` \u2192 all passed\n\n```json:metadata\n{\"files\": [\"src/screens/order.ts\", \"src/views/order.ts\", \"test/screens-order.test.ts\"], \"verifyCommand\": \"npx tsc --noEmit -p . && npm test && npx playwright test e2e/inplace.spec.ts e2e/paid.spec.ts e2e/taken.spec.ts --project=chromium\", \"acceptanceCriteria\": [\"OrderState holds all screen inputs\", \"events as listed, reducer pure\", \"screenOf returns Task 2 kinds; moves list the lifecycle\", \"vitest covers every branch, row, event and the answer race\", \"Task 2 e2e still pass\"], \"modelTier\": \"frontier\"}\n```",
|
||||
"blockedBy": [
|
||||
1,
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"lastUpdated": "2026-10-17T15:40:00Z"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"planPath": "plans/names-codes/2026-10-19-webapp-redesign-plan.md",
|
||||
"tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"subject": "Task 1: Tokens and frame",
|
||||
"status": "completed",
|
||||
"blockedBy": [],
|
||||
"description": "**Goal:** The page wears the landing's tokens, fonts, type scale, pill buttons, cards and fields in light and dark, inside the landing's header (logo, \"Your names\", theme icon) and footer (\"Pay with\" marks, links row), with the grid background, sticky bar, phone menu and `@property` theme easing gone.\n\n**Files:**\n- Modify: `src/styles.css` (lines 1-230 rewritten: fonts, tokens, base, frame, shared components; lines 650-660 motion variables kept for Task 2; lines 807-840 theme easing and phone menu deleted; remaining rules re-pointed to the new token names)\n- Modify: `src/views/shell.ts:140-323` (header, theme, phone menu, footer, frame)\n- Modify: `src/dom.ts:111-113` (`alertBox` emits `.notice` with tones `ok | warn | danger | info`) and its callers in `src/views/search.ts`, `src/views/checkout.ts`, `src/views/codes.ts`, `src/views/names.ts`, `src/views/order.ts`, `src/views/slots.ts`\n- Modify: `src/icons.ts` (delete `menu`, `moon`, `sun` line icons)\n- Modify: `index.html` (static header, theme-color, font preloads)\n- Modify: `README.md` (CSP script hashes)\n- Delete: `public/fonts/Manrope/Manrope-ExtraBold.woff2`, `public/fonts/Manrope/Manrope-ExtraBold-latin.woff2`\n- Modify: `e2e/search.spec.ts` (delete the phone-menu tests at lines 118, 142, 202), `e2e/focus.spec.ts` (delete tests at lines 212, 264, 281)\n\n**Acceptance Criteria:**\n- [ ] `getComputedStyle(document.documentElement).getPropertyValue(\"--page-color\")` is `#F3FAFF` in light and `#000832` with `html.dark`; `--primary` is `#0053D0` / `#70F0F9`.\n- [ ] `rg -n \"@property|grid-line|menu-btn|menu-open|theme-easing|--glow|--button-glow|box-shadow: var\\(--shadow\" src/styles.css` prints nothing.\n- [ ] On every route the header shows the logo image, a \"Your names\" link to `/#/names` and one theme button; no element in the page has `position: sticky`.\n- [ ] The footer shows \"Pay with\" and the Card, Bitcoin and Monero marks, and the links Wefunder, Trademark owners, Contact.\n- [ ] `.btn` computes `border-radius: 999px`, `padding: 16px 32px`, `font-size: 18px`, `font-weight: 600`, no `box-shadow`.\n- [ ] The README CSP hashes equal the output of the hash command in Step 6.\n- [ ] `npx tsc --noEmit` prints nothing; `npx vitest run` passes.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run # \u2192 no tsc output; \"Test Files \u2026 passed\"\nrg -n \"@property|grid-line|menu-btn|theme-easing\" src/styles.css # \u2192 no output\n```\n\n```json:metadata\n{\"files\":[\"src/styles.css\",\"src/views/shell.ts\",\"src/dom.ts\",\"src/views/search.ts\",\"src/views/checkout.ts\",\"src/views/codes.ts\",\"src/views/names.ts\",\"src/views/order.ts\",\"src/views/slots.ts\",\"src/icons.ts\",\"index.html\",\"README.md\",\"public/fonts/Manrope/Manrope-ExtraBold.woff2\",\"public/fonts/Manrope/Manrope-ExtraBold-latin.woff2\",\"e2e/search.spec.ts\",\"e2e/focus.spec.ts\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run && rg -n \\\"@property|grid-line|menu-btn|theme-easing\\\" src/styles.css\",\"acceptanceCriteria\":[\"`getComputedStyle(document.documentElement).getPropertyValue(\\\"--page-color\\\")` is `#F3FAFF` in light and `#000832` with `html.dark`; `--primary` is `#0053D0` / `#70F0F9`.\",\"`rg -n \\\"@property|grid-line|menu-btn|menu-open|theme-easing|--glow|--button-glow|box-shadow: var\\\\(--shadow\\\" src/styles.css` prints nothing.\",\"On every route the header shows the logo image, a \\\"Your names\\\" link to `/#/names` and one theme button; no element in the page has `position: sticky`.\",\"The footer shows \\\"Pay with\\\" and the Card, Bitcoin and Monero marks, and the links Wefunder, Trademark owners, Contact.\",\"`.btn` computes `border-radius: 999px`, `padding: 16px 32px`, `font-size: 18px`, `font-weight: 600`, no `box-shadow`.\",\"The README CSP hashes equal the output of the hash command in Step 6.\",\"`npx tsc --noEmit` prints nothing; `npx vitest run` passes.\"],\"modelTier\":\"standard\"}\n```"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"subject": "Task 2: Whole-screen builders",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
1
|
||||
],
|
||||
"description": "**Goal:** Every view draws whole screens from state through `shell.draw()`, with typed-in inputs kept as nodes, and the motion, view-transition, slot, transition-table and focus-restoration code is gone.\n\n**Files:**\n- Delete: `src/motion.ts`, `src/motion-values.ts`, `src/focus.ts`, `src/views/slots.ts`, `test/motion-values.test.ts`, `test/machine.test.ts`, `e2e/motion.spec.ts`, `e2e/cuts.spec.ts`, `e2e/cuts.ts`, `e2e/crossing.spec.ts`, `e2e/sweep.ts`, `e2e/inplace.spec.ts`, `e2e/inplace-checkout.spec.ts`, `e2e/inplace-names.spec.ts`, `e2e/inplace-search.spec.ts`\n- Rename: `src/screens/machine.ts` \u2192 `src/screens/hold.ts` (keeps only `Hold` and `NO_LEAD`)\n- Modify: `src/screens/search.ts:121-128`, `src/screens/checkout.ts:89-100`, `src/screens/order.ts:194-223`, `src/screens/names.ts:87,182,319-330`, `src/screens/routes.ts:18-29` (delete `moves`, `*Machine`, `routes`)\n- Modify: `src/views/shell.ts` (new contract), `src/views/effects.ts` (delete `oneAtATime`), `src/views/hold.ts` (gains `holdNotice`, `secondsLeft`), `src/dom.ts`, `src/icons.ts` (adds `back`), `src/views/search.ts`, `src/views/checkout.ts`, `src/views/codes.ts`, `src/views/names.ts`, `src/views/order.ts`, `src/main.ts`, `src/styles.css`\n- Modify: `test/effects.test.ts`, `test/screens-search.test.ts`, `test/screens-checkout.test.ts`, `test/screens-order.test.ts`, `test/screens-names.test.ts`, `test/screens-routes.test.ts`, `test/hold.test.ts`\n- Modify: `e2e/helpers.ts`, `playwright.config.ts`, `e2e/robustness.spec.ts:524` (drop the `@motion` tag)\n\n**Acceptance Criteria:**\n- [ ] `fd 'motion|focus|slots' src` prints nothing; `rg -n \"view-transition|@keyframes|startViewTransition|\\.animate\\(|getAnimations\" src` prints nothing.\n- [ ] `rg -n \"moves|Machine|headingFocus|routes\\b\" src/screens test` prints nothing.\n- [ ] `rg -n \"Built<|Build<|unchanging|changeTo|drawAs|live\\(|syncList|screens<\" src` prints nothing.\n- [ ] Typing in the search field, then a redraw (search answer arriving) leaves the same `<input>` node focused with its caret where it was (e2e `robustness.spec.ts` \"a name typed with .simplex\u2026\" passes in Chromium).\n- [ ] A screen of another kind moves focus to its `h1` (`tabindex=\"-1\"`) unless focus is in the header or footer; a redraw of the same kind returns focus to the control with the same `data-focus` key.\n- [ ] `npx tsc --noEmit` prints nothing; `npx vitest run` passes.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run # \u2192 no tsc output; all test files pass\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/search.spec.ts e2e/paid.spec.ts --project=chromium # \u2192 \"passed\", 0 failed\n```\n\n```json:metadata\n{\"files\":[\"src/motion.ts\",\"src/motion-values.ts\",\"src/focus.ts\",\"src/views/slots.ts\",\"test/motion-values.test.ts\",\"test/machine.test.ts\",\"e2e/motion.spec.ts\",\"e2e/cuts.spec.ts\",\"e2e/cuts.ts\",\"e2e/crossing.spec.ts\",\"e2e/sweep.ts\",\"e2e/inplace.spec.ts\",\"e2e/inplace-checkout.spec.ts\",\"e2e/inplace-names.spec.ts\",\"e2e/inplace-search.spec.ts\",\"src/screens/machine.ts\",\"src/screens/hold.ts\",\"src/screens/search.ts\",\"src/screens/checkout.ts\",\"src/screens/order.ts\",\"src/screens/names.ts\",\"src/screens/routes.ts\",\"src/views/shell.ts\",\"src/views/effects.ts\",\"src/views/hold.ts\",\"src/dom.ts\",\"src/icons.ts\",\"src/views/search.ts\",\"src/views/checkout.ts\",\"src/views/codes.ts\",\"src/views/names.ts\",\"src/views/order.ts\",\"src/main.ts\",\"src/styles.css\",\"test/effects.test.ts\",\"test/screens-search.test.ts\",\"test/screens-checkout.test.ts\",\"test/screens-order.test.ts\",\"test/screens-names.test.ts\",\"test/screens-routes.test.ts\",\"test/hold.test.ts\",\"e2e/helpers.ts\",\"playwright.config.ts\",\"e2e/robustness.spec.ts\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/search.spec.ts e2e/paid.spec.ts --project=chromium\",\"acceptanceCriteria\":[\"`fd 'motion|focus|slots' src` prints nothing; `rg -n \\\"view-transition|@keyframes|startViewTransition|\\\\.animate\\\\(|getAnimations\\\" src` prints nothing.\",\"`rg -n \\\"moves|Machine|headingFocus|routes\\\\b\\\" src/screens test` prints nothing.\",\"`rg -n \\\"Built<|Build<|unchanging|changeTo|drawAs|live\\\\(|syncList|screens<\\\" src` prints nothing.\",\"Typing in the search field, then a redraw (search answer arriving) leaves the same `<input>` node focused with its caret where it was (e2e `robustness.spec.ts` \\\"a name typed with .simplex\u2026\\\" passes in Chromium).\",\"A screen of another kind moves focus to its `h1` (`tabindex=\\\"-1\\\"`) unless focus is in the header or footer; a redraw of the same kind returns focus to the control with the same `data-focus` key.\",\"`npx tsc --noEmit` prints nothing; `npx vitest run` passes.\"],\"modeLine truncated
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"subject": "Task 3: Purchase track",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
2
|
||||
],
|
||||
"description": "**Goal:** Search, checkout and the payment screens sit on one sliding CSS track adapted from badges `main.ts:265-466`; every other screen swaps whole.\n\n**Files:**\n- Create: `src/views/track.ts`\n- Create: `e2e/track.spec.ts`\n- Modify: `src/views/shell.ts` (`draw` gains a `step` argument)\n- Modify: `src/views/search.ts`, `src/views/checkout.ts`, `src/views/order.ts` (pass their step)\n- Modify: `src/styles.css` (track rules; the stage tracker's `.track` becomes `.stages`), `src/views/order.ts:766-779` (`class: \"stages\"`)\n\n**Acceptance Criteria:**\n- [ ] On search, checkout and pay screens `main > .track > .rail` exists with three `.slide` children; exactly one is not `inert`.\n- [ ] After Register the rail's computed transform is `translateX(-W)` where W is the track's width (shift \u22121); after Back it is 0; on a pay screen it is \u22122.\n- [ ] Your names, restore, edit, trademarks and every order screen other than `pay-crypto`/`pay-card` render without `.track`.\n- [ ] Moves between non-adjacent steps, first mounts and reduced motion set `--slide:0ms`.\n- [ ] The track's height follows the slide in view when its content grows (ResizeObserver), so nothing is clipped.\n- [ ] `e2e/track.spec.ts` passes in Chromium; `npx tsc --noEmit` prints nothing; `npx vitest run` passes.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/track.spec.ts --project=chromium --project=firefox # \u2192 2 passed per project\n```\n\n```json:metadata\n{\"files\":[\"src/views/track.ts\",\"e2e/track.spec.ts\",\"src/views/shell.ts\",\"src/views/search.ts\",\"src/views/checkout.ts\",\"src/views/order.ts\",\"src/styles.css\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/track.spec.ts --project=chromium --project=firefox\",\"acceptanceCriteria\":[\"On search, checkout and pay screens `main > .track > .rail` exists with three `.slide` children; exactly one is not `inert`.\",\"After Register the rail's computed transform is `translateX(-W)` where W is the track's width (shift \u22121); after Back it is 0; on a pay screen it is \u22122.\",\"Your names, restore, edit, trademarks and every order screen other than `pay-crypto`/`pay-card` render without `.track`.\",\"Moves between non-adjacent steps, first mounts and reduced motion set `--slide:0ms`.\",\"The track's height follows the slide in view when its content grows (ResizeObserver), so nothing is clipped.\",\"`e2e/track.spec.ts` passes in Chromium; `npx tsc --noEmit` prints nothing; `npx vitest run` passes.\"],\"modelTier\":\"standard\"}\n```"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"subject": "Task 4: Home with search",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
3
|
||||
],
|
||||
"description": "**Goal:** The home page is the simplex.domains page with the countdown replaced by the search field, and every search answer appears under the field as one card or one line.\n\n**Files:**\n- Modify: `src/views/search.ts` (rewrite: ~547 \u2192 ~300 lines)\n- Modify: `src/screens/search.ts` (drop `applied`, `code-removed`, `codeFor`, `card-due`, `searching.card`)\n- Modify: `test/screens-search.test.ts`\n- Modify: `src/icons.ts` (delete `wefunderMark` and icons left unused), `src/views/shell.ts` (delete `INVEST_LINE`, `INVEST_AMOUNT`, `BADGES_URL` if unused), `src/styles.css` (home rules; delete `.hero`, `.bento`, `.tile`, `.scene`, `.showcase`, `.invest`, `.result*`, `.banner*`, `.under-search`, `.hint`)\n- Modify: `e2e/search.spec.ts`, `e2e/codes.spec.ts`, `e2e/focus.spec.ts`, `e2e/robustness.spec.ts`, `e2e/helpers.ts`\n\n**Acceptance Criteria:**\n- [ ] The home shows, top to bottom: `h1` \"Your SimpleX domain\", the sub line \"One name for your public channel and contact address\", the search field with \".simplex\" and a Search button, the answer area, the \"Invest to claim your name\" pill (link to `WEFUNDER_URL`, new tab) with the note \"Invest in SimpleX Chat to get a name for 5 years\", then the footer.\n- [ ] No countdown, \"NEW\" pill, how-it-works tiles, animated scene or code banner exists (`rg -n \"bento|scene|playScene|NEW|applied\" src/views/search.ts` prints nothing).\n- [ ] Available: one card with the name, its price, its term (\"2 years + 30 days\") and Register. Taken: the name, its chip and the entities it points at. Reserved, rule, could-not-check: one line each. Rate limited: one countdown line and Search disabled.\n- [ ] Opened from the app, from a retarget, or after a move: one notice line above the field; no invest block.\n- [ ] Vitest `screens-search` covers: `searched` sets `searching: { label }`; `answered` clears it; no `applied` field remains.\n- [ ] e2e `search.spec.ts` \"search answers every kind of name\", `robustness.spec.ts` \"a slow answer\u2026\" and \"a name typed with .simplex\u2026\" pass in Chromium.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run test/screens-search.test.ts # \u2192 passes\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/search.spec.ts e2e/robustness.spec.ts --project=chromium # \u2192 0 failed\n```\n\nRuling: the home is the simplex.domains page exactly, minus the countdown, plus the search. Keep its invest button, its modal (port the landing's modal markup and CSS) and its copy. The footer is exactly the landing's, including the \"In-app\" mark and its links.\n\n```json:metadata\n{\"files\":[\"src/views/search.ts\",\"src/screens/search.ts\",\"test/screens-search.test.ts\",\"src/icons.ts\",\"src/views/shell.ts\",\"src/styles.css\",\"e2e/search.spec.ts\",\"e2e/codes.spec.ts\",\"e2e/focus.spec.ts\",\"e2e/robustness.spec.ts\",\"e2e/helpers.ts\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run test/screens-search.test.ts && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/search.spec.ts e2e/robustness.spec.ts --project=chromium\",\"acceptanceCriteria\":[\"The home shows, top to bottom: `h1` \\\"Your SimpleX domain\\\", the sub line \\\"One name for your public channel and contact address\\\", the search field with \\\".simplex\\\" and a Search button, the answer area, the \\\"Invest to claim your name\\\" pill (link to `WEFUNDER_URL`, new tab) with the note \\\"Invest in SimpleX Chat to get a name for 5 years\\\", then the footer.\",\"No countdown, \\\"NEW\\\" pill, how-it-works tiles, animated scene or code banner exists (`rg -n \\\"bento|scene|playScene|NEW|applied\\\" src/views/search.ts` prints nothing).\",\"Available: one card with the name, its price, its term (\\\"2 years + 30 days\\\") and Register. Taken: the name, its chip and the entities it points at. Reserved, rule, could-not-check: one line each. Rate limited: one countdown line and Search disabled.\",\"Opened from the app, from a retarget, or after a move: one notice line above the field; no invest block.\",\"Vitest `screens-search` covers: `searched` sets `searching: { label }`; `answered` clears it; no `applied` field remains.\",\"e2e `search.spec.ts` \\\"search answers every kind of name\\\", `robustness.spec.ts` \\\"a slow answer\u2026\\\" and \\\"a name typed with .simplex\u2026\\\" pass in Chromium.\"],\"modelTier\":\"standard\"}\n```"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"subject": "Task 5: One-page checkout",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
4
|
||||
],
|
||||
"description": "**Goal:** Checkout is one page in this order: summary card, optional links with previews, payment method, \"I have a code\", the name code with Copy and the saved checkbox, and one Pay button; the separate name-code page and every code popup are gone.\n\n**Files:**\n- Modify: `src/views/checkout.ts` (rewrite: ~640 \u2192 ~420 lines)\n- Modify: `src/views/codes.ts` (rewrite: popup \u2192 `codeField`, ~181 \u2192 ~90 lines)\n- Modify: `src/screens/checkout.ts` (drop `page`, `entry`, `entry-opened`, `CheckoutPage`)\n- Modify: `src/screens/routes.ts` (drop `\"name-code\"`), `src/main.ts` (drop `ownerCodePage`, `codePopup`)\n- Modify: `src/session.ts` (delete `APPLIED`, `appliedCode`, `setAppliedCode`)\n- Modify: `src/views/names.ts:180-185,389-412` (no `setAppliedCode`; code rows lose Use; Add a code uses `codeField`), `src/views/order.ts:990-1006` (taken-with-code action becomes \"Search another name\")\n- Modify: `test/screens-checkout.test.ts`, `test/screens-routes.test.ts`, `src/styles.css` (checkout rules; delete `.code-entry`, `.covered*`, `.sub-links`, `.why`, `.cols`, `.summary`, `.mini-invest`, `.crumb*`)\n- Modify: `e2e/helpers.ts` (`startOrder`, `savedCode`), `e2e/codes.spec.ts`, `e2e/search.spec.ts`, `e2e/robustness.spec.ts`, `e2e/focus.spec.ts`, `e2e/app.spec.ts`\n\n**Acceptance Criteria:**\n- [ ] `/#/checkout` shows blocks in DOM order: `.summary-card`, `.links` (absent for an app order), `.methods` (or the covered line, or the waiting-order notice), `.code-entry` labelled \"I have a code\" (absent for an app order), `.name-code` with `data-testid=\"owner-code\"`, Copy and the checkbox \"I have saved my name code\" (absent for an app order), then one button whose name starts with \"Pay\" (or \"Register\" with a code, \"Continue\" for an app order).\n- [ ] The Pay button is disabled until the checkbox is ticked, while the order is sending, while a card order waits and during a hold.\n- [ ] A valid prepaid code typed into \"I have a code\" makes the total `$0.00` with the old price struck through and hides the methods; a code for longer names shows \"This code covers names of N or more letters; <name> has M.\" under the field.\n- [ ] `/#/checkout/code` routes to search; `rg -n \"codePopup|applyCode|useCode|appliedCode|ownerCodePage|owner-code\\\"\" src` prints only the `data-testid`.\n- [ ] Opening the checkout (not from the app) stores the name code as a pending code, so Your names lists it before any order exists (e2e `codes.spec.ts` \"an owner code shown at checkout is kept in Your names\u2026\").\n- [ ] e2e `search.spec.ts` \"the checkout explains every refusal\u2026\", `codes.spec.ts` code-at-checkout tests and `paid.spec.ts` \"a Bitcoin order pays in two parts\u2026\" pass in Chromium.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run # \u2192 passes\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/search.spec.ts e2e/codes.spec.ts e2e/paid.spec.ts e2e/app.spec.ts --project=chromium # \u2192 0 failed\n```\n\n```json:metadata\n{\"files\":[\"src/views/checkout.ts\",\"src/views/codes.ts\",\"src/screens/checkout.ts\",\"src/screens/routes.ts\",\"src/main.ts\",\"src/session.ts\",\"src/views/names.ts\",\"src/views/order.ts\",\"test/screens-checkout.test.ts\",\"test/screens-routes.test.ts\",\"src/styles.css\",\"e2e/helpers.ts\",\"e2e/codes.spec.ts\",\"e2e/search.spec.ts\",\"e2e/robustness.spec.ts\",\"e2e/focus.spec.ts\",\"e2e/app.spec.ts\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/search.spec.ts e2e/codes.spec.ts e2e/paid.spec.ts e2e/app.spec.ts --project=chromium\",\"acceptanceCriteria\":[\"`/#/checkout` shows blocks in DOM order: `.summary-card`, `.links` (absent for an app order), `.methods` (or the covered line, or the waiting-order notice), `.code-entry` labelled \\\"I have a code\\\" (absent for an app order), `.name-code` with `data-testid=\\\"owner-code\\\"`, Copy and the checkbox \\\"I have saved my name code\\\" (absent for an app order), then one button whose name starts with \\\"Pay\\\" (or \\\"Register\\\" with a code, \\\"Continue\\\" for an app order).\",\"The Pay button is disabled until the checkbox is ticked, while the order is sending, while a card order waits and during a hold.\",\"A valid prepaid code typed into \\\"I have a code\\\" makes the total `$0.00` with the old price struck through and hides the methods; a code for longer names shows \\\"This code covers names of N or more letters; <name> has M.\\\" under the field.\",\"`/#/checkout/code` routes to search; `rg -n \\\"codePopup|applyCode|useCode|appliedCode|ownerCodePage|owner-code\\\\\\\"\\\" src` prints only the `data-testid`.\",\"Opening the checkout (not from the app) stores the name code as a pending code, so Your names lists it before any order exists (e2e `codes.spec.ts`Line truncated
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"subject": "Task 6: Payment, registration and \"is yours\"",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
5
|
||||
],
|
||||
"description": "**Goal:** The pay, confirming, registering and registered screens are single columns in the new look, keeping the QR, amount, address with Copy, rate countdown, Stripe form, stage tracker and wait countdown.\n\n**Files:**\n- Modify: `src/views/order.ts:363-526,633-654,759-988` (pay crypto, pay card, confirming, stages, registering, registered)\n- Modify: `src/styles.css` (pay, stages, countdown, registered rules; delete `.pay-head`, `.pay-body`, `.meter`, `.celebrate`, `.tick`, `.next`, `.item`, `.txs` grid leftovers)\n- Modify: `e2e/paid.spec.ts`, `e2e/app.spec.ts`, `e2e/codes.spec.ts:306`\n\n**Acceptance Criteria:**\n- [ ] Pay with Bitcoin/Monero: one column with `h1` \"Pay with Bitcoin\", the lead, the QR in a `.card`, the Amount, address and Reference copy fields, \"Rate held m:ss\" counting down each second, the waiting line and \"Cancel this invoice\".\n- [ ] A part payment shows one warn notice above the QR and \"Send the rest\"; \"Cancel this invoice\" is gone then.\n- [ ] Pay by card: `h1` \"Pay by card\", the kept Stripe container (`.stripe`), errors under it, one \"Pay $X\" button; a redraw while confirming does not remount Stripe (one `mountCard` call per route).\n- [ ] Registering: `h1` \"Registering <name>\", the three stages (`.stages > .stage` with `done`/`active`/`failed`), the wait countdown `m:ss` with its note, the transactions rows.\n- [ ] Registered: `h1` \"<name>.simplex is yours\", the line \"Registered for \u2026 until \u2026\", the name code in a `.card` with Copy, and one next-step card with the code's QR, \"Scan or enter the code in SimpleX: the name moves to the app's wallet.\" and an \"Edit links\" link; an app order shows \"Back to SimpleX\" instead.\n- [ ] e2e `paid.spec.ts` \"a Bitcoin order pays in two parts\u2026\", \"a card order waits for Stripe\u2026\", \"Cancel this invoice reads as working\u2026\" and `codes.spec.ts:306` pass in Chromium.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/paid.spec.ts e2e/app.spec.ts e2e/codes.spec.ts --project=chromium # \u2192 0 failed\n```\n\n```json:metadata\n{\"files\":[\"src/views/order.ts\",\"src/styles.css\",\"e2e/paid.spec.ts\",\"e2e/app.spec.ts\",\"e2e/codes.spec.ts\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/paid.spec.ts e2e/app.spec.ts e2e/codes.spec.ts --project=chromium\",\"acceptanceCriteria\":[\"Pay with Bitcoin/Monero: one column with `h1` \\\"Pay with Bitcoin\\\", the lead, the QR in a `.card`, the Amount, address and Reference copy fields, \\\"Rate held m:ss\\\" counting down each second, the waiting line and \\\"Cancel this invoice\\\".\",\"A part payment shows one warn notice above the QR and \\\"Send the rest\\\"; \\\"Cancel this invoice\\\" is gone then.\",\"Pay by card: `h1` \\\"Pay by card\\\", the kept Stripe container (`.stripe`), errors under it, one \\\"Pay $X\\\" button; a redraw while confirming does not remount Stripe (one `mountCard` call per route).\",\"Registering: `h1` \\\"Registering <name>\\\", the three stages (`.stages > .stage` with `done`/`active`/`failed`), the wait countdown `m:ss` with its note, the transactions rows.\",\"Registered: `h1` \\\"<name>.simplex is yours\\\", the line \\\"Registered for \u2026 until \u2026\\\", the name code in a `.card` with Copy, and one next-step card with the code's QR, \\\"Scan or enter the code in SimpleX: the name moves to the app's wallet.\\\" and an \\\"Edit links\\\" link; an app order shows \\\"Back to SimpleX\\\" instead.\",\"e2e `paid.spec.ts` \\\"a Bitcoin order pays in two parts\u2026\\\", \\\"a card order waits for Stripe\u2026\\\", \\\"Cancel this invoice reads as working\u2026\\\" and `codes.spec.ts:306` pass in Chromium.\"],\"modelTier\":\"standard\"}\n```"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"subject": "Task 7: Order outcomes merged",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
6
|
||||
],
|
||||
"description": "**Goal:** Each order ending is one screen in the badges pattern: \"Invoice closed\" replaces the expired, cancelled, still-open and paid-late variants; \"Card payment not confirmed\" replaces card-failed and slow; taken keeps the retarget flow; failed offers Try again or the way back; a checkout rate limit is one countdown screen.\n\n**Files:**\n- Modify: `src/screens/order.ts` (kinds, `closedCase`, `paymentKind`)\n- Modify: `test/screens-order.test.ts`\n- Modify: `src/views/order.ts:334-357,536-564,656-722,927-931,990-1120` (outcome builders)\n- Modify: `src/views/shell.ts` (add `limitScreen`), `src/views/checkout.ts:402-425` (too-many uses it)\n- Modify: `src/styles.css` (`.outcome`; delete `.status*`, `.orb`, `.options`, `.option`, `.ref`)\n- Modify: `e2e/paid.spec.ts`, `e2e/robustness.spec.ts`, `e2e/taken.spec.ts`, `e2e/search.spec.ts:178`\n\n**Acceptance Criteria:**\n- [ ] `OrderKind` is exactly `\"pay-crypto\" | \"pay-card\" | \"confirming\" | \"registering\" | \"registered\" | \"taken\" | \"code-issued\" | \"failed\" | \"not-on-device\" | \"status:closed\" | \"status:card-unconfirmed\" | \"status:paid-by-invoice\" | \"status:unreadable\" | \"status:unknown-link\" | \"status:price-changed\"`.\n- [ ] Vitest: `closedCase` returns `paid-late`, `still-open`, `cancelled`, `received`, `expired` and `null` for the six fixtures in Step 1; `screenOf` maps all five closed cases to `status:closed` and card-failed and slow to `status:card-unconfirmed`.\n- [ ] Every closed screen has `h1` \"Invoice closed\", one notice, at most one button and the reference; a payment-holding invoice's notice keeps the refund line (\"Write to chat@simplex.chat with the reference below\u2026\" or \"Support refunds it: quote the reference below.\"), a cancel the service still shows open keeps \"Do not send anything more to the address.\"\n- [ ] \"Card payment not confirmed\" has one notice and at most one button (\"Load the card form again\" after a form failure, \"Enter the card again\" when Stripe has seen nothing, none when it has).\n- [ ] The checkout's rate limit is one screen: `h1` \"Too many attempts\", a countdown line, \"Try again\" disabled until it reaches zero, and Back.\n- [ ] e2e `paid.spec.ts` \"a cancelled invoice leads back\u2026\", \"a card form that does not load\u2026\", `robustness.spec.ts` \"Get a new invoice keeps\u2026\", \"a payment that arrives after expiry\u2026\", and `taken.spec.ts` pass in Chromium.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run test/screens-order.test.ts # \u2192 passes\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/paid.spec.ts e2e/robustness.spec.ts e2e/taken.spec.ts --project=chromium # \u2192 0 failed\n```\n\nRuling: every outcome keeps its way forward. On the card-form-failed screen the one button is \"Get a new invoice\".\n\n```json:metadata\n{\"files\":[\"src/screens/order.ts\",\"test/screens-order.test.ts\",\"src/views/order.ts\",\"src/views/shell.ts\",\"src/views/checkout.ts\",\"src/styles.css\",\"e2e/paid.spec.ts\",\"e2e/robustness.spec.ts\",\"e2e/taken.spec.ts\",\"e2e/search.spec.ts\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run test/screens-order.test.ts && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/paid.spec.ts e2e/robustness.spec.ts e2e/taken.spec.ts --project=chromium\",\"acceptanceCriteria\":[\"`OrderKind` is exactly `\\\"pay-crypto\\\" | \\\"pay-card\\\" | \\\"confirming\\\" | \\\"registering\\\" | \\\"registered\\\" | \\\"taken\\\" | \\\"code-issued\\\" | \\\"failed\\\" | \\\"not-on-device\\\" | \\\"status:closed\\\" | \\\"status:card-unconfirmed\\\" | \\\"status:paid-by-invoice\\\" | \\\"status:unreadable\\\" | \\\"status:unknown-link\\\" | \\\"status:price-changed\\\"`.\",\"Vitest: `closedCase` returns `paid-late`, `still-open`, `cancelled`, `received`, `expired` and `null` for the six fixtures in Step 1; `screenOf` maps all five closed cases to `status:closed` and card-failed and slow to `status:card-unconfirmed`.\",\"Every closed screen has `h1` \\\"Invoice closed\\\", one notice, at most one button and the reference; a payment-holding invoice's notice keeps the refund line (\\\"Write to chat@simplex.chat with the reference below\u2026\\\" or \\\"Support refunds it: quote the reference below.\\\"), a cancel the service still shows open keeps \\\"Do not send anything more to the address.\\\"\",\"\\\"Card payment not confirmed\\\" has one notice and at most one button (\\\"Load the card form again\\\" after a form failure, \\\"Enter the card again\\\" when Stripe has seen nothing, none when it has).\",\"The checkout's rate limit is one screen: `h1` \\\"Too many attempts\\\", a countdown line, \\\"Try again\\\" disabled until it reaches zero, and Back.\",\"e2e `paid.spec.ts` \\\"a cancelled invoice leads back\u2026\\\", \\\"a card form that does not load\u2026\\\", Line truncated
|
||||
},
|
||||
{
|
||||
"id": "7b",
|
||||
"subject": "Task 7b: Badges-like checkout flow",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
7
|
||||
],
|
||||
"description": "See plan section Task 7b."
|
||||
},
|
||||
{
|
||||
"id": "7c",
|
||||
"subject": "Task 7c: Expected purchase flow, no scroll",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
9
|
||||
],
|
||||
"description": "See plan section Task 7c."
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"subject": "Task 8: Your names, restore, edit and trademarks",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
"7b"
|
||||
],
|
||||
"description": "**Goal:** Your names lists the stored rows read once on open, with Add a code, Show code inline, Edit links, Restore and Forget asked on the page; restore, edit links and trademarks are single columns; the last popup goes.\n\n**Files:**\n- Modify: `src/views/names.ts` (rewrite: ~722 \u2192 ~450 lines)\n- Modify: `src/screens/names.ts` (`shownCode`, `code-toggled`)\n- Modify: `test/screens-names.test.ts`\n- Modify: `src/views/shell.ts` (delete `popup`, `closePopup`, `shut`, `popupKeys`, `opener`, `behindPopup`, `OVERLAY`, `POPUP_MS`, `popupRoute`)\n- Modify: `src/styles.css` (list rows; delete `.modal*`, `html.modal-open`, `--backdrop` from both token blocks, `.panel`, `.panel-sec`, `.row-item`, `.toolbar`, `.list-title`)\n- Modify: `e2e/codes.spec.ts`, `e2e/focus.spec.ts`, `e2e/robustness.spec.ts`, `e2e/paid.spec.ts:526`\n\n**Acceptance Criteria:**\n- [ ] Opening `/#/names` reads each order and each code once; a row whose read failed shows \"Not checked\" and is read again once after `api.retryDelay(e, UNREAD_RETRY_MS)`; no other timer runs (`rg -n \"FOLLOW_MS|MOST_STAGGERED|entering|storage\\\"\" src/views/names.ts` prints nothing).\n- [ ] The page shows, in order: `h1` \"Your names\", its lead, the Add a code field with an Add button, a \"Restore with a name code\" link, the Names card (pending rows, then order rows), the Codes card, and \"Forget everything in this browser\".\n- [ ] \"Show code\" on a registered row expands that row with the name code (Copy) and its QR; pressing it again (\"Hide code\") collapses it; only one row is expanded at a time.\n- [ ] \"Forget everything in this browser\" turns into the question with \"Forget everything\" and \"Keep them\" on the page, focus on \"Keep them\"; no `window.confirm` (`rg -n \"confirm\\(\" src` prints nothing).\n- [ ] Restore, edit links and trademarks are `.screen` columns with Back to Your names (trademarks: Back to home).\n- [ ] `rg -n \"popup|modal|\\\"panel\" src` prints nothing; the unused-exports check prints nothing.\n- [ ] e2e `codes.spec.ts` Your names tests, `robustness.spec.ts:299` \"a row Your names could not read is read again\u2026\" and `paid.spec.ts:526` pass in Chromium.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run test/screens-names.test.ts # \u2192 passes\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/codes.spec.ts e2e/focus.spec.ts e2e/robustness.spec.ts e2e/paid.spec.ts --project=chromium # \u2192 0 failed\n```\n\nRuling: the list is read when opened. Rows whose read failed are read again once, on a timer. There is no cross-tab listener.\n\n```json:metadata\n{\"files\":[\"src/views/names.ts\",\"src/screens/names.ts\",\"test/screens-names.test.ts\",\"src/views/shell.ts\",\"src/styles.css\",\"e2e/codes.spec.ts\",\"e2e/focus.spec.ts\",\"e2e/robustness.spec.ts\",\"e2e/paid.spec.ts\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run test/screens-names.test.ts && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/codes.spec.ts e2e/focus.spec.ts e2e/robustness.spec.ts e2e/paid.spec.ts --project=chromium\",\"acceptanceCriteria\":[\"Opening `/#/names` reads each order and each code once; a row whose read failed shows \\\"Not checked\\\" and is read again once after `api.retryDelay(e, UNREAD_RETRY_MS)`; no other timer runs (`rg -n \\\"FOLLOW_MS|MOST_STAGGERED|entering|storage\\\\\\\"\\\" src/views/names.ts` prints nothing).\",\"The page shows, in order: `h1` \\\"Your names\\\", its lead, the Add a code field with an Add button, a \\\"Restore with a name code\\\" link, the Names card (pending rows, then order rows), the Codes card, and \\\"Forget everything in this browser\\\".\",\"\\\"Show code\\\" on a registered row expands that row with the name code (Copy) and its QR; pressing it again (\\\"Hide code\\\") collapses it; only one row is expanded at a time.\",\"\\\"Forget everything in this browser\\\" turns into the question with \\\"Forget everything\\\" and \\\"Keep them\\\" on the page, focus on \\\"Keep them\\\"; no `window.confirm` (`rg -n \\\"confirm\\\\(\\\" src` prints nothing).\",\"Restore, edit links and trademarks are `.screen` columns with Back to Your names (trademarks: Back to home).\",\"`rg -n \\\"popup|modal|\\\\\\\"panel\\\" src` prints nothing; the unused-exports check prints nothing.\",\"e2e `codes.spec.ts` Your names tests, `robustness.spec.ts:299` \\\"a row Your names could not read is read again\u2026\\\" and `paid.spec.ts:526` pass in Chromium.\"],\"modelTier\":\"frontier\"}\n```"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"subject": "Task 9: Round-65 carry-over fixes",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
8
|
||||
],
|
||||
"description": "**Goal:** The four round-65 findings that survive the redesign hold in the new code and are pinned by tests: the neutral failed lead while the code check fails or is slow, the re-read of a \"Not checked\" row, the even wrap of a Monero address, and \"name.simplex\" folded in the field.\n\n**Files:**\n- Modify: `src/screens/order.ts` (gains `failedLead`), `src/views/order.ts` (uses it; old 1089-1091 deleted)\n- Modify: `test/screens-order.test.ts`, `test/rules.test.ts`\n- Modify: `e2e/paid.spec.ts:371`, `e2e/robustness.spec.ts:289,299`, `e2e/codes.spec.ts:234`\n\n**Acceptance Criteria:**\n- [ ] `failedLead` is a pure export of `src/screens/order.ts`; Vitest: invoice \u2192 \"Your payment is kept. Trying again charges nothing.\"; code refused \u2192 \"Your code can no longer pay for it, so trying again cannot register it.\"; prepaid with `codeKept` \u2192 \"Your code is kept. Trying again spends nothing more.\"; prepaid without an answer yet (slow or failed check) and no prepaid code \u2192 \"Trying again spends nothing more.\".\n- [ ] Vitest: `foldLabel(\"Bakery.simplex\")` is `\"bakery\"` and `foldLabel(\"\u200bbakery \")` is `\"bakery\"`.\n- [ ] e2e `codes.spec.ts:234` \"a failed code order is drawn before its code is checked\u2026\", `robustness.spec.ts:299` \"a row Your names could not read is read again\u2026\", `paid.spec.ts:371` \"a long address wraps into even lines\u2026\" (line-width ratio \u2265 `EVEN_LINES` at 1280 and 390 px) and `robustness.spec.ts:289` \"a name typed with .simplex\u2026\" pass in Chromium, Firefox and WebKit.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run test/screens-order.test.ts test/rules.test.ts # \u2192 passes\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/codes.spec.ts e2e/robustness.spec.ts e2e/paid.spec.ts -g \"drawn before its code|read again|even lines|typed with .simplex\" --project=chromium --project=firefox --project=webkit # \u2192 12 passed\n```\n\n```json:metadata\n{\"files\":[\"src/screens/order.ts\",\"src/views/order.ts\",\"test/screens-order.test.ts\",\"test/rules.test.ts\",\"e2e/paid.spec.ts\",\"e2e/robustness.spec.ts\",\"e2e/codes.spec.ts\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run test/screens-order.test.ts test/rules.test.ts && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test e2e/codes.spec.ts e2e/robustness.spec.ts e2e/paid.spec.ts -g \\\"drawn before its code|read again|even lines|typed with .simplex\\\" --project=chromium --project=firefox --project=webkit\",\"acceptanceCriteria\":[\"`failedLead` is a pure export of `src/screens/order.ts`; Vitest: invoice \u2192 \\\"Your payment is kept. Trying again charges nothing.\\\"; code refused \u2192 \\\"Your code can no longer pay for it, so trying again cannot register it.\\\"; prepaid with `codeKept` \u2192 \\\"Your code is kept. Trying again spends nothing more.\\\"; prepaid without an answer yet (slow or failed check) and no prepaid code \u2192 \\\"Trying again spends nothing more.\\\".\",\"Vitest: `foldLabel(\\\"Bakery.simplex\\\")` is `\\\"bakery\\\"` and `foldLabel(\\\"\u200bbakery \\\")` is `\\\"bakery\\\"`.\",\"e2e `codes.spec.ts:234` \\\"a failed code order is drawn before its code is checked\u2026\\\", `robustness.spec.ts:299` \\\"a row Your names could not read is read again\u2026\\\", `paid.spec.ts:371` \\\"a long address wraps into even lines\u2026\\\" (line-width ratio \u2265 `EVEN_LINES` at 1280 and 390 px) and `robustness.spec.ts:289` \\\"a name typed with .simplex\u2026\\\" pass in Chromium, Firefox and WebKit.\"],\"modelTier\":\"standard\"}\n```"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"subject": "Task 10: Tests, frame check and README",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
"7c"
|
||||
],
|
||||
"description": "**Goal:** Every remaining e2e spec matches the new screens, one Playwright check proves the track slide and screen swaps never show a near-empty frame in Chromium and Firefox 158, and the README describes the new layout and runs.\n\n**Files:**\n- Create: `e2e/frames.spec.ts`\n- Modify: `playwright.config.ts` (projects `chromium`, `firefox`, `webkit`, `firefox158`)\n- Modify: every `e2e/*.spec.ts` still failing on old selectors; `e2e/helpers.ts`\n- Modify: `README.md` (Layout, Run, Test sections)\n\n**Acceptance Criteria:**\n- [ ] `e2e/frames.spec.ts` passes in the `chromium` and `firefox158` projects and is excluded from `firefox` and `webkit`.\n- [ ] With a blank frame injected into `draw()` (Step 3), the frames spec fails in Chromium; with it reverted, it passes.\n- [ ] `rg -n \"motion|crossing|inplace|cuts|sweep|popup|Check a code|Continue to payment\" e2e playwright.config.ts` prints nothing.\n- [ ] `SLOTS=3 npx playwright test --project=chromium --project=firefox --project=webkit` reports 0 failed.\n- [ ] README's Test section gives `ulimit -c 0`, the `SLOTS=3 \u2026 dev/start.sh` line, `SLOTS=3 npx playwright test` and the `FIREFOX_158=\u2026 --project=firefox158` line, and no motion projects.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test --project=chromium --project=firefox --project=webkit # \u2192 \"N passed\", 0 failed\nFIREFOX_158=/nix/store/fzqp8k7z4y915fsdssb6x16b7iiqg2dm-firefox-158.0/bin/firefox SLOTS=3 npx playwright test --project=firefox158 # \u2192 1 passed\n```\n\n```json:metadata\n{\"files\":[\"e2e/frames.spec.ts\",\"playwright.config.ts\",\"e2e/*.spec.ts\",\"e2e/helpers.ts\",\"README.md\"],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test --project=chromium --project=firefox --project=webkit && FIREFOX_158=/nix/store/fzqp8k7z4y915fsdssb6x16b7iiqg2dm-firefox-158.0/bin/firefox SLOTS=3 npx playwright test --project=firefox158\",\"acceptanceCriteria\":[\"`e2e/frames.spec.ts` passes in the `chromium` and `firefox158` projects and is excluded from `firefox` and `webkit`.\",\"With a blank frame injected into `draw()` (Step 3), the frames spec fails in Chromium; with it reverted, it passes.\",\"`rg -n \\\"motion|crossing|inplace|cuts|sweep|popup|Check a code|Continue to payment\\\" e2e playwright.config.ts` prints nothing.\",\"`SLOTS=3 npx playwright test --project=chromium --project=firefox --project=webkit` reports 0 failed.\",\"README's Test section gives `ulimit -c 0`, the `SLOTS=3 \u2026 dev/start.sh` line, `SLOTS=3 npx playwright test` and the `FIREFOX_158=\u2026 --project=firefox158` line, and no motion projects.\"],\"modelTier\":\"standard\"}\n```"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"subject": "Task 11: Verification gate",
|
||||
"status": "pending",
|
||||
"blockedBy": [
|
||||
10
|
||||
],
|
||||
"description": "**Goal:** The redesign is proven by two clean full e2e runs in Chromium, Firefox 157, WebKit and Firefox 158, and by review rounds (chain, design against the landing, motion, code) repeated until every reviewer reports no issues, with source at about 5.5k lines.\n\n> **USER-ORDERED GATE \u2014 NON-SKIPPABLE.** This task was requested by the user in the current conversation. It MUST NOT be closed by walking around it, by declaring it \"verified inline\", or by substituting a cheaper check. Close only after every item in `acceptanceCriteria` has been re-validated independently, with output captured.\n\n**Files:**\n- Modify: only files a review finding requires, each fix as its own `webapp: <subject>` commit\n\n**Acceptance Criteria:**\n- [ ] Run 1 and run 2 of `SLOTS=3 npx playwright test --project=chromium --project=firefox --project=webkit` each end with \"0 failed\" (no \"failed\" or \"flaky\" line), output captured.\n- [ ] Run 1 and run 2 of `FIREFOX_158=\u2026 SLOTS=3 npx playwright test --project=firefox158` each report 1 passed, output captured.\n- [ ] Chain review: a reviewer drives a Bitcoin order, a card order, a code order, a taken-then-retarget order and a link edit against the locally deployed PR #36 contract and reports, from `GET /control/state` and anvil reads, that every registration's owner, expiry and records match what the page showed: \"no issues\".\n- [ ] Design review: a reviewer compares `SHOTS=1` screenshots of every screen (desktop 1280\u00d7900 and phone 390\u00d7844, light and dark, Chromium and Firefox 158) with `/project/git/simplex.domains/index.html` rendered at the same sizes and with the token block, and reports \"no issues\".\n- [ ] Motion review: a reviewer records video of search \u2192 checkout \u2192 payment \u2192 confirming \u2192 registered and Back, in Chromium and Firefox 158, and reports no blank frame, no flash and no layout jump: \"no issues\".\n- [ ] Code review: a reviewer checks the diff `7ed55cf..HEAD` against the Global Constraints (dead code, duplication, magic values, comments, determinism, warnings) and reports \"no issues\"; `npx tsc --noEmit` prints nothing; the unused-exports check prints nothing.\n- [ ] Rounds repeat after each fix until one round has all four reviewers reporting \"no issues\".\n- [ ] `fd -e ts -e css . src | xargs wc -l | tail -1` reports a total \u2264 5,600 (about 5.5k), and `wc -l < src/styles.css` \u2264 650.\n\n**Verify:**\n```bash\ncd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run && fd -e ts -e css . src | xargs wc -l | tail -1 && wc -l < src/styles.css # \u2192 total \u2264 5600; styles \u2264 650\nulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test --project=chromium --project=firefox --project=webkit 2>&1 | tee /tmp/gate-run1.log | tail -5 # \u2192 \"0 failed\"; repeat as run2\nFIREFOX_158=/nix/store/fzqp8k7z4y915fsdssb6x16b7iiqg2dm-firefox-158.0/bin/firefox SLOTS=3 npx playwright test --project=firefox158 2>&1 | tail -3 # \u2192 \"1 passed\"\n```\n\n```json:metadata\n{\"files\":[],\"verifyCommand\":\"cd /project/git/simplex.domains/webapp && npx tsc --noEmit && npx vitest run && fd -e ts -e css . src | xargs wc -l | tail -1 && wc -l < src/styles.css && ulimit -c 0 && source $S/tools/webkit-env.sh && SLOTS=3 npx playwright test --project=chromium --project=firefox --project=webkit 2>&1 | tee /tmp/gate-run1.log | tail -5 && FIREFOX_158=/nix/store/fzqp8k7z4y915fsdssb6x16b7iiqg2dm-firefox-158.0/bin/firefox SLOTS=3 npx playwright test --project=firefox158 2>&1 | tail -3\",\"acceptanceCriteria\":[\"Run 1 and run 2 of `SLOTS=3 npx playwright test --project=chromium --project=firefox --project=webkit` each end with \\\"0 failed\\\" (no \\\"failed\\\" or \\\"flaky\\\" line), output captured.\",\"Run 1 and run 2 of `FIREFOX_158=\u2026 SLOTS=3 npx playwright test --project=firefox158` each report 1 passed, output captured.\",\"Chain review: a reviewer drives a Bitcoin order, a card order, a code order, a taken-then-retarget order and a link edit against the locally deployed PR #36 contract and reports, from `GET /control/state` and anvil reads, that every registration's owner, expiry and records match what the page showed: \\\"no issues\\\".\",\"Design review: a reviewer compares `SHOTS=1` screenshots of every screen (desktop 1280\u00d7900 and phone 390\u00d7844, light and dark, Chromium and Firefox 158) with `/project/git/simplex.domains/index.html` rendered at the same sizes and with the token block, and reports \\\"no issues\\\".\",\"Motion review: a reviewer records video of search \u2192 checkout \u2192 payment \u2192 confirming \u2192 registered and Back, in Chromium and Firefox 158, and reports no blank frame, no flash and no layout jump: \\\"no issues\\\".\",\"Code review: a reviewer checks the diff `7ed55cf..HEAD` against the Global Constraints (dead code, duplication, magic values, comments, determinism, warnings) and reports \\\"no issues\\\"; Line truncated
|
||||
}
|
||||
],
|
||||
"lastUpdated": "2026-10-19T19:03:15.704506+00:00"
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
# Names webapp redesign: simpler, in the simplex.domains look
|
||||
|
||||
Status: approved direction (owner, 2026-10-19). Two details were taken as defaults while the owner was away and are easy to change: the "Your names" header link and the checkout block order.
|
||||
|
||||
## Goal
|
||||
|
||||
The names webapp (`simplex.domains/webapp`) looks like simplex.domains and works like the badges webapp (`simplex-chat/apps/simplex-badge-service/web`): one column, one action per screen, few screens, little code. Source goes from about 8.8k lines to about 5.5k.
|
||||
|
||||
## Decisions (owner, 2026-10-19)
|
||||
|
||||
| Topic | Decision |
|
||||
|---|---|
|
||||
| Visual design | simplex.domains tokens: colours, gradients, fonts, type scale, pill buttons, light and dark |
|
||||
| Home | The current simplex.domains page minus the countdown, plus the search |
|
||||
| Motion | The badges model: the purchase steps slide on one CSS track, other screens swap whole; no view transitions, no in-place diffing |
|
||||
| Purchase flow | Steps a buyer expects, one per screen, each centred and fitting the window without scrolling: Name (search) → Links (optional, Skip) → Your name code (save it; "I have saved it" enables Continue) → Check your order (summary incl. links, Pay with, "I have a code", Pay) → Payment → Registering → "is yours" (owner, 2026-10-19 evening) |
|
||||
| Prepaid codes | Entered at checkout and in Your names only; no header popup, no applied-code banner |
|
||||
| Links | Set before paying, in their own optional step; editable later on Edit links |
|
||||
| Taken after paying | The retarget flow stays |
|
||||
|
||||
## 1. Look and frame
|
||||
|
||||
Tokens come from `simplex.domains/index.html`. The badges app already maps them onto app components (`badges web/public/styles.css`), and this design follows that mapping.
|
||||
|
||||
- Fonts:
|
||||
- GT-Walsheim 700 for headings, with the vertical heading gradient (`#53C1FF -50%` → `#0053D0 160%`; cyan in dark).
|
||||
- Manrope for text: 18px body; 500 for leads, notes and soft text; 600 for buttons and uppercase labels at .18em tracking.
|
||||
- Colours:
|
||||
- Ground `#F3FAFF` with the landing's two radial gradients.
|
||||
- Ink `#0D1B3A`, soft ink at .651.
|
||||
- Primary `#0053D0`, accent `#0197FF`.
|
||||
- Surface white, surface line `rgba(0,83,208,.14)`.
|
||||
- Dark palette as `html.dark` on the landing (ground `#000832`, primary `#70F0F9`, surface `#0A1C44`).
|
||||
- Status colours (ok, warn, danger) as in the badges app.
|
||||
- Components:
|
||||
- Buttons: flat pill, radius 999px, padding 16px 32px, 18px/600, hover `brightness(1.06)`, full width up to the column.
|
||||
- Cards: 1px surface line, radius 24px, no shadow.
|
||||
- The QR card and the name code frame use the same card.
|
||||
- Inputs: pills like the landing's email field.
|
||||
- Frame:
|
||||
- The landing's header: logo and theme icon, not sticky, no bar, plus a "Your names" text link next to the theme icon on every page (default).
|
||||
- The landing's footer: "Pay with" marks and the links row.
|
||||
- Inner pages: one centred 640px column with a Back link above the heading.
|
||||
- Removed:
|
||||
- the sticky header bar and phone menu;
|
||||
- the grid background, the violet gradient, gradient-and-glow buttons and card shadows;
|
||||
- the "NEW" pill, the how-it-works tiles and their animated scene;
|
||||
- the five-step progress bar, skeleton shimmer and the `@property` theme easing.
|
||||
|
||||
## 2. Screens and flow
|
||||
|
||||
**Home** is the simplex.domains page with the countdown replaced by the search field:
|
||||
- the headline "Your SimpleX domain" and its subtitle;
|
||||
- the "Invest to claim your name" button and its note;
|
||||
- the footer.
|
||||
|
||||
The search answer appears under the field in the same column:
|
||||
- Available: name, price, term, Register.
|
||||
- Taken: who it points at.
|
||||
- Reserved, invalid, could not be checked: one line each.
|
||||
- Rate limited: the countdown line, Search disabled.
|
||||
|
||||
**Checkout** (one page), blocks in this order (default):
|
||||
1. Summary card: name, term, registered until, total.
|
||||
2. Optional links: contact address and channel, with their previews.
|
||||
3. Payment method: Card, Bitcoin, Monero.
|
||||
4. "I have a code": a field that, when valid, makes the total $0.
|
||||
5. Your name code: the code, Copy, and the "I have saved my name code" checkbox.
|
||||
6. One Pay button, enabled once the code is saved.
|
||||
|
||||
**Payment and registration**:
|
||||
- Crypto: QR, amount, address with Copy, the rate countdown.
|
||||
- Card: the Stripe form.
|
||||
- Then confirming and registering, with the stage tracker and its countdown kept.
|
||||
- Then "is yours": the name, until when, the name code again, and the next step in the app.
|
||||
|
||||
**Order outcomes**: one screen each, in the badges pattern (heading, one notice, one button):
|
||||
- "Invoice closed" replaces the six expired, cancelled, still-open and paid-late variants. It keeps the refund and "don't send more" wording where the invoice saw a payment.
|
||||
- "Card payment not confirmed" replaces card-failed and slow-card.
|
||||
- Taken after paying: the retarget flow, as today.
|
||||
- Failed registration: Try again, or the code refusal with a way back to search.
|
||||
- A rate limit is one screen with a countdown.
|
||||
- Unknown link, not on this device, unreadable: one screen each.
|
||||
|
||||
**Your names** is the rows from storage, read once when opened (no follow timers, no staggered rows). It also has:
|
||||
- Add a code;
|
||||
- Show code, inline in the row;
|
||||
- Edit links;
|
||||
- Restore;
|
||||
- Forget, which asks on the page itself, not in a `window.confirm`.
|
||||
|
||||
## 3. Code
|
||||
|
||||
- **Deleted:**
|
||||
- `src/motion.ts`, `src/motion-values.ts` and the view-transition and keyframe CSS;
|
||||
- the `{sections, update}` screen contract and its helpers (`drawAs`, `changeTo`, `syncList`, `live`, slot carry);
|
||||
- the `moves` transition tables and the route-to-motion matrix;
|
||||
- focus restoration in `src/focus.ts`: the new screen's heading takes focus, as in badges;
|
||||
- the popups (`src/views/codes.ts` as a popup), skeletons, button spinner crossfade, offline-note footer reservation, phone menu, and the step bar.
|
||||
- **Kept:**
|
||||
- `src/screens/*`: the pure reducers and screen-for-state functions, unit-tested, minus the transition tables.
|
||||
- Unchanged: `crypto/*`, `api.ts`, `flow.ts`, `store.ts`, `session.ts`, `qr.ts`, `stripe.ts`, `sw/`.
|
||||
- **Views:**
|
||||
- Each screen is a builder from state to nodes, swapped whole with `replaceChildren`.
|
||||
- Inputs the user is typing in (search, links, code) keep their node across redraws.
|
||||
- The purchase steps sit on one track that slides by CSS transform, as `badges main.ts:265-466`.
|
||||
- **Target:** about 5.5k lines of source, with `styles.css` at about 600 lines.
|
||||
|
||||
## 4. Tests and verification
|
||||
|
||||
- **Vitest:** reducers and screen functions keep their tests, minus the transition-table tests.
|
||||
- **e2e:**
|
||||
- Kept: the functional specs (search, codes, paid, taken, robustness, offline, focus basics), updated to the new screens.
|
||||
- Deleted: `motion`, `cuts`, `crossing`, `sweep` and the four `inplace` specs.
|
||||
- Added: one check that the track slide and screen swaps show no blank frame, run in Chromium and Firefox 158.
|
||||
- **Review rounds:** run again on the result until every reviewer reports no issues:
|
||||
- e2e in Chromium, Firefox 157 and WebKit;
|
||||
- chain against the local contract;
|
||||
- design against the landing in Chromium and Firefox 158;
|
||||
- motion;
|
||||
- code.
|
||||
|
||||
The round-65 findings that survive the redesign are carried into it:
|
||||
- a failed or slow code check shows the neutral "Trying again spends nothing more." line;
|
||||
- a "Not checked" row is read again;
|
||||
- Monero addresses wrap evenly;
|
||||
- "name.simplex" is folded in the field.
|
||||
Reference in new issue
Block a user