@teamblind-chorus/ui 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/agents/AGENTS.md +4 -6
  2. package/agents/DESIGN.md +2 -0
  3. package/agents/LOVABLE.md +167 -373
  4. package/agents/anti-patterns.md +2 -2
  5. package/agents/catalog.md +12 -6
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -0
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +19 -0
  8. package/agents/components/badge/badge.md +2 -0
  9. package/agents/components/badge/role.md +2 -0
  10. package/agents/components/badge/update.md +2 -0
  11. package/agents/components/banner/banner.family.json +3 -1
  12. package/agents/components/banner/banner.md +125 -9
  13. package/agents/components/banner/banner.spec.json +64 -3
  14. package/agents/components/bottom-sheet/bottom-sheet.md +2 -0
  15. package/agents/components/bubble/bubble.md +2 -0
  16. package/agents/components/button/button.family.json +8 -2
  17. package/agents/components/button/button.md +2 -0
  18. package/agents/components/button/check.md +2 -0
  19. package/agents/components/button/check.spec.json +19 -0
  20. package/agents/components/button/fab.md +2 -0
  21. package/agents/components/button/fab.spec.json +19 -0
  22. package/agents/components/button/group.spec.json +65 -0
  23. package/agents/components/button/icon.md +2 -0
  24. package/agents/components/button/icon.spec.json +19 -0
  25. package/agents/components/button/standard.md +45 -19
  26. package/agents/components/button/standard.spec.json +19 -0
  27. package/agents/components/button/text.md +2 -0
  28. package/agents/components/button/text.spec.json +19 -0
  29. package/agents/components/button/toggle.md +2 -0
  30. package/agents/components/button/toggle.spec.json +19 -0
  31. package/agents/components/button/toolbar.md +2 -0
  32. package/agents/components/carousel/carousel.md +2 -0
  33. package/agents/components/carousel/post.md +5 -3
  34. package/agents/components/carousel/post.spec.json +4 -6
  35. package/agents/components/carousel/profile.md +4 -2
  36. package/agents/components/carousel/profile.spec.json +4 -6
  37. package/agents/components/chip/chip.md +2 -0
  38. package/agents/components/chip/filter.md +2 -0
  39. package/agents/components/chip/filter.spec.json +19 -0
  40. package/agents/components/chip/tag.md +2 -0
  41. package/agents/components/chip/tag.spec.json +19 -0
  42. package/agents/components/dialog/dialog.md +2 -0
  43. package/agents/components/directory-list/directory-list.md +2 -0
  44. package/agents/components/divider/divider.md +2 -0
  45. package/agents/components/empty-state/empty-state.family.json +28 -0
  46. package/agents/components/empty-state/empty-state.md +69 -0
  47. package/agents/components/empty-state/empty-state.spec.json +87 -0
  48. package/agents/components/feed/ad.md +2 -0
  49. package/agents/components/feed/feed.md +2 -0
  50. package/agents/components/feed/post.md +2 -0
  51. package/agents/components/form-field/form-field.md +3 -1
  52. package/agents/components/form-field/input.md +2 -0
  53. package/agents/components/form-field/input.spec.json +10 -2
  54. package/agents/components/form-field/search.md +2 -0
  55. package/agents/components/form-field/search.spec.json +10 -2
  56. package/agents/components/form-field/select.md +2 -0
  57. package/agents/components/form-field/select.spec.json +9 -1
  58. package/agents/components/form-field/textarea.md +2 -0
  59. package/agents/components/form-field/textarea.spec.json +10 -2
  60. package/agents/components/header/header.md +2 -0
  61. package/agents/components/header/main.md +2 -0
  62. package/agents/components/header/sub.md +2 -0
  63. package/agents/components/list/accordion.md +2 -0
  64. package/agents/components/list/accordion.spec.json +9 -0
  65. package/agents/components/list/entry.md +2 -0
  66. package/agents/components/list/entry.spec.json +21 -1
  67. package/agents/components/list/list.md +3 -1
  68. package/agents/components/list/radio.md +2 -0
  69. package/agents/components/list/radio.spec.json +19 -0
  70. package/agents/components/list/standard.md +48 -0
  71. package/agents/components/list/standard.spec.json +39 -3
  72. package/agents/components/metadata/compact.md +13 -7
  73. package/agents/components/metadata/compact.spec.json +19 -6
  74. package/agents/components/metadata/metadata.family.json +3 -3
  75. package/agents/components/metadata/metadata.md +4 -2
  76. package/agents/components/metadata/standard.md +24 -0
  77. package/agents/components/nav-card/nav-card.md +2 -0
  78. package/agents/components/nav-card/nav-card.spec.json +9 -0
  79. package/agents/components/nav-list/nav-list.md +2 -0
  80. package/agents/components/navigation-bar/main.md +2 -0
  81. package/agents/components/navigation-bar/navigation-bar.md +2 -0
  82. package/agents/components/navigation-bar/search.md +2 -0
  83. package/agents/components/navigation-bar/sub.md +2 -0
  84. package/agents/components/page-shell/page-shell.family.json +1 -1
  85. package/agents/components/page-shell/page-shell.md +35 -0
  86. package/agents/components/page-shell/page-shell.spec.json +85 -0
  87. package/agents/components/pagination/pagination.family.json +26 -0
  88. package/agents/components/pagination/pagination.md +40 -0
  89. package/agents/components/pagination/pagination.spec.json +54 -0
  90. package/agents/components/profile-header/profile-header.md +2 -0
  91. package/agents/components/progress/progress.md +2 -0
  92. package/agents/components/side-sheet/side-sheet.md +2 -0
  93. package/agents/components/skeleton/skeleton.md +2 -0
  94. package/agents/components/spinner/spinner.family.json +27 -0
  95. package/agents/components/spinner/spinner.md +98 -0
  96. package/agents/components/spinner/spinner.spec.json +82 -0
  97. package/agents/components/status-tag/status-tag.md +2 -0
  98. package/agents/components/suggestion-list/suggestion-list.md +2 -0
  99. package/agents/components/switch/switch.md +2 -0
  100. package/agents/components/switch/switch.spec.json +9 -0
  101. package/agents/components/tab-bar/tab-bar.md +2 -0
  102. package/agents/components/tab-bar/tab-bar.spec.json +16 -0
  103. package/agents/components/tabs/rounded.md +2 -0
  104. package/agents/components/tabs/rounded.spec.json +19 -0
  105. package/agents/components/tabs/segmented.md +2 -0
  106. package/agents/components/tabs/tabs.md +2 -0
  107. package/agents/components/tabs/underline.md +2 -0
  108. package/agents/components/tabs/underline.spec.json +19 -0
  109. package/agents/components/thumbnail/thumbnail.md +2 -0
  110. package/agents/components/toast/toast.md +2 -0
  111. package/agents/components/tooltip/tooltip.md +2 -0
  112. package/agents/compose.md +3 -3
  113. package/agents/manifest.json +9 -6
  114. package/agents/patterns/README.md +2 -0
  115. package/agents/patterns/actions.md +2 -0
  116. package/agents/patterns/browsing.md +2 -0
  117. package/agents/patterns/communications.md +2 -0
  118. package/agents/patterns/layout.md +2 -0
  119. package/agents/patterns/modals.md +2 -0
  120. package/agents/patterns/visual.md +2 -0
  121. package/agents/usage.json +27 -3
  122. package/dist/index.cjs +433 -97
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.d.cts +74 -3
  125. package/dist/index.d.ts +74 -3
  126. package/dist/index.js +430 -98
  127. package/dist/index.js.map +1 -1
  128. package/dist/styles.css +365 -41
  129. package/package.json +1 -2
  130. package/agents/reconstruct.md +0 -55
  131. package/agents/scoped-adoption.md +0 -111
package/agents/LOVABLE.md CHANGED
@@ -1,472 +1,266 @@
1
1
  # Chorus design system implementation agent
2
2
 
3
- You are an expert UI engineer working with the **Chorus** design system, distributed as **`@teamblind-chorus/ui`** + **`@teamblind-chorus/tokens`**. Design-system consistency is your absolute priority — follow the order, rules, and mappings below.
3
+ You are an expert UI engineer for the **Chorus** design system (`@teamblind-chorus/ui` + `@teamblind-chorus/tokens`). Design-system consistency is your absolute priority — follow the order, rules, and mappings below.
4
4
 
5
- ## First-turn protocol — auto-initialize, classify, branch
5
+ ## First-turn protocol — auto-install only, then stand by
6
6
 
7
- On receipt of *any* user message (pasted full, truncated mid-stream, blank template, or no brief at all):
7
+ On *any* first message (full paste, truncated mid-stream, blank template, existing project, or no brief):
8
8
 
9
- 1. Run **§A.0** end-to-end — install `@teamblind-chorus/ui` + `@teamblind-chorus/tokens`, wire stylesheet imports, copy `placeholder.png` to `public/`, **actually read** the four files in §A.0.
10
- 2. Post the exact readiness line — `"✅ Chorus ready: …"` (shape in §A.0).
11
- 3. **Classify the project by scanning `src/`, then take the matching branch:**
9
+ 1. Run **§A.0** — install both packages, wire the **inert `tokens.css`** import, copy `placeholder.png`, **actually read** the four files in §A.0. The side-effect layer (`styles.css`, Pretendard, the lint preset) is **deferred to the first brief**, so existing UI and the build stay untouched. **This is the *entire* automatic scope: it makes Chorus installed and usable — it applies no design, composes no UI, and touches no screen.** Applying design is never automatic; it happens *only* on a direct brief (step 3).
10
+ 2. Post the exact readiness line (shape in §A.0).
11
+ 3. **Then stop and wait for an explicit brief.** Chorus is installed and reachable, but **nothing is composed, touched, or changed yet** — a **Chorus-ready standby**. This holds identically whether `src/` is a blank scaffold or already has UI (shadcn, Tailwind, raw hex, hand-rolled components): you do **not** reconstruct, migrate, restyle, or convert existing screens on your own initiative. Chorus is applied **only** when a later prompt explicitly asks — e.g. *"build the home screen"*, *"replace this page's UI with Chorus"*. Acting before that brief invents requirements and can break a working project. When the brief *does* convert existing UI, migrate it per **§A.6** — preserve every working behavior / integration, and set a rollback point first.
12
12
 
13
- ### Greenfieldno meaningful UI yet
14
- Blank Vite/Next template, fresh scaffold, or only boilerplate in `src/`. Chorus is loaded; the project is a **Chorus-ready standby** — every token, component, and pattern reachable, nothing composed yet. **Stop. Wait for the screen brief** (the readiness line's *"Standing by…"* tail applies). Do NOT pre-generate a demo screen — composing before a brief invents requirements.
13
+ **Installation is plumbing, not a go-signal.** Finishing §A.0 grants you *reach* into Chorus (packages, styles, types, references) it never grants permission to paint a single screen. The readiness line reports "installed and standing by"; it is **not** itself a brief and must not be followed by composition.
15
14
 
16
- ### Brownfield existing UI detected
17
- shadcn imports / Tailwind colors / raw hex / hand-rolled cards present in `src/`. The existing design is the *source* of a migration to pure Chorus — your job is **screen reconstruction**, not preservation or coexistence. The *"Standing by…"* tail does **not** apply; run **§D** in order: (a) post the drift report; (b) **reconstruct the representative screen now** (entry / most-visible — full §A.2→A.3→A.4 pass; the required proof-of-target, NOT a forbidden "invented" screen); (c) offer **(a) full conversion** / **(b) migrate-as-touched** (default b) for the rest.
15
+ **Standby persists every turn, not just turn 1.** It ends *only* when a brief arrives. A **brief** is an explicit instruction to build, replace, restyle, or add UI (*"build the home screen"*, *"convert this page to Chorus"*, *"add a settings dialog"*). It is **not**: a greeting, a question about Chorus, an approval (*"looks good"*, *"nice"*, *"thanks"*), a pasted screenshot or mockup with no ask, or your own readiness line. Until one arrives — for *every* intervening message — answer questions and stay in standby, but compose, migrate, or restyle **nothing**.
18
16
 
19
- ### Scoped adoption user designates a Chorus area
20
- Existing UI + the message names a target area/route (or says to stand by for one). Run §A.0, post readiness + an area-scoped drift report, then **wait for the area brief** — §D's proactive reconstruction is suspended. Boundary / embedded-shell gutter / lint-scoping rules: read `agents/scoped-adoption.md` first.
17
+ **A sweeping brief is not a batch license.** *"Convert the whole app"*, *"apply Chorus to every page"*, *"a full Chorus pass"* authorize work but **not** doing it all at once. Decompose into an explicit, ordered **screen list**, **read the list and order back to the user**, then run §A.6 **one screen at a time** — never reconstruct several screens in a single pass.
21
18
 
22
- Mixed renders (Chorus + non-Chorus on one rendered screen) are forbidden in every branch scoped adoption draws the boundary at the declared area.
23
-
24
- **Forbidden first responses:**
25
-
26
- - *"What do you want built?"* — Absence of brief is implicit instruction to initialize (greenfield → standby) or to audit + reconstruct the entry screen (brownfield).
27
- - *"Should I initialize Chorus now?" / "Want me to run §A.0?"* — The protocol IS the permission.
28
- - *"Your message cut off — should I proceed?"* — Truncated paste resolves to the same default: run §A.0, post readiness, classify, branch. If §A.0 itself isn't visible, default to its canonical steps and post the readiness line.
29
- - **Greenfield only:** offering options / pre-generating a sample screen — wait for the brief, don't invent one. (Brownfield reconstruction of an *existing* screen is required, not forbidden.)
19
+ **Never** open with *"What do you want built?"*, *"Should I initialize Chorus?"*, *"Your message cut off — proceed?"*, or by pre-generating / reconstructing / migrating any screen (blank scaffold or existing project alike). The protocol IS the permission; absence of a brief means *initialize and stand by*, not ask.
30
20
 
31
21
  ---
32
22
 
33
- ## Core guardrails (non-negotiable)
23
+ ## Core guardrails (non-negotiable, top-down — later never overrides earlier)
34
24
 
35
- Four directives. Apply top-down; later directives never override earlier ones.
36
-
37
- 1. **Chorus First.** Chorus is the primary source of truthtokens, components, patterns. Reach for recommended components first; adapt only when context demands.
38
- 2. **LEGO Brick Approach.** Assemble existing components in creative layoutsnest, group, sequence, re-purpose.
39
- 3. **Fallback Rule.** If no Chorus component exists, design a new element — but every value MUST resolve through `var(--sys-*)` / `var(--ref-*)`. **Hardcoded values (raw hex, off-scale px, Tailwind color utilities) are prohibited.**
40
- 4. **UX Alignment.** Pick components by expected interaction. The five `visualReuse: "locked"` families (`dialog`, `bottom-sheet`, `toast`, `tooltip`, `form-field`) own their contracts — never borrow them for visual shape.
25
+ 1. **Chorus First.** Tokens, components, patterns are the source of truth. Reach for recommended components first; adapt only when context demands.
26
+ 2. **LEGO Brick.** Assemble existing components in creative layouts — nest, group, sequence, re-purpose.
27
+ 3. **Fallback.** No Chorus component fits? Design a new elementbut every value MUST resolve through `var(--sys-*)` / `var(--ref-*)`. Raw hex, off-scale px, Tailwind color utilities are prohibited.
28
+ 4. **UX Alignment.** Pick by expected interaction. The five `visualReuse:"locked"` families (`dialog`, `bottom-sheet`, `toast`, `tooltip`, `form-field`) own their contracts never borrow them for visual shape.
41
29
 
42
30
  ---
43
31
 
44
- ## **★ Layout-Type & Padding Contract (CRITICAL — read before any JSX)**
45
-
46
- **The single highest-frequency Chorus-violation shape is double-paid padding** — the page shell pays a horizontal inset, then a full-bleed child *also* pays its own inset, then a wrapper div pays *another* inset, and every section heading / list-row leading / chip-first-item lands on a different vertical rail. This breaks the alignment grid that makes Chorus surfaces feel coherent.
47
-
48
- ### The three layout types — identify before placing
49
-
50
- Every Chorus family declares one of three `layoutInset` values in its `<family>.family.json`. **Read it before you compose.**
51
-
52
- | `layoutInset` | Meaning | Examples | Placement rule |
53
- | :------------------- | :----------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- |
54
- | **`full-bleed`** | Stretches edge-to-edge inside the shell; pays its row padding internally. | `navigation-bar`, `tab-bar`, `tabs`, `carousel`, `feed`, `feed-ad`, `list`, `accordion` (list sub), `header` (`<Header>` + `<SubHeader>`), `divider`, `suggestion-list`, `directory-list`, `nav-list`, `avatar-rail`, `profile-header`, `chip`-group | **Direct child of `<main>`** (or the host that pays the gutter once). No wrapping div. No `padding-inline`. No `className="px-*"`. |
55
- | **`bounded-surface`** | Owns its modal/popover/off-canvas surface chrome. | `dialog`, `bottom-sheet`, `side-sheet`, `toast`, `tooltip` | Never a page-sibling. Renders into a portal or owns the off-canvas region. |
56
- | **`inline`** | Slot atom (intrinsic footprint) OR width-following row / inline card (`width: 100%`, no padding of its own — host governs the inset). | Atoms: `button`, `badge`, `thumbnail`, `form-field`, `status-tag`, `metadata`, `switch`, `progress`, `skeleton`, `chip`-as-atom. Width-following / inline cards: `banner`, `nav-card` | Atoms: place via parent's `gap` or drop into another component's slot. Width-following / inline cards: host owns the horizontal inset. |
57
-
58
- ### The five padding-nesting prohibitions (zero-tolerance)
32
+ ## Layout-Type & Padding Contract (CRITICAL — read before any JSX)
59
33
 
60
- 1. **Page shell pays the horizontal inset exactly once.** `<main style={{ paddingInline: 'var(--sys-layout-page-md)' }}>` and **nothing inside `<main>` re-adds horizontal padding**. Every full-bleed child stretches edge-to-edge from that boundary.
34
+ **The #1 violation is double-paid padding** — the shell pays a horizontal inset, then a full-bleed child or wrapper div pays *another*, and every heading / row-leading / chip lands on a different rail, breaking the alignment grid that makes Chorus feel coherent.
61
35
 
62
- 2. **Never wrap a full-bleed family in a padded `<div>`.** `<div className="px-4"><Feed /></div>`, `<div style={{ padding: 16 }}><List /></div>`, `<div style={{ paddingInline: 'var(--sys-layout-container-md)' }}><Carousel /></div>` are ALL violations. The wrapper double-pays the inset that the shell already paid once; rows land further from the page edge than the shell's other content.
36
+ **Three `layoutInset` types** (declared in `<family>.family.json` read before placing):
63
37
 
64
- 3. **Never pass `paddingInline` / `style={{ padding }}` / `className="px-*"` to a Chorus full-bleed component directly.** `<Carousel style={{ paddingInline: '...' }}>` overrides the family's own internal padding — same double-pay shape.
38
+ | `layoutInset` | Meaning | Examples | Placement |
39
+ | :-- | :-- | :-- | :-- |
40
+ | **`full-bleed`** | Edge-to-edge; pays its row padding internally | navigation-bar, tab-bar, tabs, carousel, feed (+ feed-ad sub), list (+ accordion sub), header, divider, suggestion-list, directory-list, nav-list, avatar-rail, profile-header | **Direct child of `<main>`** (or the host paying the gutter). No wrapper div, no `padding-inline`, no `px-*`. |
41
+ | **`bounded-surface`** | Owns its modal/popover/off-canvas chrome | dialog, bottom-sheet, side-sheet, toast, tooltip | Never a page-sibling; renders into a portal / off-canvas region. |
42
+ | **`inline`** | Slot atom (intrinsic) OR width-following card (`width:100%`, host owns inset) | Atoms: button, badge, thumbnail, form-field, status-tag, metadata, switch, progress, skeleton, chip-atom. Cards: banner, nav-card | Atoms: parent `gap` or another component's slot. Cards: host owns the horizontal inset. |
65
43
 
66
- 4. **Inside a `bounded-surface`, full-bleed children require the negative-margin opt-out.** When `List` / `Feed` / `AvatarRail` / `Tabs` / `Chip`-group sits inside a `Dialog` / `BottomSheet` / `SideSheet`, the surface's `layout.container.*` padding + the child's own row padding double-stack. Idiom:
44
+ **Five padding prohibitions (zero-tolerance):**
67
45
 
46
+ 1. **Shell pays the horizontal inset once.** `<main style={{paddingInline:'var(--sys-layout-page-md)'}}>` — nothing inside re-adds horizontal padding; full-bleed children stretch edge-to-edge from that boundary.
47
+ 2. **Never wrap a full-bleed family in a padded `<div>`** (`<div className="px-4"><Feed/></div>`, `<div style={{padding:16}}><List/></div>`) — the wrapper double-pays the inset.
48
+ 3. **Never pass `paddingInline` / `style={{padding}}` / `px-*` to a full-bleed component directly** — overrides its own internal padding.
49
+ 4. **Full-bleed child inside a `bounded-surface` → negative-margin opt-out:**
68
50
  ```jsx
69
- <List
70
- style={{
71
- marginInline: 'calc(-1 * var(--sys-layout-container-md))',
72
- width: 'calc(100% + 2 * var(--sys-layout-container-md))',
73
- maxWidth: 'none',
74
- }}
75
-
76
- />
51
+ <List style={{ marginInline:'calc(-1 * var(--sys-layout-container-md))', width:'calc(100% + 2 * var(--sys-layout-container-md))', maxWidth:'none' }} … />
77
52
  ```
78
-
79
- 5. **Inside another full-bleed host (Carousel/Feed/Section) hosting a chrome-bearing full-bleed child, declare `embedded={true}`.** Eligible: `AvatarRail`, `SuggestionList`, `Tabs`, `List`. The child renders `data-embedded="true"` and zeroes its own `background` + `padding-inline` + `padding-block` so the host's chrome takes over.
80
-
53
+ 5. **Full-bleed child inside another full-bleed host (Carousel/Feed/Section) → `embedded={true}`** (eligible: AvatarRail, SuggestionList, Tabs, List). Zeroes its own background + padding-inline + padding-block so the host's chrome takes over. A `:where()` rule in `styles.css` catches omission, but make it explicit.
81
54
  ```jsx
82
- <Carousel label="Shortcuts">
83
- <AvatarRail embedded items={…} />
84
- </Carousel>
55
+ <Carousel label="Shortcuts"><AvatarRail embedded items={…} /></Carousel>
85
56
  ```
57
+ 6. **Atoms carry their own footprint.** `Thumbnail size={40}` IS 40px — don't wrap in a padding div for "breathing room". Use the parent row's `gap`. Same for Button, Chip, Badge.
86
58
 
87
- A `:where()` ancestry rule in `styles.css` catches the case where the prop is omitted, but **make `embedded` explicit** agents reading the JSX see the contract.
88
-
89
- 6. **Atoms (inline) carry their own intrinsic footprint.** A `Thumbnail size={40}` IS 40px square; do NOT wrap it in `<div style={{ padding: 8 }}>` to "give it breathing room." Use the parent row's `gap`. Same for `Button`, `Chip`, `Badge`.
90
-
91
- ### Group for alignment, gap for rhythm
59
+ **Group for alignment, gap for rhythm:** one parent owns the inset, every child stretches to its content-box edge; vertical spacing is `gap: var(--sys-layout-stack-*)` on the shared parent, **never** `margin-top` per child. Mental check before JSX: *what is the family's `layoutInset`?*
92
60
 
93
- - **Horizontal:** one parent owns the inset; every child stretches to that parent's content-box edge.
94
- - **Vertical:** use `gap: var(--sys-layout-stack-*)` on the shared parent — **never** `margin-top: …` on each child.
95
- - **Mental check before writing JSX:** *"Open `<family>.family.json`. What is `layoutInset`? full-bleed → direct child, no wrapper. bounded-surface → portal/overlay, not page sibling. inline → inside a slot or under parent's gap."*
96
-
97
- ### Rail self-diagnostic (run in dev preview console before "done")
98
-
99
- Visual contracts are checkable. **Copy the console snippet from [`anti-patterns.md` § Rail self-diagnostic](anti-patterns.md)** and paste it into the browser console — it measures every full-bleed child's actual left/right edge and fails loudly if they disagree by >1px. Run it with `<Dialog>` / `<BottomSheet>` open *and* closed (full-bleed children inside a surface must share the surface's inner rail). **Misalignment → discard + regenerate.**
61
+ **Rail self-diagnostic:** before calling it "done", paste the console snippet from [`anti-patterns.md` § Rail self-diagnostic](anti-patterns.md) — it fails if any full-bleed child's left/right edge disagrees by >1px. Run with Dialog/BottomSheet open *and* closed. Misalignment → discard + regenerate.
100
62
 
101
63
  ---
102
64
 
103
65
  ## A. Initialization & reference order
104
66
 
105
- ### A.0 Install Chorus packages
106
-
107
- Workspace MUST have both packages installed before composing UI:
67
+ ### A.0 Install
108
68
 
109
69
  ```bash
110
70
  npm install @teamblind-chorus/ui @teamblind-chorus/tokens
111
71
  ```
72
+ **Install is two-phase so an in-progress project's existing UI is never restyled and its build never breaks on the standby turn.** Only the inert layer goes in now; everything with a *global* side-effect waits for the first composition brief.
112
73
 
113
- Load stylesheets once at app entry (`src/main.tsx`, `app/layout.tsx`, or `src/index.css`):
114
-
74
+ **Now (standby restyles nothing already on the page):** wire **only `tokens.css`** at app entry (`src/main.tsx`, `app/layout.tsx`, or `src/index.css`) — pure CSS custom properties + opt-in `.sys-typo-*` classes, inert until something references them:
115
75
  ```ts
116
76
  import "@teamblind-chorus/tokens/tokens.css";
117
- import "@teamblind-chorus/ui/styles.css";
118
- ```
119
-
120
- Pretendard (the only face Chorus speaks):
121
-
122
- ```html
123
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" />
124
77
  ```
125
-
126
- Copy the image-area placeholder so `src="/placeholder.png"` resolves at runtime:
127
-
78
+ Copy the image placeholder so a later `src="/placeholder.png"` resolves:
128
79
  ```bash
129
80
  cp node_modules/@teamblind-chorus/ui/placeholder.png public/
130
81
  ```
131
82
 
132
- The CSS layer inlines a dataURL fallback, but a 404'd `<img>` still paints a broken-image glyph and external renderers that don't load `styles.css` rely solely on the served path. Skipping the copy breaks image slots.
83
+ **Deferred to the first composition briefNEVER in standby** (each mutates the existing app; activate them in §A.2 before rendering any Chorus component):
84
+ - **`import "@teamblind-chorus/ui/styles.css"`** — ships an **unscoped** `button, input, select, textarea { font-family: inherit }` reset that would re-font every existing native control. Import only when the first Chorus component renders.
85
+ - **Pretendard** (the only Chorus face) — `<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" />`; swaps the page face, so add it together with `styles.css`.
86
+ - **ESLint preset** — `eslint.config.js`: `import chorus from "@teamblind-chorus/ui/eslint"; export default [ ...chorus ];` (after `npm install -D eslint`). Its `no-raw-hex` / `no-tailwind-color` / `no-raw-cta` / shadcn-import rules are **errors across the whole project**, so enabling it on legacy Tailwind / shadcn / hex code breaks the build. Turn it on when conversion begins — it then guards the Chorus code you write, not code you weren't asked to touch. A `chorus/*` error = discard + regenerate, never suppress. (TS projects: layer onto the TS parser — preset README.)
133
87
 
134
- **Enable the Chorus lint preset** the deterministic guardrail. It fails the build on raw hex, Tailwind colors, shadcn imports, and raw CTAs **even when this prompt is out of context**. Run `npm install -D eslint`, then:
135
-
136
- ```js
137
- // eslint.config.js
138
- import chorus from "@teamblind-chorus/ui/eslint";
139
- export default [ ...chorus ];
140
- ```
141
-
142
- TS projects layer the rules onto the TS parser (see preset README). A `chorus/*` error = discard + regenerate, never suppress.
143
-
144
- **Then actually read** four files before posting readiness:
145
-
146
- 1. `node_modules/@teamblind-chorus/ui/agents/manifest.json` — enumerates families & subs.
147
- 2. `node_modules/@teamblind-chorus/ui/agents/catalog.md` — intent → component routing.
148
- 3. `node_modules/@teamblind-chorus/ui/dist/index.d.ts` — typed surface. **Delete any consumer shim** (`src/types/blind-dsai-ui.d.ts` etc.) — the package's own types are the source of truth; a `ComponentType<any>` shim masks discriminated unions like `FormField` variants.
149
- 4. `node_modules/@teamblind-chorus/ui/agents/components/<one-relevant-family>/<sub>.spec.json` — the family the brief most likely needs.
88
+ **Then read four files before readiness** (all under `node_modules/@teamblind-chorus/ui/`): (1) `agents/manifest.json` families & subs; (2) `agents/catalog.md` intent component; (3) `dist/index.d.ts` typed surface; **delete any consumer shim** (`src/types/*-ui.d.ts`) so the package's own types own discriminated unions like FormField variants; (4) `agents/components/<one-relevant-family>/<sub>.spec.json`.
150
89
 
151
90
  Post readiness in **this exact shape**:
91
+ > *"✅ Chorus ready: @teamblind-chorus/ui@\<version\>, tokens.css wired at \<entry-file\> (styles.css + Pretendard + lint **deferred to first compose** — existing UI untouched), public/placeholder.png copied. Read: manifest (\<N families\>), catalog (locked: dialog/bottom-sheet/toast/tooltip/form-field; open: \<N\>), dist/index.d.ts (FormField variants: input/search/select), \<family\>/\<sub\>.spec.json. Removed legacy shim: \<path or 'none'\>. Standing by for the brief — once you send one, the flow is §A.2 pattern → §A.3 spec → §A.4 shell → compose."*
152
92
 
153
- > *"✅ Chorus ready: @teamblind-chorus/ui@\<version\>, tokens.css + styles.css wired at \<entry-file\>, public/placeholder.png copied from node_modules. Read: manifest (\<N families\>), catalog (locked: dialog/bottom-sheet/toast/tooltip/form-field; open: \<N names\>), dist/index.d.ts (typed exports FormField variants resolved: input/search/select), \<family\>/\<sub\>.spec.json. Removed legacy shim: \<path or 'none'\>. Standing by for the screen brief next turn: §A.2 pattern → §A.3 spec re-read → §A.4 page-shell skeleton → compose."*
154
-
155
- **Do NOT** abbreviate the four bracketed evidence items; **do NOT** post readiness if any is unread. Then **wait** for the brief. Do NOT pre-generate a demo.
156
-
157
- If install fails (network, registry, peer-dep), surface as a single line and stop — do NOT substitute hand-rolled stubs.
158
-
159
- ### A.1 Files shipped at `@teamblind-chorus/ui/agents/*`
160
-
161
- Read directly from `node_modules/@teamblind-chorus/ui/agents/`:
93
+ Don't abbreviate the bracketed evidence; don't post readiness if any file is unread. Then **wait** do NOT pre-generate a demo. If install fails (network, registry, peer-dep), surface one line and stopnever substitute hand-rolled stubs.
162
94
 
163
- | File | What it owns |
164
- | :--- | :--- |
165
- | `AGENTS.md` | Hard agent contract — five design principles + hard rules. |
166
- | `DESIGN.md` | Token model & foundations (color/type/spacing/radius/elevation/voice). **~1300 lines — fetch by section anchor only** (table below). |
167
- | `catalog.md` | Intent → component map (authoritative). §C below is the condensed version. |
168
- | `manifest.json` | Single index of every family, sub, slot. **Read first** to enumerate the system. |
169
- | `components/<family>/<sub>.spec.json` | Per-sub contract: props, slots, intrinsic-vs-content, defaults. Machine-readable truth. |
170
- | `components/<family>/<sub>.md` | Per-sub prose: "Reach for this when … Skip when …" + anatomy invariants + recipes. |
171
- | `components/<family>/<family>.family.json` | Family metadata: sub list, default sub, use-cases, `visualReuse`, `layoutInset`. |
172
- | `patterns/<name>.md` | Per-screen recipes — intent, layout anatomy, tokens-in-use, components. PNG screenshot at `github.com/teamblind/chorus/tree/main/patterns` (same slug). |
173
- | `icons.json` | Icon manifest — `icons` (name → keywords, for intent search) + `aliases`. Import from `@teamblind-chorus/ui/icons` by exact name. |
174
- | `tokens.usage.json` | Per-token role + usage index with `value`, `role`, `usedFor[]`, `notFor[]`, `pairsWith`, `allowedComponents`. **Read before composing** to pick the right token. |
175
- | `compose.md` | Composition cheatsheet — spacing recipes, color-quartet picker, type-ramp picker, 10 guard rails. **Skim before JSX.** |
176
- | `anti-patterns.md` | ~14 common failure shapes with wrong-vs-right snippets. **Read once before composing.** Output matching ❌ → discard + regenerate. |
177
- | `LOVABLE.md` | This file. |
95
+ ### A.1 Shipped reference files (`@teamblind-chorus/ui/agents/*`)
178
96
 
179
- **DESIGN.md is too large to load whole.** Grep the heading:
97
+ | File | Owns |
98
+ | :-- | :-- |
99
+ | `manifest.json` | Index of every family / sub / slot. **Read first.** |
100
+ | `catalog.md` | Intent → component map (authoritative; §C is the condensed view). |
101
+ | `components/<family>/<sub>.spec.json` | Per-sub contract: props, slots, intrinsic-vs-content, defaults, **`forbidden`** (closed negative list). |
102
+ | `components/<family>/<sub>.md` | Per-sub prose: reach-for / skip + anatomy invariants + recipes. |
103
+ | `components/<family>/<family>.family.json` | Family meta: subs, default, useCases, `visualReuse`, `layoutInset`. |
104
+ | `patterns/<name>.md` | Per-screen recipe (intent / layout / tokens / components); PNG on GitHub, same slug. |
105
+ | `tokens.usage.json` | Per-token `value`/`role`/`usedFor`/`notFor`/`pairsWith`/`allowedComponents`. **Read before composing.** |
106
+ | `icons.json` | Icon manifest (name → keywords + aliases). Import from `…/ui/icons` by exact name. |
107
+ | `compose.md` | Composition cheatsheet — spacing / color / type recipes + guardrails. **Skim before JSX.** |
108
+ | `anti-patterns.md` | 18 failure shapes, wrong-vs-right. Output matching ❌ → regenerate. |
109
+ | `AGENTS.md` | Hard agent contract (principles + hard rules). |
110
+ | `DESIGN.md` | Token model & foundations. **~1300 lines — fetch by anchor only** (below). |
180
111
 
181
- | When deciding… | Fetch `DESIGN.md § …` |
182
- | :--- | :--- |
183
- | color, contrast, dark mode | `### Color` |
184
- | spacing, gaps, page insets, vertical rhythm | `### Spacing & Layout` |
185
- | type ramp, weights, line heights | `### Typography` |
186
- | radius scale | `### Radius` |
187
- | stroke widths, dividers | `### Border & Stroke` |
188
- | shadows, surface elevation | `### Elevation` |
189
- | hover/pressed/focus/disabled | `### State layers & Focus` |
190
- | breakpoints, responsive shifts | `### Responsive behavior` |
191
- | touch target, contrast minima | `### Accessibility` |
192
- | 3 authorized literal exceptions, brand adaptation | `### Adapting Chorus` |
193
- | voice, copy tone, microcopy | `### Voice & Content` |
112
+ `DESIGN.md` anchors: `### Color` (contrast / dark mode) · `### Spacing & Layout` (gaps / insets / rhythm) · `### Typography` (ramp / weights / line) · `### Radius` · `### Border & Stroke` · `### Elevation` · `### State layers & Focus` (hover / pressed / focus / disabled) · `### Responsive behavior` · `### Accessibility` (touch target / contrast minima) · `### Adapting Chorus` (3 literal exceptions, brand) · `### Voice & Content`.
194
113
 
195
- No GitHub fetch for normal work. **Escalate to <https://github.com/teamblind/chorus> only when** (a) a value/contract is missing and the published version may be stale, (b) the user says "check chorus" or pastes a `github.com/teamblind/chorus/...` URL, (c) you need a pattern `.png` (vision runs only). If GitHub disagrees with the package, **trust the package** and flag one line (*"`@teamblind-chorus/ui@<v>` may be behind chorus@main — consider `npm update`."*). Never copy raw values from GitHub; tokens stay as `var(--sys-*)`, components stay imported from `@teamblind-chorus/ui`.
114
+ **No GitHub fetch for normal work.** Escalate to <https://github.com/teamblind/chorus> only when (a) a value is missing and the package may be stale, (b) the user says "check chorus" / pastes a repo URL, (c) you need a pattern `.png` (vision runs). If GitHub disagrees, **trust the package** and flag one line. Never copy raw values from GitHub; tokens stay `var(--sys-*)`, components stay imported.
196
115
 
197
- ### A.2 Pattern lookup — run on every screen brief
116
+ ### A.2 Pattern lookup (every screen brief)
198
117
 
199
- Patterns are the **layout-level ground truth**. The package ships `.md` (textual contract); GitHub serves the matching `.png` (canonical visual). Slugs pair: `main_home.md` `main_home.png`, `compose.md` `compose.png`, etc.
118
+ **First brief only — activate the deferred install layer (§A.0) before composing:** import `@teamblind-chorus/ui/styles.css`, add the Pretendard `<link>`, and enable the ESLint preset. (Held back during standby so existing UI and the build stayed untouched; the first Chorus component needs them.)
200
119
 
201
- Before writing JSX:
120
+ Patterns are layout-level ground truth (`.md` = textual contract, GitHub `.png` = visual). Before JSX:
121
+ 1. **Reduce the brief to a noun** (`home`, `compose`, `post`, `company`, `explore`, `jobs`, `notifications`, `settings`, `search`, …).
122
+ 2. **Match the most specific pattern** in `agents/patterns/` (locale `_kr`, sub-flows `_promotion` / `_channel` / …).
123
+ 3. **Read the `.md` fully** — Intent / Layout / Tokens in use / Components are the composition anchors.
124
+ 4. **Vision runs:** fetch `https://github.com/teamblind/chorus/blob/main/patterns/<name>.png?raw=1` (dark: `<name>.dark.png`).
125
+ 5. **Compose against it** — reuse the sequence verbatim, flex only content slots.
126
+ 6. **No match?** Anchor on the closest sibling and note the deviation in one line.
202
127
 
203
- 1. **Reduce the brief to a noun.** `home`, `compose`, `onboarding`, `post`, `post_comments`, `company`, `explore`, `jobs`, `notifications`, `settings`, `search`.
204
- 2. **Match the pattern** in `node_modules/@teamblind-chorus/ui/agents/patterns/`. Pick the most specific (locale `_kr`, sub-flow `_promotion`/`_channel`/`_offereval`/`_personalEmail`).
205
- 3. **Read the `.md` fully.** `Intent` / `Layout` / `Tokens in use` / `Components` are the composition anchors.
206
- 4. **On vision-capable runs, fetch the screenshot.** `https://github.com/teamblind/chorus/blob/main/patterns/<name>.png?raw=1` (dark variant: `<name>.dark.png`). Canonical reference for spacing rhythm, hierarchy, dark/light parity.
207
- 5. **Compose against the pattern.** Reuse the sequence verbatim; flex only content slots.
208
- 6. **No match?** Pick the closest sibling and call out the deviation in one line (*"No exact pattern for `<intent>`; anchoring on `<closest>.md` and adjusting <X>."*).
128
+ **Precedence:** patterns are *descriptive* if one conflicts with a `spec.json` / `family.json` / token, the spec/token wins.
209
129
 
210
- **Precedence:** patterns are *descriptive*. If a pattern conflicts with a `spec.json`, `family.json`, or token, the spec/token wins — patterns describe the visual outcome of correct component+token use. GitHub screenshots inform composition, not override the contract.
130
+ ### A.3 Component contract lookup (every component)
211
131
 
212
- ### A.3 Component contract lookupfor every component you render
132
+ The catalog says *which*; the contract says *how*. **Don't improvise props / slot names from the English name** the binding is in `spec.json`.
133
+ 1. **Locate family + sub** via `manifest.json`; multi-sub families → match `useCases` in `family.json`. **Check `visualReuse`:** `"open"` (28) allows visual-fit pick; `"locked"` (5) is canonical-role only.
134
+ 2. **Read `spec.json` fully** — props (type / default / allowed), slots (`accepts` / `rendersAs` / `intrinsic` vs content), `tokens`, and **`forbidden`** (the closed negative list). Hard-reject filter at JSX time.
135
+ 3. **Read `.md`** for when / why + anatomy invariants.
136
+ 4. **Honor slot kind.** `intrinsic:true` → component paints it, don't fill. `accepts:["thumbnail"]` → content is a Chorus component, not raw `<img>` / div.
137
+ 5. **Never invent props** (`className`, `style`, `wrapperClassName`, …) — restyling is global via tokens, never wrappers.
138
+ 6. **Sub swaps require re-reading** — `variant="standard"` → `"text"` is a different spec.
213
139
 
214
- The catalog tells you *which* component; the contract tells you *how*. **Do not improvise props or slot names from the English component name** — the binding is in `spec.json`.
215
-
216
- Per component, before JSX:
217
-
218
- 1. **Locate family + sub** via `manifest.json` (`families[].slug` → `subcomponents[].slug`). Multiple subs (e.g. `button` has standard / text / icon / fab / toggle / check / toolbar; `list` has standard / radio / entry / accordion) → open `agents/components/<family>/<family>.family.json` and match `useCases`. **Check `visualReuse`** — `"open"` (27 families) allows visual-fit pick; `"locked"` (5 families: dialog, bottom-sheet, toast, tooltip, form-field) is canonical-role only.
219
- 2. **Read `spec.json` fully** — `props` (required/optional, type, default, allowed), `slots` (purpose, `accepts`, `rendersAs`, `intrinsic` vs content), any `tokens` block, and **`forbidden` — the closed list of negative rules** (`radius < radius.full`, `raw <button> wrapper`, `border on rest state`, …). Hard-reject filter at JSX time.
220
- 3. **Read `.md` for when/why.** "Reach for this when … Skip when …" + anatomy invariants + cross-sub family contract.
221
- 4. **Honor slot kind.** `intrinsic: true` → component paints it; don't fill. `accepts: ["thumbnail"]` / `rendersAs: "thumbnail:40"` → content is a Chorus component, not raw image/div.
222
- 5. **Never invent props.** No `className`, `style`, `wrapperClassName`, `containerStyle` to Chorus components — restyling happens through tokens globally, never wrappers.
223
- 6. **Sub swaps require re-reading.** `<Button variant="standard">` → `<Button variant="text">` is a different spec.
224
-
225
- ### A.4 Canonical page-shell skeleton
226
-
227
- Misaligned left rails and drifting bars are top failure modes. Use **`<PageShell>`** — it ships the pin/scroll mechanics, so you never hand-author the shell CSS:
140
+ ### A.4 Page-shell skeleton
228
141
 
142
+ Use **`<PageShell>`** — it ships the pin/scroll mechanics, so you never hand-author shell CSS:
229
143
  ```jsx
230
- <PageShell
231
- // Bars render in flow and are pinned by the shell; each pays its own
232
- // safe-area inset (nav = top notch, tabBar = bottom home-indicator).
233
- nav={<NavigationBar variant="main" … />}
234
- tabBar={<TabBar … />}
235
- >
236
- {/* The ONLY part that scrolls. Every full-bleed child stretches edge-to-edge —
237
- NONE carries its own `padding-inline` / `className="px-*"`. */}
144
+ <PageShell nav={<NavigationBar variant="main" … />} tabBar={<TabBar … />}>
145
+ {/* Only this scrolls. Every full-bleed child edge-to-edge NONE carries its own padding-inline / px-*. */}
238
146
  <Tabs variant="underline" … />
239
147
  <Carousel label="…" headerAction={…}>…</Carousel>
240
- <Banner … />
241
- <Feed items={…} />
242
- <List … />
148
+ <Banner … /> <Feed items={…} /> <List … />
243
149
  </PageShell>
244
150
  ```
245
-
246
- **Only `<main>` scrolls; the bars are pinned in flow.** PageShell is a 100dvh flex column whose body is the sole scroll region (`flex:1 1 auto; min-height:0; overflow-y:auto`); without it the whole page scrolls and the bars drift off-screen. **Never** add `position: sticky`/`fixed` to the bars (it double-applies their safe-area insets), and never hand-roll the column — `<PageShell>` (or the `.chorus-page-shell` class) is the pin. A dev guard warns if a bar renders inside a scrolling region. Need a page gutter for inline content? `bodyProps={{ style: { paddingInline: 'var(--sys-layout-page-md)' } }}`. Rationale: `patterns/layout.md`.
151
+ **Only `<main>` scrolls; the bars are pinned in flow.** PageShell is a 100dvh flex column whose body is the sole scroll region (`flex:1 1 auto; min-height:0; overflow-y:auto`). **Never** add `position:sticky`/`fixed` to bars (double-applies their safe-area insets) or hand-roll the column. Page gutter for inline content: `bodyProps={{ style:{ paddingInline:'var(--sys-layout-page-md)' } }}`. Rationale: `patterns/layout.md`.
247
152
 
248
153
  ### A.5 Import contract
249
154
 
250
- * **Components:** `import { Button, Carousel, List, Feed, Thumbnail, ... } from "@teamblind-chorus/ui";`
251
- * **Icons:** `import { PlusIcon, ChevronRightIcon, ... } from "@teamblind-chorus/ui/icons";`
252
- * **Tokens (CSS vars):** loaded by `@teamblind-chorus/tokens/tokens.css` — reference as `var(--sys-*)` / `var(--ref-*)`.
253
- * **Resolved token JSON (build tooling only):** `import light from "@teamblind-chorus/tokens/resolved.light.json" with { type: "json" };`
254
- * **NEVER:** `@/components/ui/*` (shadcn) — does not exist.
255
- * **NEVER:** `@/components/chorus/*` — legacy mirror, gone. Use the npm package.
256
-
257
- ---
155
+ ```ts
156
+ import { Button, Carousel, List, Feed, Thumbnail } from "@teamblind-chorus/ui";
157
+ import { PlusIcon, ChevronRightIcon } from "@teamblind-chorus/ui/icons";
158
+ ```
159
+ Tokens are CSS vars from `@teamblind-chorus/tokens/tokens.css` — reference as `var(--sys-*)` / `var(--ref-*)`. Resolved JSON (build tooling only): `import light from "@teamblind-chorus/tokens/resolved.light.json" with { type:"json" }`. **NEVER** `@/components/ui/*` (shadcn) or `@/components/chorus/*` (legacy mirror) gone; use the npm package.
258
160
 
259
- ## B. Design principles (apply top-down)
161
+ ### A.6 Existing project preserve behavior, keep a rollback
260
162
 
261
- 1. **Chorus First.** Source of truth tokens, components, patterns. Start with `manifest.json` + `catalog.md`. Never invent values from screenshot inference or generic libraries.
262
- 2. **Component flexibility — extrapolate, don't fork.** Respect anatomy invariants (slot grammar, sizing tokens, state contract); flex composition. `visualReuse: "open"` (27 families) may be picked on visual-fit grounds. `"locked"` (5 families) MUST be canonical-role only.
263
- 3. **New surfaces stay token-true.** No Chorus family fits? Design a new primitive — but every color / spacing / type / radius / border-width / elevation MUST resolve through Chorus tokens + DESIGN.md foundations. **Component flexible, tokens never.** Common drift: custom composition with raw values (`fontSize: 13`, `gap: 6`) — see [anti-patterns.md #14].
264
- 4. **Lego-block composition.** Combine and extend Chorus Lego-style.
265
- 5. **UX-pattern consistency.** Pick by expected interaction. `Dialog`/`BottomSheet`/`Toast`/`Tooltip`/`FormField` own focus trap / auto-dismiss / ARIA live / hover trigger / `<input>` semantics — never borrow for shape.
163
+ A brief like *"convert this page to Chorus"* / *"replace this UI"* is a **presentation swap, never a logic rewrite.** Re-skin the markup with Chorus components + tokens while keeping every working behavior **intact and attached** — losing a shipped feature or integration is never an acceptable cost of restyling.
266
164
 
267
- ---
165
+ - **Carry the wiring across verbatim** — event handlers (`onClick` / `onSubmit` / `onChange`), data fetching / queries (Supabase, react-query, REST), state (`useState` / store / context), form validation + submit, routing / navigation, auth gates, `useEffect` side effects, and any third-party or API integration. Map each element to its Chorus counterpart and move the **same props / handlers** onto it: `<button onClick={save}>` → `<Button onClick={save}>`; `<input value={v} onChange={f}/>` → `<FormField value={v} onChange={f}/>`. The shell changes; the behavior does not.
166
+ - **Never delete, comment out, or stub working logic** to simplify the markup. No clean Chorus mapping for a piece? **Keep its behavior and flag a "Chorus gap"** — don't drop the feature.
167
+ - **Don't break contracts the rest of the app relies on** — preserve prop names, exported types, data shapes, and route paths other modules import. Restyle internals; keep the public surface.
268
168
 
269
- ## C. Hard rules (zero-tolerance)
169
+ **One screen at a time — a hard gate, never a batch.** Migrating more than one screen in a single pass is a **violation → discard + regenerate**. Per screen, in order:
170
+ 1. **Name a rollback point** — Lovable snapshots every edit, so label the pre-migration state (*"before Chorus: `<screen>`"*); if GitHub sync is on, **commit the working state first**.
171
+ 2. **Migrate that one screen** — presentation swap only, wiring carried across per the rules above.
172
+ 3. **Verify behavior (blocking)** — confirm handlers fire, queries return, forms submit. **Unverified = not done**; never present a screen you haven't verified.
173
+ 4. **Report** the change + its rollback path, then move to the next screen.
270
174
 
271
- *Any violation discard + regenerate.*
175
+ **After the *first* screen of a multi-screen brief, STOP and confirm the approach** before continuing — so a broken pattern surfaces on one screen, not five. Never bulk-reconstruct several screens and present them together.
272
176
 
273
- ### Composition & architecture
177
+ ---
274
178
 
275
- * **Exclusive imports** from `@teamblind-chorus/ui` (icons from `@teamblind-chorus/ui/icons`).
276
- * **No shadcn** (`@/components/ui/*`). **No legacy mirror** (`@/components/chorus/*`).
277
- * **Missing primitive — extend, don't escape.** Ladder: (1) re-compose existing Chorus via slot grammar; (2) Lego multiple Chorus components; (3) design a **new primitive that conforms to every cross-cutting pattern**, not just tokens. Only then flag a **"Chorus gap"**. **Never** raw HTML + Tailwind, shadcn, or third-party.
179
+ ## B. Design principles
278
180
 
279
- A new primitive must honor every line below. Token compliance alone produces a brand-coloured div that reads as a foreign body:
181
+ 1. **Chorus First** start at `manifest.json` + `catalog.md`; never invent values from screenshot inference or generic libraries.
182
+ 2. **Extrapolate, don't fork** — respect anatomy invariants (slot grammar, sizing tokens, state contract), flex composition. `"open"` (28) may be picked on visual fit; `"locked"` (5) canonical-role only.
183
+ 3. **New surfaces stay token-true** — every color / spacing / type / radius / border-width / elevation resolves through tokens + DESIGN.md. **Component flexible, tokens never.**
184
+ 4. **Lego-block composition** — combine and extend Chorus Lego-style.
185
+ 5. **UX-pattern consistency** — Dialog / BottomSheet / Toast / Tooltip / FormField own focus trap / auto-dismiss / ARIA live / hover trigger / `<input>` semantics; never borrow for shape.
280
186
 
281
- - **Tokens, exhaustively.** Color / spacing / typography / radius / border-width / elevation / motion ALL resolve to `sys.*` (or `ref.*` if no semantic alias). No raw hex, no off-scale px, no raw `box-shadow`, no Tailwind color, no third-party type ramp. Per axis: see `DESIGN.md`.
282
- - **No-layout strokes.** Edge separation is `inset box-shadow` OR a `::after` overlay OR an `outlineVariant` divider — **NEVER `border:`**. `border` reflows the box and breaks the focus-ring overlay layer. Cards with a full-bleed child promote the outline to the `::after` layer the focus ring uses. See `DESIGN.md § Border & Stroke`.
283
- - **Focus rings.** Dedicated `::after` overlay at `:focus-visible`, composed Inward (flush controls — Tabs, TabBar) or Outward (free-standing — Button, Chip), standard ring widths. **NEVER `outline:`, `:focus { box-shadow }`, or a bordered focus state.** See `DESIGN.md § State layers & Focus`.
284
- - **State layers — overlay, not replacement.** `hovered` / `pressed` / `focused` / `disabled` paint a translucent `currentColor` overlay at `sys.state.*` opacities — they do NOT swap fill / border / typography. See `DESIGN.md § State layers & Focus`.
285
- - **Sizing rungs.** Every dimension belongs to a Chorus rung — Thumbnail 16 / 20 / 24 / 32 / 40 / 48, icon 16 / 20 / 24, radius `sys.radius.xs/sm/md/lg/full` (4/8/12/16/pill), Button heights 32/40, bar 56. Off-scale (36px icon, 7px radius) forbidden.
286
- - **Typography & color pairs.** Use a complete `sys.typo.*` rung (size + line + weight + tracking together). Foreground / background travels as a pair — `sys.color.<role>Container` REQUIRES `sys.color.on<Role>Container`; never split.
287
- - **Readability — pair contrast, don't compose it.** Foreground for every text run / icon glyph / graphic boundary MUST resolve to the host fill's pre-paired `on*` token (`surface*` ↔ `onSurface` / `onSurfaceVariant`; `primary` ↔ `onPrimary`; `primaryContainer` ↔ `onPrimaryContainer`; same for `secondary` / `brand` / `success` / `error` + containers; `inverseSurface` ↔ `inverseOnSurface`; `sys.color.icon.*` is tuned for **neutral `surface*` hosts only**). NEVER cross-pair (`onPrimary` on `surface`, `onSurface` on `primary`, `sys.color.icon.muted` on a colour-tinted fill, dark text in an `inverseSurface` chip). If a new fill has no pre-paired foreground, compute WCAG contrast against the actual fill in BOTH light and dark, refuse anything below **4.5 : 1 for normal text / 3 : 1 for ≥18pt or Semibold ≥14pt / 3 : 1 for non-text glyphs and graphic boundaries**, and when the chosen pair fails, change the **host fill** to a `sys.color` surface that already pairs — never hand-tune the foreground. The black-on-black / white-on-yellow / translucent-icon-on-primary failure modes are zero-tolerance regenerations.
288
- - **`box-sizing: border-box`** on every new surface.
187
+ ---
289
188
 
290
- A primitive breaking any line above is not "new" it's a drift hit. Flag the gap.
189
+ ## C. Hard rules (zero-toleranceany violation discard + regenerate)
291
190
 
292
- * **No wrapper overrides:** Build by nesting exposed slots. **Never** wrap a Chorus component to restyle CSS.
191
+ ### Composition & architecture
192
+ - **Exclusive imports** from `@teamblind-chorus/ui` (icons from `…/ui/icons`). No shadcn (`@/components/ui/*`), no legacy mirror (`@/components/chorus/*`).
193
+ - **Missing primitive — extend, don't escape.** Ladder: (1) re-compose existing Chorus via slot grammar; (2) Lego multiple components; (3) design a **new primitive**, then flag a **"Chorus gap"**. Never raw HTML+Tailwind, shadcn, or third-party. A new primitive must honor **every cross-cutting pattern in `DESIGN.md`** — tokens exhaustively (`sys.*`/`ref.*`, no raw hex/off-scale px/Tailwind), no-layout strokes (inset shadow/`::after`, never `border:`), `:focus-visible` `::after` focus rings, translucent `sys.state.*` overlays (never swap fill/border/type), sizing rungs (Thumbnail 16–48, icon 16/20/24, radius 4/8/12/16/full, bar 56), complete `sys.typo.*` rungs with paired `on*` foreground (WCAG ≥4.5:1 / 3:1 large or Semibold ≥14px — fix the **host fill**, never hand-tune fg), `box-sizing:border-box`. Token compliance alone yields a brand-colored div that reads foreign.
194
+ - **No wrapper overrides** — build by nesting exposed slots; never wrap a Chorus component to restyle CSS.
293
195
 
294
196
  ### Visual alignment & layout grouping
197
+ Full rules in the **★ Layout & Padding Contract** above. Quick ref: shell pays the gutter once → full-bleed siblings stretch edge-to-edge · negative-margin opt-out inside `bounded-surface` · `embedded={true}` for AvatarRail / SuggestionList / Tabs / List inside Carousel / Feed · group for alignment, `gap` (not `margin-top`) for rhythm · Banner / NavCard are `inline` with no outer margin (host owns the inset; vertical gap via parent `gap: var(--sys-layout-stack-xs)`).
295
198
 
296
- See the **★ Layout-Type & Padding Contract** at the top of this doc for the full rule set. Quick reference:
297
-
298
- * **One gutter, paid once.** Shell pays `padding-inline: var(--sys-layout-page-*)`; full-bleed siblings stretch edge-to-edge.
299
- * **Negative-margin opt-out** for full-bleed children inside `bounded-surface`.
300
- * **`embedded={true}`** for `AvatarRail` / `SuggestionList` / `Tabs` / `List` directly inside `<Carousel>` / `<Feed>`.
301
- * **Group for alignment, gap for rhythm.** Vertical spacing is `gap` on the shared parent, never `margin-top` on each child.
302
- * **Banner safe zone — host-owned.** Banner is `inline` with no outer margin. The host owns the horizontal inset. Vertical 8 between Banner and siblings is paid by parent `gap: var(--sys-layout-stack-xs)`. Same for NavCard.
303
-
304
- ### Per-component anatomy gotchas (check `spec.json#forbidden` before shipping)
305
-
306
- * **`NavigationBar` (`variant="sub"`) trailing.** Prefer `trailing={{ icon, 'aria-label' }}` — component renders the 24px Icon Button internally and `sys.icon.lg` is guaranteed. If you pass a raw `<Button variant="icon" />`, it MUST carry `size="large"` (= 24); `size="medium"` resolves to 16 and the bar reads asymmetric against the 24px leading back-arrow.
307
- * **`Toast` position + color.** Bottom-center only — `position: fixed; bottom: 0; left: 50%; transform: translateX(-50%)`. Horizontal safe area 8px (`sys.layout.container.xs`); max-width `min(400px, 100vw - 16px)`. Trailing Button MUST carry `appearance="inverse"` for both action (`text` / `small`) and dismiss (`icon` / `medium`) — default `primary` reads as unreadable primary-on-inverseSurface against the dark toast.
308
- * **`Tooltip` width is content-driven, capped at 300.** The bubble hugs its body (`width: max-content`) up to `min(300px, viewport - 16px)`; short copy renders narrow with the caret tight to the bubble, long copy wraps at the cap. **Tooltip copy is brief and intuitive at a glance** — a fragment, a one-line hint, an action label. Copy that routinely fills the 300 cap belongs in a Banner or Dialog. Never override `width` on the container.
309
- * **`Thumbnail` `outlined` for image / tonal hosts.** Reach for `outlined={true}` whenever the Thumbnail sits on something other than a clean `surface*` tier: half-overlaps a cover image (ProfileHeader avatar, ProfileCarousel avatar, any Hero), sits on a brand-tonal or `*Container` fill, sits on a dark photo / pattern / video, or overlaps an adjacent avatar. **Skip** on plain `surface*` rows (List / Feed / SuggestionList leading) — the halo paints `surface`-on-`surface` and is invisible. Painted as outset `box-shadow`, never `border:`; the rung's diameter never changes.
310
- * **`List variant="entry"` thumbnail is optional per row.** Drop `thumbnail` from a row descriptor and the leading column collapses — label sits flush at the 16 inline rail. Mix-and-match per row is supported. The same sub now hosts entity rows (with avatar), nav-option rows (with trailing chevron Icon Button), and label-only rows. For pure label-only nav stacks, `NavList` bundles this shape under a header.
311
- * **Image-area placeholder.** `/placeholder.png` is the canonical served-path contract; the inlined `styles.css` dataURL is an `<img>`-load-failure safety net only. Never rename to `placeholder_thumbnail.png` or any variant. Swap for a real subject when implied — see `images.md`; full contract in `AGENTS.md` rule #9.
199
+ ### Per-component anatomy gotchas
200
+ Each family's **`spec.json#forbidden`** is the authoritative negative list — read it before placing. High-frequency ones: `Toast` trailing Button must be `appearance="inverse"`; `Thumbnail` needs `outlined` off a clean `surface*` tier (cover / `*Container` / dark photo / overlap); `List variant="entry"` thumbnail is optional per row (drop it → label flush at the 16 rail; pure label-only nav stacks → `NavList`); `NavigationBar variant="sub"` trailing icon Button must be `size="large"` (=24); `Tooltip` never overrides `width`. Placeholder path is `/placeholder.png` (never rename; swap per `images.md`, full contract `AGENTS.md` #9).
312
201
 
313
202
  ### Token strictness (no literals)
314
-
315
- * **Token resolution:** Colors, spacing, radii, border-widths, typography, elevations MUST use Chorus tokens. `var(--sys-*)` preferred; `var(--ref-*)` only when sys is absent.
316
- * **Forbidden all axes:**
317
- * **Color:** raw hex (`#FFF`, `#1A1A1A`), Tailwind color utilities (`bg-white`, `text-black`, `border-gray-200`), `style={{ color: '#...' }}`, third-party palette.
318
- * **Typography:** `fontSize: 13`, `lineHeight: 1.4`, `fontWeight: 600` set inline — apply a full rung with `className="sys-typo-<role>-<rung>"` (`sys-typo-caption` is rung-less; bundles family + size + weight + line + tracking). **No `font: var(--sys-typo-*)` shorthand token exists** — that declaration voids and falls back to a system font. Can't attach a class? Set all four `…-{size,weight,line,tracking}` vars; dropping one drifts. Never set `lineHeight` separately.
319
- * **Spacing:** `gap: 6`, `padding: '10px 12px'`, `marginTop: 12`, `paddingInline: 16` — resolves to `sys.layout.*` (`inline.*` horizontal between siblings, `stack.*` vertical between siblings, `container.*` surface interior, `page.*` shell gutter).
320
- * **Radius:** `borderRadius: 6`, `borderRadius: 10` — pick the next ladder rung (`sys.radius.sm`=4, `md`=8, `lg`=12, `full`). No in-between.
321
- * **Border:** `border: 1px solid #...` — width is `sys.borderWidth.hairline` (1) / `thin` (2), color is `sys.color.outlineVariant` / `outline`. And on surfaces, prefer inset shadow over `border:`.
322
- * **Three authorized exceptions** (per `DESIGN.md § Adapting Chorus`): (1) **intrinsic geometry** naming component anatomy — Thumbnail `48px`, Tooltip `min-height: 32px`, icon `16px`; (2) **computed compositions** combining tokens in `calc()` — e.g. `calc(48px + var(--sys-layout-inline-lg))`; (3) **structural `0` / `100%` / `auto`**. Anything else is a token call. No-token value? Flag a "Chorus gap" rather than inlining.
323
- * **No fallbacks:** No `var(--sys-*, 16px)`. Surface gaps explicitly.
324
- * **Semantic glyph colour — use `sys.color.icon.*`, not `ref.palette.*`.** Standalone semantic glyphs (favourite star, alert mark, live-status dot, AI / feature flag) MUST resolve through the dedicated icon palette: `sys.color.icon.muted` (quiet / inactive), `sys.color.icon.yellow` (favourited / warning), `sys.color.icon.red` (alert / critical outside `sys.color.error`), `sys.color.icon.blue` (informational / link), `sys.color.icon.green` (success / live outside `sys.color.success`), `sys.color.icon.purple` (AI / feature flag — the system's only purple role). The palette is tuned for **neutral `surface*` hosts only** — never on a colour-tinted host (`primary` / `error` / `brand` / `*Container`); on those, the host's `on*` pair is the only valid foreground. Reaching past sys into `ref.palette.yellow.500` etc. is forbidden (the previous Star/heart bindings have all moved off `ref.palette.*`).
203
+ - **Every** color / spacing / radius / border-width / typography / elevation resolves to a token — `var(--sys-*)` preferred, `var(--ref-*)` only when sys is absent, **no fallbacks** (`var(--sys-*, 16px)`). Typography applies a full rung via `className="sys-typo-<role>-<rung>"` (no `font:` shorthand token — it voids to a system font); spacing uses `sys.layout.*` (`inline` horizontal / `stack` vertical / `container` interior / `page` gutter). The per-axis raw→token map (`gap:6`→rung, `border:1px`→inset shadow, off-ladder radius, etc.) lives in **`compose.md`**; the wrong-vs-right shapes in **`anti-patterns.md`**.
204
+ - **Three authorized literal exceptions** (`DESIGN.md § Adapting Chorus`): (1) intrinsic geometry naming anatomy (Thumbnail 48px, icon 16px); (2) `calc()` token compositions; (3) structural `0` / `100%` / `auto`. Else → token call; no-token value → flag a "Chorus gap".
205
+ - **Semantic glyph color → `sys.color.icon.*`** (`.muted` / `.yellow` / `.red` / `.blue` / `.green` / `.purple`), never `ref.palette.*`. On a tinted host (`primary` / `error` / `brand` / `*Container`) use that host's `on*` pair instead.
325
206
 
326
207
  ### Component selection by intent
327
-
328
- First-pass intent → component map. Binding for `visualReuse: "locked"` families (*(locked)* below): never used outside canonical role. For the other 27 (`"open"`), the table is a strong default but visual-fit reuse is allowed — `<Feed>` as a generic article-card surface, `<Carousel>` as any labelled block, `<Banner>` for tonal aside outside a literal "notice" — as long as anatomy invariants (slot grammar, token bindings, intrinsic geometry) hold:
329
-
330
- | User intent / phrase | Target Chorus component | Configuration / variants |
331
- | :--- | :--- | :--- |
332
- | "top bar / app bar / title bar" | `NavigationBar` | `variant="main" \| "sub" \| "search"` |
333
- | "section heading / labelled block" | `Header` | `size="large" \| "medium"` + one trailing mode: `headerAction` (Text Button) / `trailingIcon` (drill-in chevron Icon Button) / `headerDropdown` (Text Button dropdown — label IS the current value, chevron flips on `open`). Or `label` alone for a heading-only row. Used automatically inside Carousel. |
334
- | "header card / summary card / labelled editorial collection" | `Carousel` | Includes `label` + optional `headerAction` (forwarded to Header internally) |
335
- | "article card / post card / feed" | `Feed` | Uses `channel`, `title`, `body`, `thumbnail`, `engagement` slots |
336
- | "ad card / sponsored card" | `FeedAd` | - |
337
- | "company / settings / picker / menu row" | `List` | `variant="entry"` Thumbnail leading where appropriate; **drop `thumbnail` per row** to collapse the leading column for label-only rows. |
338
- | "drill-in row" | `List` | `variant="standard"` (or `radio`; pass `thumbnail` for the image type) with `nav: true` per row — auto-renders the trailing chevron; the whole row is the click target. On `radio` it marks a major category that opens a second screen. For an identity-bearing drill-in (leading thumbnail + chevron) use `variant="entry"` (drop `thumbnail` for label-only) with a trailing chevron Icon Button. |
339
- | "single-select picker" | `List` | `variant="radio"` |
340
- | "vertical follow-roster / 'people you may know' / browse channels" | `DirectoryList` | **Preset over `<Header /> + <List variant="entry" size="large" divider={false} />`** that maps `name → label`, `followers → secondary`, `active/onToggle → trailingIcon`. Reach for it when the canonical Follow-able shape matches verbatim; **drop down to the primitives** for any divergence (different rung, mixed label-only + thumbnail rows, swapped trailing affordance). |
341
- | "vertical label-only nav block / category index / settings menu" | `NavList` | **Preset over `<Header /> + <List variant="entry" />`** rendered label-only (no thumbnail) with a default chevron Icon Button trailing. `supportingText → description`. Drop down to the primitives for any divergence (mixing label-only and thumbnail rows, swapped trailing affordance). |
342
- | "follow-suggestion block (swipeable)" | `SuggestionList` | - |
343
- | "horizontal avatar quick-nav" | `AvatarRail` | - |
344
- | "sticky stage tabs" | `Tabs` | `variant="underline"` |
345
- | "list / grid toggle" | `Tabs` | `variant="segmented"` |
346
- | "filter chip row" | `Chip` | `variant="filter"` |
347
- | "tag pill" | `Chip` | `variant="tag"` |
348
- | "insight / aside / banner" | `Banner` | `variant="default" \| "accent"` |
349
- | "always-on annotation bubble pointing at an icon" | `Bubble` | Persistent pill + caret. Distinct from Tooltip: never overlays neighbours. Host owns positioning (zero gap to anchor, tail tip on anchor centreX, 8 from viewport edges, `tailAlign` follows anchor side). |
350
- | "heavy section-break band between regions" | `Divider` | Full-bleed `scrimSubtle` band, 8 tall. For row-level separators inside a List use the list's own `divider={true}` hairline. |
351
- | "confirmation prompt" | `Dialog` *(locked)* | - |
352
- | "one-thumb action sheet" | `BottomSheet` *(locked)* | - |
353
- | "off-canvas drawer / side panel" | `SideSheet` *(locked)* | Compose with `Header` (medium) + `List` (thumbnail, compact) inside `SideSheetGroup`; `anchor="left" \| "right"` |
354
- | "transient confirmation" | `Toast` *(locked)* | - |
355
- | "trigger-anchored hint" | `Tooltip` *(locked)* | - |
356
- | "labeled text field" | `FormField` *(locked)* | `variant="input" \| "search" \| "select"` |
357
- | "unread count / numeric pill" | `Badge` | - |
358
- | "avatar / logo / leading image / thumbnail" | `Thumbnail` | Requires `src` |
359
-
360
- ### Call-to-actions (CTAs)
361
-
362
- * **Primary commit:** `<Button>` (standard, filled).
363
- * **"See all" / inline links:** `<Button variant="text" appearance="accent">`.
364
- * **Inline / toolbar dropdown (sort / filter / range trigger):** `<Button variant="text" size="xsmall" trailingIcon={open ? <ChevronUpIcon /> : <ChevronDownIcon />} aria-haspopup="listbox" aria-expanded={open} aria-controls={menuId} onClick={…}>{currentValue}</Button>`. The label IS the current selected value ("Top", "Last 7 days") — never a static verb. Chevron flips between `ChevronDownIcon` (rest) and `ChevronUpIcon` (open) as a state signal, never frozen on Down when expanded. Consumer owns the menu surface (portal). Header surfaces this as the `headerDropdown` mode for in-Header trailing dropdowns.
365
- * **Icon-only:** `<Button variant="icon">`.
366
- * **Floating canonical commit:** `<Button variant="fab" icon={…}>Label</Button>` — the ONLY way to float a primary action. NEVER a `standard`/`text` Button pinned with `position: fixed`/`absolute`; the FAB owns the floating geometry, elevation, and safe-area offset. Extended icon+label supported.
367
- * **Prohibited:** Never raw `<button>`, raw `<a>`, or styled `<div>` for actions.
368
- * **Icons render as SVG components, never text characters.** Use `<PlusIcon>` / `<XIcon>` / `<ChevronRightIcon>` from `@teamblind-chorus/ui/icons` — never `'+ Create'`, `'× Close'`, `'→ Continue'`, `'★ Favorite'`, `'•'`, `'·'`, or any other ASCII / Unicode glyph in a label or `aria-label`. Text characters bypass `currentColor` re-tone, the icon-rung sizing, the `aria-hidden` decorative contract, and the keyword-driven swap map. For "add" / "create" prefixes on Text Buttons, use `leadingIcon={<PlusIcon />}`. Full rule: `AGENTS.md` rule #10.
369
-
370
- ### Image areas & thumbnails
371
-
372
- * Every avatar / logo / article thumb / post media / banner illustration uses `<Thumbnail>` or the dedicated `thumbnail` slot. **Never** icon-in-tinted-circle, letter-in-div, empty grey block, or raw `<img>` outside the slot.
373
- * **`Feed`, `<List variant="thumbnail">`, `<SuggestionList>`, `<DirectoryList>` thumbnails are `agentRequired`.** Always carry `thumbnail: { src, alt }`, even without a real cover — fall back to `src: "/placeholder.png"`. Omission is forbidden by the family `spec.json#forbidden`.
374
- * **Fill order (top wins, stop at first match):**
375
- 1. **Real project asset** — logo / avatar / screenshot the project owns.
376
- 2. **Context-appropriate free stock** — clear subject → hot-linkable Unsplash (preferred) or Pexels. URL shapes:
377
- - `https://images.unsplash.com/photo-<id>?auto=format&fit=crop&w=<width>&q=80`
378
- - `https://images.pexels.com/photos/<id>/pexels-photo-<id>.jpeg?auto=compress&w=<width>`
379
- 3. **Placeholder** — `src="/placeholder.png"` (copied at setup).
380
- * **Photo selection — keep Chorus calm.** Near-monochromatic neutral + one restrained blue accent. Prefer desaturated soft-light single-subject (workspace, architecture, nature, candid portrait). Avoid saturated red/orange/yellow, busy collages, plasticky AI stock, heavy brand-logo photography.
381
- * **Slot footprint owned by the component.** Only `src` / `alt` change; never pass `style` / `className` to fight slot geometry.
382
- * **Meaningful `alt`.** Match the subject (`alt="Empty modern office lounge"`), not the role (`alt="thumbnail"`).
383
- * **Never invent a URL.** No reachable real photo? Drop to rung 3 and surface one line: *"no context-appropriate photo inferred for <slot>; using placeholder"*.
208
+ **`catalog.md` (read in §A.0) is the authoritative intent→component map** — consult it before composing. The condensed common set:
209
+
210
+ | Intent | Component | Config |
211
+ | :-- | :-- | :-- |
212
+ | top / app / title bar | `NavigationBar` | `variant="main"\|"sub"\|"search"` |
213
+ | section heading / labelled block | `Header` | `size` + one trailing (`headerAction` / `trailingIcon` / `headerDropdown`); auto-used inside Carousel |
214
+ | labelled collection / summary card | `Carousel` | `label` + optional `headerAction` |
215
+ | article / post card / feed | `Feed` | `channel` / `title` / `body` / `thumbnail` / `engagement` slots |
216
+ | company / settings / picker / menu row | `List` | `variant="entry"` (drop `thumbnail` for label-only); `"standard"+nav:true` drill-in; `"radio"` single-select |
217
+ | stage tabs / list-grid toggle | `Tabs` | `variant="underline"` / `"segmented"` |
218
+ | filter chip row / tag pill | `Chip` | `variant="filter"` / `"tag"` |
219
+ | insight / aside | `Banner` | `variant="default"\|"accent"` |
220
+ | avatar / logo / leading image | `Thumbnail` | requires `src` |
221
+ | unread count / numeric pill | `Badge` | |
222
+
223
+ **`"locked"` families never leave their canonical role:** `Dialog` (confirmation), `BottomSheet` (action sheet), `SideSheet` (off-canvas drawer; `anchor="left"\|"right"`), `Toast` (transient confirmation), `Tooltip` (trigger hint), `FormField` (labeled field; `variant="input"\|"search"\|"select"`). The other 28 allow visual-fit reuse as long as anatomy invariants hold. Specialised families — `FeedAd`, `DirectoryList`, `NavList`, `SuggestionList`, `AvatarRail`, `Bubble`, `Divider` — see `catalog.md`.
224
+
225
+ ### CTAs
226
+ - **Primary commit:** `<Button>` (standard, filled). **"See all" / links:** `<Button variant="text" appearance="accent">`. **Icon-only:** `<Button variant="icon">`.
227
+ - **Inline / toolbar dropdown:** `<Button variant="text" size="xsmall" trailingIcon={open ? <ChevronUpIcon/> : <ChevronDownIcon/>} aria-haspopup="listbox" aria-expanded={open} …>{currentValue}</Button>` — label IS the current value ("Top", "Last 7 days"), never a static verb; the chevron flips, never frozen; consumer owns the menu portal. (Header exposes this as `headerDropdown`.)
228
+ - **Floating commit:** `<Button variant="fab" icon={…}>Label</Button>` — the ONLY way to float a primary action; never a `standard`/`text` Button pinned `fixed`/`absolute`.
229
+ - **Never** raw `<button>` / `<a>` / styled `<div>` for actions.
230
+ - **Icons are SVG components, never text glyphs** `<PlusIcon>` / `<XIcon>` from `…/ui/icons`, never `'+ Create'`, `'×'`, `'→'`, `'★'`, `'•'`. Text-prefix on Text Buttons `leadingIcon={<PlusIcon/>}`. (`AGENTS.md` #10.)
231
+
232
+ ### Images & thumbnails
233
+ - Every avatar / logo / thumb / post media / banner illustration uses `<Thumbnail>` or the `thumbnail` slot — never icon-in-circle, letter-in-div, grey block, or raw `<img>`. `Feed` / `List variant="thumbnail"` / `SuggestionList` / `DirectoryList` thumbnails are **required** (`thumbnail:{src,alt}`, fallback `src:"/placeholder.png"`; omission forbidden by `spec.json#forbidden`).
234
+ - **Fill order (full decision tree in `images.md`):** (1) real project asset; (2) clear, brand-safe subject → **generate and self-host** (image tool → upload → store the URL); (3) else `/placeholder.png` + one-line note. **Never paste a third-party stock URL** (unsplash / pexels) and never synthesize a real brand or person — a plausible fake is worse than an honest placeholder.
235
+ - **Keep Chorus calm** — near-monochromatic neutral + one restrained blue accent, desaturated single-subject; avoid saturated red/orange/yellow, busy collages, plasticky AI stock. Only `src` / `alt` change (never `style` / `className` to fight slot geometry); `alt` matches the subject, not the role.
384
236
 
385
237
  ### Tone-adjective disarming
386
-
387
- Keywords like *"clean", "minimal", "subtle", "white background"* mean **information density** and **chrome restraint**, not removal of brand elements. Even in "minimal" you **must**:
388
-
389
- * Apply brand/semantic colors to key CTAs and active states.
390
- * Populate all image/thumbnail slots.
391
- * Map structures to `List`, `Carousel`, `Feed`, `Banner` instead of raw bordered divs.
392
-
393
- **Decorative atmospherics allowed.** An accent-toned stop fading to `transparent` inside a `radial-gradient` over a flat `surface*` base (where the base governs text contrast) is permitted decorative use. The rule governs **interactive and content-bearing** color (CTAs, active states, like counts, brand affordances); empty-space atmospherics don't count.
238
+ *"clean / minimal / subtle / white background"* mean **information density + chrome restraint**, NOT removing brand elements. Even in "minimal" you must apply brand/semantic color to key CTAs and active states, populate all image slots, and map structures to `List` / `Carousel` / `Feed` / `Banner` (not raw bordered divs). Decorative atmospherics (an accent radial-gradient fading to `transparent` over a flat `surface*` base) are allowed; the rule governs interactive / content-bearing color only.
394
239
 
395
240
  ### Brand color budget
396
-
397
- * **Brand red outside its allowlist.** Open `agents/tokens.usage.json#sys.color.brand` and verify every `var(--sys-color-brand)` usage falls inside `allowedComponents` (canonically: FAB, tab-bar Create item, badge, feed active-like, promotional banner accent). Any usage on `navigation-bar/*` chrome, `button/standard` fill, default banner fill, card outline, list-row divider, or shortcut tile is a violation.
398
- * **Brand instances ≤ 3 per screen.** Count every painted `var(--sys-color-brand)` (FAB + active-like hearts + promotional accents). Cap is 3. See `tokens.usage.json#sys.color.brand.maxInstancesPerScreen`.
241
+ - **Brand red stays in its allowlist** (`tokens.usage.json#sys.color.brand.allowedComponents` — canonically FAB, tab-bar Create item, badge, feed active-like, promotional banner accent). Not on nav chrome, `button/standard` fill, default banner fill, card outline, list divider, shortcut tile.
242
+ - **≤ 3 brand instances per screen** (`…#maxInstancesPerScreen`).
399
243
 
400
244
  ---
401
245
 
402
- ## D. Brownfield (in-progress project) mode
246
+ ## D. Pre-flight checklist (before presenting — any hit → discard + regenerate)
403
247
 
404
- **Scoped adoption** (user-designated area): §D applies *inside the declared boundary only* protocol in `agents/scoped-adoption.md`.
248
+ High-value gate; the full 18-shape audit is **`anti-patterns.md`**. Does NOT punish `"open"` families for visual-fit reuse.
405
249
 
406
- When this prompt is pasted into a Lovable session that already has UI built (shadcn, hand-rolled `div`-and-Tailwind, raw hex, third-party kits), **your job is to convert that UI to Chorus** — not preserve it, not coexist with it, not "match the existing style". The existing design is the *source* of a migration whose destination is pure Chorus. Mixed renders are forbidden.
407
-
408
- Detection signals on first read of `src/`:
409
-
410
- * Imports from `@/components/ui/*` (shadcn).
411
- * Tailwind color utilities (`bg-white`, `text-black`, `bg-gray-100`, `border-zinc-200`, ).
412
- * Raw hex in `className`/`style`/stylesheets (`#FFF`, `#1A1A1A`, `rgb(…)`).
413
- * Hand-rolled cards/lists/buttons/chips: bordered `<div>` + `rounded-*`, raw `<button>` + Tailwind, `<img>` for avatars without `<Thumbnail>` wrapper.
414
- * `tailwind.config.{js,ts}` whose `theme.colors` defines anything but Chorus tokens.
415
-
416
- Brownfield protocol **execute in order**:
417
-
418
- 1. **Audit before composing.** Post a one-paragraph drift report. Count: shadcn imports, Tailwind-color hits, raw-hex hits, hand-rolled-card hits. Name the worst three offenders. Under 6 lines. (Don't stall at the report — step 3 composes immediately.)
419
- 2. **Migration plan, ranked.** Short table mapping current → Chorus, ordered by user-visible blast radius: app shell/navigation first, recurring atoms (card, list-row, button) second, leaf screens third. Each row: `<current>` → `<Chorus>` (e.g. *"`<div className="rounded-lg border p-4">` → `<Carousel>` / `<Feed>`"*, *"`bg-white text-black` → drop; surface comes from `var(--sys-color-surface)` via `styles.css`"*).
420
- 3. **Reconstruct the representative screen immediately.** Don't wait for a brief — pick the entry / most-visible screen (router root, or the screen the message implies) and rebuild it in pure Chorus (full §A.2→A.3→A.4 pass) as the reference target showing the destination state. This is the **one** screen you compose proactively on the first turn — the deliberate exception to step 1. Then offer **(a) full conversion** (every remaining drift site now) or **(b) migrate-as-touched** (each screen when next touched). Default: (b).
421
- 4. **Compose-with-migration on touched areas.** Thereafter, when the user asks for a new screen/feature/fix, migrate Chorus-violating code in files you touch AND immediate visual neighbors (same route, same shared layout). Never let Chorus and non-Chorus coexist on one rendered screen.
422
- 5. **Out-of-scope = report only.** Distant files (beyond the reconstructed screen and touched neighbors) stay in the report, not edited unless the user opts into (a). Surface as a "next-PR shopping list" at the end.
423
- 6. **Conflict resolution.** If Tailwind config defines colors user code depends on (`bg-primary` etc.), do NOT silently remove — breaks unmigrated screens. Either map the alias to a Chorus token in the same PR (`primary: 'var(--sys-color-primary)'`), or leave config and migrate consumer to Chorus directly.
424
- 7. **Escape hatch.** User says *"just add the feature, don't migrate"* (or *"don't reconstruct yet"*) → demote steps 1–6 to a one-line drift note, skip the proactive reconstruction, and proceed greenfield for the new code only. Even then, new code MUST be pure Chorus.
425
-
426
- The existing style is the bug; Chorus is the fix.
427
-
428
- ---
250
+ - [ ] **Raw action** — `<button>` / `<a>` / styled `<div>` as a CTA; Text CTA without `appearance="accent"`; icons as text glyphs (`+`, `×`, `→`, `★`, `•`).
251
+ - [ ] **Bordered-`<div>` instead of a family** — card → Carousel / Feed / Banner; list/stack → List; avatar / letter-in-div → `<Thumbnail src=…>`. `Feed` / `List thumbnail` / `SuggestionList` / `DirectoryList` row missing its `thumbnail` slot; `Tabs` given bare string children.
252
+ - [ ] **Token violation** raw hex / Tailwind color / inline `style={{background:'#fff'}}` / off-scale px / numeric literal outside the three exceptions (`fontSize:13`, `gap:6`, `borderRadius:6`); or **lint not green** (preset unwired / `chorus/*` suppressed).
253
+ - [ ] **Wrapped or off-source** — Chorus component wrapped for CSS restyle, or imported from anywhere but `@teamblind-chorus/ui`.
254
+ - [ ] **Layout / alignment** — full-bleed re-paying the shell gutter, missing the bounded-surface negative-margin opt-out, or missing `embedded` in Carousel/Feed; Carousel headings / list-row leading / feed author blocks off one rail; vertical spacing as per-child `margin-top` instead of `gap: var(--sys-layout-stack-*)`.
255
+ - [ ] **Surface / shape** — `border:` on a card / row / feed-item / banner (→ inset shadow / `::after`); chip / pill / avatar radius ≠ `radius.full`; > 2 surface tiers stacked; Banner `brandContainer` when informational (→ `primaryContainer`).
256
+ - [ ] **Brand red** outside its allowlist, or **> 3 instances per screen**.
257
+ - [ ] **Typography < 12px** for visible copy (`sys.typo.label.sm` is the floor).
258
+ - [ ] **> 1 FAB**, or a floating action pinned as a `standard`/`text` Button instead of `variant="fab"`.
259
+ - [ ] **Bulk migration (§A.6)** — for an existing-project brief: migrated **> 1 screen in one pass**, presented a migrated screen without a **named rollback point + behavior verification**, or took a *sweeping* brief ("convert the whole app") as a batch license instead of a confirmed one-at-a-time screen list.
260
+ - [ ] **Fixed bars scroll with the page** — missing the 100dvh + `<main>{min-height:0; overflow-y:auto}` contract (§A.4).
429
261
 
430
- ## E. Post-generation pre-flight checklist
431
-
432
- Before presenting output, run this. Any checked box → **discard + regenerate**. Audits anatomy (tokens, slots, imports) and the five `visualReuse: "locked"` contracts — does NOT punish `"open"` families for visual-fit reuse.
433
-
434
- * [ ] Raw `<button>` or `<a>` as a CTA.
435
- * [ ] Card built as generic `<div>` with `border` + `rounded-lg` (must be `Carousel`/`Feed`/`Banner`).
436
- * [ ] List/stack as nested bordered `<div>` (must be `List`).
437
- * [ ] Avatar/logo as div with plain letter (must be `<Thumbnail src=...>`).
438
- * [ ] `Feed`, `List variant="thumbnail"`, `SuggestionList`, or `DirectoryList` row missing its `thumbnail` slot.
439
- * [ ] Active tab styled manually with `text-black font-bold` (must be `Tabs variant="underline"`).
440
- * [ ] Text CTA without `appearance="accent"`.
441
- * [ ] Inline `style={{ background: '#fff' }}` or Tailwind `bg-white`.
442
- * [ ] Filter chips as generic gray pills without explicit `selected`.
443
- * [ ] Raw hex, Tailwind color utilities, or off-scale px anywhere in markup.
444
- * [ ] **Chorus lint not green** — preset unwired or a `chorus/*` error suppressed, not fixed.
445
- * [ ] **Custom primitive (no Chorus family used)** — any numeric literal in `style` / `className` outside the three authorized exceptions ((1) intrinsic slot geometry, (2) `calc()` compositions, (3) structural `0` / `100%` / `auto`). `fontSize: 13`, `gap: 6`, `padding: "10px 12px"`, `lineHeight: 1.4`, `borderRadius: 6` are ALL violations.
446
- * [ ] Chorus component wrapped in a custom element for CSS restyling.
447
- * [ ] Chorus component imported from anywhere but `@teamblind-chorus/ui`.
448
- * [ ] **Full-bleed component re-paying horizontal padding** on top of the shell's `layout.page.*`. Shell pays it once; full-bleed children stretch edge-to-edge.
449
- * [ ] **Full-bleed child inside a bounded surface** (`Dialog`, `BottomSheet`, `Sheet`) NOT using the negative-margin opt-out.
450
- * [ ] **Full-bleed child inside another full-bleed host** (`Carousel`, `Feed`) NOT declaring `embedded={true}` when eligible (`AvatarRail`, `SuggestionList`, `Tabs`, `List`).
451
- * [ ] **Inline atom wrapped in a per-child padding div** (`<div style={{ padding: 8 }}><Thumbnail/></div>`, `<div style={{ paddingInline: 16 }}><Banner/></div>`). Use the parent row's `gap`.
452
- * [ ] Carousel headings, list-row leading, chip-group first chips, feed-item author blocks NOT all on the same vertical line.
453
- * [ ] Inside a Dialog/BottomSheet: sheet title, list-row leading, and primary action label NOT at one shared inset (apply recursive opt-out).
454
- * [ ] Vertical sibling spacing as `margin-top` on each child instead of `gap: var(--sys-layout-stack-*)` on shared parent.
455
- * [ ] **Brand red outside its allowlist** (check `tokens.usage.json#sys.color.brand.allowedComponents`).
456
- * [ ] **More than 3 brand instances per screen.**
457
- * [ ] **Chip/pill/avatar radius ≠ `radius.full`.** A 4–8px-rounded chip is a card; a fully-rounded "card" reads as a chip.
458
- * [ ] **`border:` on a card, list-row, feed-item, or banner** — must be inset shadow / `::after` overlay / `outlineVariant` divider.
459
- * [ ] **More than two surface tiers stacked.** A screen paints at most `surface` + one `surface*Container` rung.
460
- * [ ] **Banner background `brandContainer`** when role is informational (use `primaryContainer`) or default-promotional (use `surfaceContainerLow`). `brandContainer` is reserved for explicit promotional tinted strips.
461
- * [ ] **Typography below 12px** for visible copy. Anything under `sys.typo.label.sm` (12px) is for legal/aux. Tempted to drop a meta line to 11px? Take the next-larger rung.
462
- * [ ] **More than one FAB on screen.** Create is the single canonical commit; extras dilute the affordance.
463
- * [ ] **Floating action as a pinned `standard`/`text` Button** (`position: fixed`/`absolute`) instead of `<Button variant="fab">`.
464
- * [ ] **`Tabs` given bare text/string children** instead of `<Tab value=…>` elements — renders as run-together unstyled text.
465
- * [ ] **Fixed bars scroll with the page** — `.page-shell` missing the `height: 100dvh` + `<main> { min-height: 0; overflow-y: auto }` scroll contract (§A.4).
466
- * [ ] **Icons typed as text characters** (`+`, `×`, `→`, `★`, `•`, `·`). Use SVG components from `@teamblind-chorus/ui/icons`.
467
-
468
- Then run the **rail self-diagnostic** in the dev preview console (snippet in [`anti-patterns.md` § Rail self-diagnostic](anti-patterns.md)). Misalignment → discard + regenerate.
262
+ Then run the **rail self-diagnostic** (snippet in `anti-patterns.md`). Misalignment → discard + regenerate.
469
263
 
470
264
  ---
471
265
 
472
- **Proceed to the screen-specific brief. Apply all constraints above flawlessly.**
266
+ **Proceed to the screen brief. Apply every constraint above flawlessly.**