Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions .claude/agents/design-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ Cite the DESIGN.md section for every finding. If a rule is not in DESIGN.md and

1. **Token adherence** (Tokens, Derivation rules): semantic tokens only. The Copy Register gate (`tools/check-copy.mjs`) scans tracked web and mobile source for raw brand-accent literals and rejects findings outside the committed `tools/copy-baseline.json` ratchet; the accent-AA test covers the contrast floors. You catch the rest: raw `--slate-*` refs (Derivation rule 7), a new font family / radius / colour outside the spec, `transition-all`, `h-screen`, borders where the kit uses inset rings. **Accent rationing** (Identity & anchor) is yours and is the one most often broken: the accent belongs to the active tab, progress and ring indicators, done dots, the primary CTA, the FAB, and active nav; *that is the whole list*. It is never decorative on a card, row, border, heading, or a non-state icon. Flag `--primary` used as small text on the canvas (that is `--primary-soft`).
2. **The three shipping tests** (Working model) — run all three, not just the last.
- **AI-slop test:** any decorative gradient (`--gradient-header` and `GradientTop` are deleted, so there is no sanctioned gradient left to be outside of) including gradient borders and gradient text, any glow, decorative background orbit arcs, a coloured side-stripe border, cards-in-cards, connector or tree lines, gray text on coloured backgrounds, rounded-square icon tiles above headings, oversized centered H1 outside hero, and the hero-metric template used as decoration.
- **AI-slop test:** any decorative gradient (`--gradient-header` and `GradientTop` are deleted, so there is no sanctioned gradient left to be outside of) including gradient borders and gradient text, any glow, decorative background orbit arcs, a coloured side-stripe border, cards-in-cards, connector or tree lines, gray text on coloured backgrounds, rounded-square icon tiles above headings, oversized centered H1 outside hero, and the hero-metric template used as decoration. **Five newer tells carry the same weight:** a **glass or Liquid Glass material** or a frosted panel used as a look; a **sparkle icon** used as an AI or default marker (identity is the Astra glyph); a **pill used as a CTA outside `PillButton`**, or any pill in a hero, or a static element wearing the pill radius (radius 999 means pressable, a badge is radius 8); a **default component-library theme or a default white accent** on any surface, marketing included; and a **descriptive subtitle** beneath a heading, label, card, or settings row that restates it. That last one is the most common copy-slop shape in agent-written UI and it walks past every gate in the repo.
- **Squint test:** blur the surface — one focal element should survive per view.
- **Scene-sentence test:** describe the rendered surface in one sentence. If it reads like generic SaaS ("a clean modern dashboard with cards"), say so and name what would make it read as Orbit. **There is no glow and no gradient wash** (Bans) — identity is carried by the logo mark, the Astra glyph, ring indicators, and a rationed accent on a near-black canvas.
3. **Restraint has a floor** (Identity & anchor; vault ADR) — the failure mode opposite to slop. A quieting pass must not flatten every element to one size and weight, go grayscale, strip all personality, or trade an affordance for calm. Reduce drama, keep the point of view. Flag a surface that has become generic in the name of restraint.
4. **Layout & composition** (Layout & spacing, Surface rules, Type roles) — the 65ch measure (Measure and wrapping); spacing rhythm (tight within a group, air between groups); **concentric radii** (outer = inner + padding — a parent and its inset child never share a radius); **optical alignment** (chevrons, play triangles, leading plus glyphs, and a glyph in a circular well need a 1-2px nudge off geometric center); "a card is not a layout primitive"; one focal element per view; the ≤4 options ceiling; blur and glass restraint.
5. **Motion** (Motion) — the **frequency gate** and the **closed purpose list** (Whether); the **remediation order** (How); the **delight budget**. `local/no-overshoot-easing` and `local/will-change-discipline` cover the mechanical half only — the judgement half (does this motion earn its place at this frequency?) is yours. `local/animate-presence-exit` covers only the statically decidable half; an `exit` whose `AnimatePresence` ancestor lives at another call site is invisible to it, so verify the pairing by reading. **`local/no-arbitrary-zindex` is not implemented** (the semantic stacking scale does not exist yet, #539) — until it does, flag arbitrary `z-*` values by eye.
6. **States & copy** (States, Copy): the loading/empty/error **triad** and the **full state set**; copy naming and **"say it once"**; error content and placement; **confirmation-dialog warrant** (does this action deserve a dialog?); **eyebrow rationing**. The Copy Register gate (`tools/check-copy.mjs`) scans i18n string values, deliberately not keys, for shouted strings and the cliché register and rejects findings outside the committed `tools/copy-baseline.json` ratchet; do not re-flag those. Do flag copy that is merely *wordy*, redundant with its heading, or inconsistent in terminology across the two locales (`.claude/rules/product-and-content.md`, rule 5).
4. **Layout & composition** (Layout & spacing, Surface rules, Type roles) — the 65ch measure (Measure and wrapping); **the 2x gap rule** (the gap between two groups is at least twice the gap within a group, or the grouping reads as noise); **concentric radii** (outer = inner + padding, and treat layers as independent once the padding exceeds 24px); **optical alignment** (chevrons, play triangles, leading plus glyphs, and a glyph in a circular well need a 1-2px nudge off geometric center; a trailing-icon pill takes 2px less padding on the icon side); "a card is not a layout primitive"; one focal element per view; the ≤4 options ceiling; blur and glass restraint; **D34, exactly one separation device per boundary** (a separator is never paired with a large gap, because the gap already did the job).
4b. **Icons** (Shape, shadow, motion, icons) — sizes are the set's native grid, **16 / 20 / 24 only**, default 24; an off-grid size such as 22 renders soft. Stroke matches the adjacent text weight: 1.5 beside 400, 2 beside 500 and 600, 2.5 beside 700. One icon set and one stroke strategy per surface, `currentColor` for states, outline as default and fill for active.
5. **Motion** (Motion) — the **frequency gate** and the **closed purpose list** (Whether); the **remediation order** (How); the **delight budget**. `local/no-overshoot-easing` and `local/will-change-discipline` cover the mechanical half only — the judgement half (does this motion earn its place at this frequency?) is yours. `local/animate-presence-exit` covers only the statically decidable half; an `exit` whose `AnimatePresence` ancestor lives at another call site is invisible to it, so verify the pairing by reading. `local/no-arbitrary-zindex` ships and the semantic stacking scale exists (DESIGN.md `## Stacking`), so do not re-flag what it catches; a tier used at the wrong level is still yours.
6. **States & copy** (States, Voice): the loading/empty/error **triad** and the **full nine-state set** (default, hover, focus, active, disabled, loading, error, empty, **at capacity**); a boundary is not an error, so an at-capacity state uses neutral tokens and carries no upgrade call to action. The **listing thresholds** (Listing) are checkable: 0 / 1 / few / many / too many, and a collection that can exceed 20 declares its behaviour before it ships, never as a wrapping pill row. Copy naming and **"say it once"**; the **supporting-copy ban** and its form-field carve-out; error content and placement; **confirmation-dialog warrant** (does this action deserve a dialog?); **eyebrow rationing**. The Copy Register gate (`tools/check-copy.mjs`) scans i18n string values, deliberately not keys, for shouted strings and the cliché register and rejects findings outside the committed `tools/copy-baseline.json` ratchet; do not re-flag those. Do flag copy that is merely *wordy*, redundant with its heading, or inconsistent in terminology across the two locales (`.claude/rules/product-and-content.md`, rule 5).
7. **Frozen & special surfaces** (Habit list, Special surfaces) — the **habit-list treatment** and the **paywall shape** are frozen; flag any drift. **Flow-shape parity with neighbours**: a new flow that does not match the IA of the flows beside it is a conceptual misalignment, not a one-off — name the root cause (`.claude/rules/product-and-content.md`, rule 3).
8. **Buttons — width, brevity, icons** (Buttons) — full-bleed pills are AI slop: a pill hugs its content unless it is in the allowlist (the single primary action of a mobile bottom-sheet/dialog, a mobile form submit in auth/onboarding, a full-screen empty-state CTA, or a `ConfirmDialog` paired action row). `local/no-fullbleed-button` catches web `fullWidth`/`w-full`/`flex-1`, but **cannot see mobile StyleSheet width** (`alignSelf: 'stretch'` / `width: '100%'`) — flag stretched mobile pills by eye. Also flag over-wordy labels (a verb the dialog title already frames: "Log all"→"Log", "Registrar todos"→"Registrar") and commit CTAs missing their canonical leading glyph (create→plus, confirm→check, destructive→trash). Taxonomy is `primary`/`secondary`/`ghost`/`destructive` × `sm`/`md`/`lg` — flag a raw ad-hoc pill that should be `PillButton`.
8. **Buttons — width, brevity, icons** (Buttons) — full-bleed pills are AI slop: a pill hugs its content unless it is in the allowlist (the single primary action of a mobile bottom-sheet/dialog, a mobile form submit in auth/onboarding, a full-screen empty-state CTA, or a `ConfirmDialog` paired action row). `local/no-fullbleed-button` catches web `fullWidth`/`w-full`/`flex-1`, but **cannot see mobile StyleSheet width** (`alignSelf: 'stretch'` / `width: '100%'`) — flag stretched mobile pills by eye. Also flag over-wordy labels (a verb the dialog title already frames: "Log all"→"Log", "Registrar todos"→"Registrar") and commit CTAs missing their canonical leading glyph (create→plus, confirm→check, destructive→trash). Taxonomy is `primary`/`secondary`/`ghost`/`destructive`/`caution` × `xs`/`sm`/`md`/`lg` — flag a raw ad-hoc pill that should be `PillButton`. A submit stays **enabled** until the request starts, then disables with a spinner **keeping its original label**, because the label is what tells assistive tech which button is busy.

8b. **Overlay** (Overlay) — an overlay is **content-height by default**; a fixed detent is allowed only where the content is genuinely long, never as the default. **Exactly one scroll container, owned by the primitive**: a caller that nests its own scroll view inside a scrollable sheet is a Blocker, and so is an overlay that opens scrolled away from its own first line. The panel is opaque, because a translucent panel over arbitrary content cannot have its contrast checked. Web wraps `@base-ui/react` `Dialog`; mobile wraps TrueSheet. Flag any hand-rolled focus trap, scroll lock, or portal added beside them.
9. **A11y — the judgement half** (Accessibility) — **colour as the only signal** (Perception); the **3:1 non-text contrast floor** for icons, borders, and state indicators; **focus management in overlays** (Keyboard and focus) — where focus lands on open, that it is trapped, and where it returns on close; **reduced-transparency and reduced-contrast handling**. Icon-only controls need a localized accessible label in BOTH locales; hit targets ≥44 (reach the minimum by padding, not by growing the glyph). The `jsx-a11y` rules and `local/require-focus-replacement` cover the static half — semantics and announcement *quality* are yours. Note that 200% zoom, keyboard traps, and screen-reader semantics need the live DOM and belong to the proposed a11y CI gate, not to you (DESIGN.md "Not enforceable here") — say so rather than guessing.
10. **Cross-platform parity** — a UI change in `apps/web` or `apps/mobile` must have its mirror. Defer the deep check to `parity-checker`, but flag an obvious one-sided change.
11. **Responsive + orientation** (Desktop density & orientation, Sub-screen navigation) — desktop composes horizontally (no single stretched mobile column); pills are intrinsic-width at desktop; every sub-screen has a visible back affordance.

**Staged gates.** `local/no-decorative-glow` and `local/no-raw-gradient` ship at `warn` pending bundle 5's cleanup, so the existing violations are known debt — do not report them as new findings. A **newly introduced** glow or raw gradient in this diff is still a Blocker: the token is deleted and the ban is settled.
**Staged gates.** `local/no-decorative-glow` and `local/no-raw-gradient` ship at `warn` until the `eslint-rules/` re-derivation lands (the PR after human grant 1), so the existing violations are known debt — do not report them as new findings. A **newly introduced** glow or raw gradient in this diff is still a Blocker: the token is deleted and the ban is settled. The same applies to the spacing scale: its step values are **provisional until grant 1**, so do not flag an off-scale value against a scale that has not been chosen yet.

**The colour system changed on 2026-08-15.** One scheme, not six. A deep space-blue accent, not violet. `--fg-on-primary` is the canvas ink and **not white**, because white on the accent measures 2.41:1. `--status-frozen` is a neutral plus the snowflake glyph and no longer a hue. Flag any surviving reference to six schemes, a violet accent, `Rubik`/`Inter`/`Roboto`, or a cyan frozen dot.

## Screenshot critique

Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/redesign-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Redesign drift control

# D36: the redesign lands on the long-lived `redesign/main` integration branch, not on
# `main`. That branch therefore drifts behind `main` every time anything else merges,
# and the drift is invisible until an R ticket hits a conflict weeks later.
#
# This job merges `main` into `redesign/main` on every push to `main`, so the
# integration branch is never more than one commit behind. It uses a real merge and
# never a rebase or a force push, because `redesign/main` is shared: every open R-ticket
# branch is cut from it, and rewriting its history would strand all of them.
#
# When the merge conflicts, the job fails loudly and stops. Resolving a conflict is a
# human decision about two real changes, and a machine picking a side silently is how a
# redesign quietly loses a fix that landed on main.

on:
push:
branches: [main]
Comment thread
thomasluizon marked this conversation as resolved.
Resolved
Comment thread
thomasluizon marked this conversation as resolved.
Resolved
workflow_dispatch:

permissions:
contents: write

concurrency:
group: redesign-drift
cancel-in-progress: false

jobs:
merge-main-into-redesign:
runs-on: ubuntu-latest
steps:
- name: Check out the full history
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Skip when the integration branch does not exist
id: exists
run: |
if git ls-remote --exit-code --heads origin redesign/main >/dev/null 2>&1; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "redesign/main does not exist yet; nothing to keep in sync."
fi

- name: Merge main into redesign/main
if: steps.exists.outputs.present == 'true'
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git switch redesign/main

if git merge-base --is-ancestor origin/main HEAD; then
echo "redesign/main already contains main. Nothing to do."
exit 0
fi

if ! git merge --no-ff --no-edit origin/main; then
git merge --abort || true
echo "::error::main does not merge cleanly into redesign/main. Resolve it by hand; this job never picks a side."
exit 1
fi

git push origin redesign/main
echo "Merged main into redesign/main."
Loading