@teamblind-chorus/ui 1.2.0 → 2.0.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 (141) hide show
  1. package/README.md +3 -3
  2. package/agents/AGENTS.md +6 -6
  3. package/agents/DESIGN.md +245 -244
  4. package/agents/LOVABLE.md +40 -11
  5. package/agents/catalog.md +4 -4
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -4
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +10 -14
  8. package/agents/components/badge/role.md +7 -9
  9. package/agents/components/badge/role.spec.json +6 -6
  10. package/agents/components/badge/update.md +6 -8
  11. package/agents/components/badge/update.spec.json +5 -5
  12. package/agents/components/banner/banner.md +16 -18
  13. package/agents/components/banner/banner.spec.json +14 -14
  14. package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
  15. package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
  16. package/agents/components/bubble/bubble.md +8 -10
  17. package/agents/components/bubble/bubble.spec.json +11 -11
  18. package/agents/components/button/button.md +1 -1
  19. package/agents/components/button/check.md +9 -11
  20. package/agents/components/button/check.spec.json +8 -10
  21. package/agents/components/button/fab.md +7 -9
  22. package/agents/components/button/fab.spec.json +10 -12
  23. package/agents/components/button/group.spec.json +4 -4
  24. package/agents/components/button/icon.md +21 -23
  25. package/agents/components/button/icon.spec.json +12 -14
  26. package/agents/components/button/standard.md +40 -42
  27. package/agents/components/button/standard.spec.json +20 -22
  28. package/agents/components/button/text.md +21 -23
  29. package/agents/components/button/text.spec.json +13 -15
  30. package/agents/components/button/toggle.md +7 -9
  31. package/agents/components/button/toggle.spec.json +10 -12
  32. package/agents/components/button/toolbar.md +24 -26
  33. package/agents/components/button/toolbar.spec.json +10 -12
  34. package/agents/components/carousel/carousel.md +1 -1
  35. package/agents/components/carousel/post.md +15 -21
  36. package/agents/components/carousel/post.spec.json +17 -17
  37. package/agents/components/carousel/profile.md +9 -45
  38. package/agents/components/carousel/profile.spec.json +17 -17
  39. package/agents/components/chip/chip.md +1 -1
  40. package/agents/components/chip/filter.md +22 -24
  41. package/agents/components/chip/filter.spec.json +17 -13
  42. package/agents/components/chip/tag.md +22 -24
  43. package/agents/components/chip/tag.spec.json +19 -15
  44. package/agents/components/dialog/dialog.md +1 -3
  45. package/agents/components/dialog/dialog.spec.json +3 -3
  46. package/agents/components/directory-list/directory-list.md +1 -3
  47. package/agents/components/directory-list/directory-list.spec.json +2 -2
  48. package/agents/components/divider/divider.family.json +1 -1
  49. package/agents/components/divider/divider.md +12 -14
  50. package/agents/components/divider/divider.spec.json +8 -8
  51. package/agents/components/empty-state/empty-state.md +9 -9
  52. package/agents/components/empty-state/empty-state.spec.json +14 -14
  53. package/agents/components/feed/ad.md +2 -4
  54. package/agents/components/feed/ad.spec.json +10 -10
  55. package/agents/components/feed/post.md +41 -43
  56. package/agents/components/feed/post.spec.json +35 -39
  57. package/agents/components/form-field/form-field.md +1 -1
  58. package/agents/components/form-field/input.md +32 -34
  59. package/agents/components/form-field/input.spec.json +34 -33
  60. package/agents/components/form-field/search.md +2 -4
  61. package/agents/components/form-field/search.spec.json +19 -18
  62. package/agents/components/form-field/select.md +18 -20
  63. package/agents/components/form-field/select.spec.json +30 -29
  64. package/agents/components/form-field/textarea.md +3 -5
  65. package/agents/components/form-field/textarea.spec.json +32 -31
  66. package/agents/components/header/main.md +4 -6
  67. package/agents/components/header/main.spec.json +3 -3
  68. package/agents/components/header/sub.md +6 -8
  69. package/agents/components/header/sub.spec.json +3 -3
  70. package/agents/components/list/accordion.md +34 -45
  71. package/agents/components/list/accordion.spec.json +20 -20
  72. package/agents/components/list/entry.md +59 -81
  73. package/agents/components/list/entry.spec.json +20 -23
  74. package/agents/components/list/list.md +2 -2
  75. package/agents/components/list/radio.md +13 -20
  76. package/agents/components/list/radio.spec.json +16 -20
  77. package/agents/components/list/standard.md +50 -72
  78. package/agents/components/list/standard.spec.json +18 -21
  79. package/agents/components/metadata/compact.md +4 -6
  80. package/agents/components/metadata/compact.spec.json +6 -6
  81. package/agents/components/metadata/metadata.md +1 -1
  82. package/agents/components/metadata/standard.md +12 -14
  83. package/agents/components/metadata/standard.spec.json +10 -10
  84. package/agents/components/nav-card/nav-card.md +25 -27
  85. package/agents/components/nav-card/nav-card.spec.json +19 -19
  86. package/agents/components/nav-list/nav-list.md +2 -8
  87. package/agents/components/nav-list/nav-list.spec.json +3 -3
  88. package/agents/components/navigation-bar/main.md +9 -11
  89. package/agents/components/navigation-bar/main.spec.json +6 -6
  90. package/agents/components/navigation-bar/search.md +6 -8
  91. package/agents/components/navigation-bar/search.spec.json +9 -9
  92. package/agents/components/navigation-bar/sub.md +9 -11
  93. package/agents/components/navigation-bar/sub.spec.json +7 -7
  94. package/agents/components/pagination/pagination.family.json +1 -1
  95. package/agents/components/pagination/pagination.md +3 -3
  96. package/agents/components/pagination/pagination.spec.json +5 -5
  97. package/agents/components/profile-header/profile-header.md +9 -11
  98. package/agents/components/profile-header/profile-header.spec.json +9 -9
  99. package/agents/components/progress/progress.family.json +1 -1
  100. package/agents/components/progress/progress.md +5 -5
  101. package/agents/components/progress/progress.spec.json +8 -8
  102. package/agents/components/side-sheet/side-sheet.md +11 -13
  103. package/agents/components/side-sheet/side-sheet.spec.json +3 -3
  104. package/agents/components/skeleton/skeleton.md +7 -9
  105. package/agents/components/skeleton/skeleton.spec.json +5 -5
  106. package/agents/components/spinner/spinner.family.json +1 -1
  107. package/agents/components/spinner/spinner.md +8 -10
  108. package/agents/components/spinner/spinner.spec.json +9 -9
  109. package/agents/components/status-tag/status-tag.md +7 -9
  110. package/agents/components/status-tag/status-tag.spec.json +5 -5
  111. package/agents/components/suggestion-list/suggestion-list.md +3 -7
  112. package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
  113. package/agents/components/switch/switch.md +12 -14
  114. package/agents/components/switch/switch.spec.json +17 -18
  115. package/agents/components/tab-bar/tab-bar.md +9 -11
  116. package/agents/components/tab-bar/tab-bar.spec.json +25 -27
  117. package/agents/components/tabs/rounded.md +6 -8
  118. package/agents/components/tabs/rounded.spec.json +17 -15
  119. package/agents/components/tabs/segmented.md +4 -6
  120. package/agents/components/tabs/segmented.spec.json +4 -8
  121. package/agents/components/tabs/underline.md +9 -11
  122. package/agents/components/tabs/underline.spec.json +14 -16
  123. package/agents/components/thumbnail/thumbnail.md +5 -7
  124. package/agents/components/thumbnail/thumbnail.spec.json +8 -8
  125. package/agents/components/toast/toast.md +5 -7
  126. package/agents/components/toast/toast.spec.json +3 -3
  127. package/agents/components/tooltip/tooltip.md +6 -8
  128. package/agents/components/tooltip/tooltip.spec.json +4 -4
  129. package/agents/tokens.usage.json +71 -226
  130. package/dist/index.cjs +212 -223
  131. package/dist/index.cjs.map +1 -1
  132. package/dist/index.d.cts +16 -16
  133. package/dist/index.d.ts +16 -16
  134. package/dist/index.js +212 -223
  135. package/dist/index.js.map +1 -1
  136. package/dist/styles.css +386 -387
  137. package/eslint/rules.js +7 -7
  138. package/package.json +2 -3
  139. package/agents/anti-patterns.md +0 -533
  140. package/agents/compose.md +0 -240
  141. package/agents/images.md +0 -66
package/agents/compose.md DELETED
@@ -1,240 +0,0 @@
1
- # compose.md — composition cheatsheet
2
-
3
- A 1-page lookup for the design-token decisions every screen runs into. Skim **before composing JSX**; pair with [`tokens.usage.json`](tokens.usage.json) (which token for which slot) and [`DESIGN.md`](DESIGN.md) (deep rationale). When this file and DESIGN.md disagree, DESIGN.md wins.
4
-
5
- The recipes below answer the five compositional situations every product surface runs into. Not new tokens — every line resolves to a step in the standard `sys.*` ladder.
6
-
7
- ---
8
-
9
- ## Spacing recipes
10
-
11
- ### Page shell horizontal gutter
12
-
13
- | Pick | When |
14
- | --- | --- |
15
- | **`sys.layout.page.md`** (16px) | Default for every ordinary app route (feed, settings, compose, detail). |
16
- | `sys.layout.page.sm` (8px) | Dashboards / admin tables / dense multi-pane. |
17
- | `sys.layout.page.lg` (24→40px) | Marketing / editorial / landing. |
18
- | `sys.layout.page.xl` (40→64px) | Showcase heroes only. |
19
-
20
- **Paid once at the page shell.** All **thirteen full-bleed families** (`family.json#layoutInset` is authoritative) inherit it — `avatar-rail`, `carousel`, `directory-list`, `divider`, `feed`, `header`, `list`, `nav-list`, `navigation-bar`, `profile-header`, `suggestion-list`, `tab-bar`, `tabs` — plus the `feed-ad` (feed sub) and `accordion` (list sub) that inherit from their parent. Never re-pay `padding-inline` on the child. `chip` is `inline`, but a chip *group* (filter rail) is rail-responsible — place it like a full-bleed child (no padding wrapper). Inline cards (`banner`, `nav-card`) are NOT in this list — they don't claim the page rail themselves; the host (page shell at the top level, or another container when wrapped) pays their horizontal inset for them. See LOVABLE.md §A.4.
21
-
22
- ### Surface interior padding
23
-
24
- | Pick | When |
25
- | --- | --- |
26
- | **`sys.layout.container.md`** (16px) | Default — card, list-row, sheet content, section horizontal padding. |
27
- | `sys.layout.container.sm` (12px) | Button / input-field padding. Also the one-rung step-down for a child nested inside a `container.md` parent. |
28
- | `sys.layout.container.xs` (8px) | Chip body, segmented-control items, dense list rows. |
29
- | `sys.layout.container.lg` (24→32px) | Dialog body, feature-card callouts, primary dialog interiors. |
30
- | `sys.layout.container.xl/2xl/3xl` | Hero / marketing only. |
31
-
32
- **Nesting rule.** Parent at `container.md` → child at `container.sm` → grandchild at `container.xs`. Same direction across the tree — never invert, never skip rungs. A 16px parent with a 4px grandchild reads as compression, not hierarchy.
33
-
34
- ### Vertical sibling rhythm (`gap` between stacked siblings)
35
-
36
- | Pick | When |
37
- | --- | --- |
38
- | **`sys.layout.stack.md`** (16px) | Default — paragraph↔paragraph, card↔card, item↔item within one section. |
39
- | `sys.layout.stack.xs` (8px) | One tightly-bound group (bullet rows, metadata lines, cluster of fields about the same entity). Also section↔section separator when paddings alone don't separate them. |
40
- | `sys.layout.stack.2xs` (4px) | Visually bonded pairs only — label↔input, title↔subtitle, caption↔parent text. Not for general content. |
41
- | `sys.layout.stack.sm` (12px) | Form field↔field gap. |
42
- | `sys.layout.stack.lg` (24→32px) | Distinct content groups within a section — heading block↔body block, form group↔submit cluster. |
43
- | `sys.layout.stack.xl` (32→40px) | Page-section break — strong content break, still one scroll region. |
44
-
45
- **Apply on the shared parent via `gap`** (`flex-direction: column; gap: var(--sys-layout-stack-md)`). Never `margin-top` per child.
46
-
47
- ### Horizontal sibling rhythm
48
-
49
- | Pick | When |
50
- | --- | --- |
51
- | **`sys.layout.inline.md`** (12→16px) | Default — button group, inline action cluster, icon button row. |
52
- | `sys.layout.inline.sm` (8px) | Chip↔chip in a filter row, dense action cluster. |
53
- | `sys.layout.inline.xs` (4px) | Glyph↔label inside a tight control. |
54
- | `sys.layout.inline.lg` (12→16px) | Spacious inline pair, header trailing-action cluster. |
55
- | `sys.layout.inline.xl/2xl` | Toolbar cluster / marketing pair. |
56
-
57
- ---
58
-
59
- ## Color quartet picker
60
-
61
- Color tokens come in **four-token quartets** — `<role>` and `<role>Container` fills, each paired with `on<Role>` and `on<Role>Container` foregrounds. **Never split the pair.**
62
-
63
- | Surface intent | Fill | Foreground |
64
- | --- | --- | --- |
65
- | **Page / card / list row / feed item** | `sys.color.surface` | `sys.color.onSurface` (primary text) + `sys.color.onSurfaceVariant` (meta text) |
66
- | **Primary commit / link / active selection** | `sys.color.primary` | `sys.color.onPrimary` |
67
- | **Soft primary tint (info banner, filter chip selected)** | `sys.color.primaryContainer` | `sys.color.onPrimaryContainer` |
68
- | **Editorial / promotional / FAB** | `sys.color.brand` | `sys.color.onBrand` |
69
- | **Soft brand tint (promotional callout)** | `sys.color.brandContainer` | `sys.color.onBrandContainer` |
70
- | **Success state / positive metric** | `sys.color.success` | `sys.color.onSuccess` |
71
- | **Destructive commit / error state** | `sys.color.error` | `sys.color.onError` |
72
- | **Search input bar fill** | `sys.color.surfaceContainerLow` | `sys.color.onSurface` |
73
- | **Banner / cover band / image-area underlay** | `sys.color.surfaceContainerHigh` | `sys.color.onSurface` |
74
- | **Toast** | `sys.color.inverseSurface` | `sys.color.inverseOnSurface` |
75
- | **Dialog / BottomSheet scrim** | `sys.color.scrim` | n/a (decorative) |
76
- | **Card outline / list-row divider** | n/a (stroke) | `sys.color.outlineVariant` |
77
- | **Form-field active stroke / high-emphasis divider** | n/a (stroke) | `sys.color.outline` |
78
-
79
- ---
80
-
81
- ## Type ramp picker — by surface intent
82
-
83
- | Surface | Ramp |
84
- | --- | --- |
85
- | **Page-level title** (Home navigation-bar title, top-region heading) | `sys.typo.heading.lg` (24→32px) |
86
- | **Section / card title** (Section label, Feed.title, Job card title) | `sys.typo.heading.md` (20px) |
87
- | **Sub-section heading inside a card** | `sys.typo.heading.sm` (16px) |
88
- | **Single-topic body** (article, post detail, long-form description) | `sys.typo.body.md` (16px) |
89
- | **Mixed-group body** (settings page, compact feed item descriptions, card listing several short blocks) | `sys.typo.body.sm` (14px) |
90
- | **Primary button label / tab label** | `sys.typo.label.lg` (16px) |
91
- | **List-row primary label / chip label** | `sys.typo.label.md` (14px) |
92
- | **Meta line / supporting label / counter** | `sys.typo.label.sm` or `sys.typo.label.sm` (12px) |
93
- | **Article / figure caption / footnote** | `sys.typo.label.sm` (12px) |
94
-
95
- **Body-size rule of thumb**: single-topic page → `body.md`. Second peer text group joins → drop to `body.sm`.
96
-
97
- ## Type ramp picker — by component slot
98
-
99
- When composing a specific component, this table is more specific than the intent table above — pick the exact slot rather than reasoning about intent.
100
-
101
- | Component slot | Ramp | Weight |
102
- | --- | --- | --- |
103
- | `navigation-bar/main.title` (page-level wordmark) | `sys.typo.heading.lg` | 600 Semibold |
104
- | `navigation-bar/sub.title` (centered title in drill-in) | `sys.typo.heading.md` | 600 Semibold |
105
- | `section.label` (Section header) | `sys.typo.label.lg` | 600 Semibold |
106
- | `section.headerAction` (See all link) | `sys.typo.label.md` | 500 Medium |
107
- | `feed/post.title` (post headline) | `sys.typo.heading.md` | 600 Semibold |
108
- | `feed/post.body` (post body preview, 2-line clamp) | `sys.typo.body.md` | 400 Regular |
109
- | `feed/post.meta` (channel · time) | `sys.typo.label.sm` | 400 Regular |
110
- | `feed/post.author.name` | `sys.typo.label.md` | 600 Semibold |
111
- | `feed/post.engagement.count` (likes / comments / views) | `sys.typo.label.sm` | 400 Regular |
112
- | `list/standard.primary` (row primary label) | `sys.typo.body.md` | 400 Regular |
113
- | `list/standard.supporting` (row supporting text) | `sys.typo.body.sm` | 400 Regular |
114
- | `button/standard.label` | `sys.typo.label.lg` | 600 Semibold |
115
- | `button/text.label` | `sys.typo.label.md` | 500 Medium |
116
- | `chip.label` (filter / tag) | `sys.typo.label.md` | 500 Medium |
117
- | `tab/underline.label` (top-tab label) | `sys.typo.label.md` | 600 Semibold (active) / 500 Medium (rest) |
118
- | `tab-bar.item.label` (bottom-tab label) | `sys.typo.label.sm` | 500 Medium |
119
- | `form-field.label` | `sys.typo.label.md` | 500 Medium |
120
- | `form-field.input.value` | `sys.typo.body.md` | 400 Regular |
121
- | `form-field.helperText` | `sys.typo.label.sm` | 400 Regular |
122
- | `banner.title` | `sys.typo.label.lg` | 600 Semibold |
123
- | `banner.body` | `sys.typo.body.sm` | 400 Regular |
124
- | `toast.body` | `sys.typo.label.md` | 500 Medium |
125
- | `dialog.title` | `sys.typo.heading.sm` | 600 Semibold |
126
- | `dialog.body` | `sys.typo.body.md` | 400 Regular |
127
- | `badge.label` (numeric / text) | `sys.typo.label.sm` | 600 Semibold |
128
- | `suggestion-list.row.name` | `sys.typo.label.md` | 600 Semibold |
129
- | `suggestion-list.row.followers` | `sys.typo.label.sm` | 400 Regular |
130
-
131
- **Avoid the under-12px trap.** Agents often default to 11-13px for "compact" copy — that breaks Korean / CJK hierarchy. When unsure, take the next-larger rung. The smallest rung for *visible* copy is 12px (`label.sm` / `label.sm`); below is reserved for legal / aux.
132
-
133
- ---
134
-
135
- ## Composition guard rails (hard one-liners)
136
-
137
- DESIGN.md rules condensed to a single line each. Read as **immediate-reject** triggers when reviewing your own output.
138
-
139
- 1. **Brand red is an accent marker, never a surface.** No `navigation-bar` chrome paints brand. No banner background paints brand (use `primaryContainer` for info, `surfaceContainerLow` / `secondaryContainer` for promotional). Brand instances per screen ≤ 3 — canonically: Create tab item (1), feed active-like (≤2), optional promotional banner accent (1). See [`tokens.usage.json#sys.color.brand`](tokens.usage.json).
140
- 2. **Card outlines: `outlineVariant` hairline as inset shadow** (or `::after` overlay when the card hosts a full-bleed child). **Never `border:` on a card.** A `border` reflows the box; an inset shadow / overlay does not. The reflow is the bug.
141
- 3. **List rows: only `outlineVariant` divider between rows.** No per-row `border`. The list owns the seam, the row owns the click target.
142
- 4. **Surface tier ≤ 2 levels per screen.** `surface` plus one `surface*Container` rung is the cap. A third nested surface tone reads muddy — promote one to a different family (Banner, Card, Section header) instead of layering.
143
- 5. **Chip / pill / avatar radius is always `radius.full`.** A 4px-rounded "chip" is a card; pick one component. Likewise a 999-rounded "card" reads as a chip.
144
- 6. **Banner role decides the fill.** Informational → `sys.color.primaryContainer`. Promotional → `sys.color.surfaceContainerLow` (with optional brand accent on the leading icon, *not* the background). Error notice → `sys.color.errorContainer`. **`brandContainer` is reserved for promotional tinted strips, not default banners.**
145
- 7. **Page inset is paid once at the page shell.** Every `full-bleed` family (AvatarRail, Carousel, DirectoryList, Divider, Feed — plus its FeedAd sub — Header (both `<Header>` and `<SubHeader>`), List — plus its accordion sub — NavList, NavigationBar, ProfileHeader, SuggestionList, TabBar, Tabs) stretches edge-to-edge and pays its own row inset internally — never wrap one in a padded container or pass it `padding-inline`, or it double-pays the rail and lands at a different margin than its siblings. `Banner` / `nav-card` are **inline** (host owns the inset), not full-bleed; `Chip` is **inline** too, but a chip *group* is rail-responsible — place it like a full-bleed child. See `family.json#layoutInset` — the authoritative per-family value.
146
- 8. **Nesting tightens, never widens.** Parent `container.md` → child `container.sm` → grandchild `container.xs`. Inverting reads as compression, not hierarchy.
147
- 9. **Spec slot grammar is closed.** If a slot is not declared in `spec.json#slots`, it does not exist. Do not synthesize new slots, do not pass `className` / `style` overrides.
148
- 10. **FAB count ≤ 1 per screen.** Create is the single canonical commit — additional FABs dilute the affordance.
149
-
150
- ---
151
-
152
- ## Radius picker
153
-
154
- | Shape | Pick |
155
- | --- | --- |
156
- | **Card / banner / dialog body / Feed item / Section card** | `sys.radius.md` (8px) |
157
- | **BottomSheet top corners / toast pill / large dialog** | `sys.radius.lg` (12px) |
158
- | **Poll option row / list radio / tag pill** | `sys.radius.sm` (4px) |
159
- | **Chip outer / pill button / circular Thumbnail / avatar** | `sys.radius.full` |
160
- | **Hero surface** | `sys.radius.xl` / `2xl` |
161
-
162
- ---
163
-
164
- ## Stroke + elevation picker
165
-
166
- | Slot | Pick |
167
- | --- | --- |
168
- | **Card outline / list-row divider / form-field rest stroke** | `sys.borderWidth.hairline` (1px) × `sys.color.outlineVariant` |
169
- | **Form-field active / focus ring outer** | `sys.borderWidth.thin` (2px) × `sys.color.outline` or `sys.color.focus` |
170
- | **Cards at rest / hovered list rows** | `sys.elevation.raised` |
171
- | **FAB / floating menu / dropdown** | `sys.elevation.floating` |
172
- | **Dialog / modal / popover above scrim** | `sys.elevation.overlay` |
173
- | **BottomSheet** | `sys.elevation.sheet` (shadow projects upward) |
174
-
175
- ---
176
-
177
- ## Quick decision flow when composing a new surface
178
-
179
- 1. **What lives on it?**
180
- - Authored content (post body, article) → Feed / Section
181
- - Menu / settings / picker rows → List
182
- - Editorial collection → Section + carousel sub
183
- - Page-level chrome → NavigationBar / TabBar
184
- 2. **What's the surface fill?** Pick from the [color quartet picker](#color-quartet-picker) by intent.
185
- 3. **What's the interior padding?** Default `container.md`; tighten one rung per level of nesting.
186
- 4. **What's the vertical rhythm to siblings?** Default `stack.md`; tighten to `stack.xs` for one-bound-group; widen to `stack.lg` for distinct groups.
187
- 5. **What's the corner radius?** Default `radius.md`; pill / chip → `radius.full`; large sheet → `radius.lg`.
188
- 6. **Edge stroke (if any)?** `borderWidth.hairline × outlineVariant`. If the surface hosts an opaque full-bleed child (cover image, hero), promote the outline to a `::after` overlay layer (DESIGN.md § Border & Stroke).
189
-
190
- If a step has no good match — that's a **Chorus gap**, not a license to invent. Flag in one line and stop.
191
-
192
- ---
193
-
194
- ## When you go custom (no Chorus family fits)
195
-
196
- The LEGO ladder didn't surface a fit and you've genuinely exhausted visual-reuse on the `"open"` families (count owned by LOVABLE §C / `family.json#visualReuse`) — you're building a custom primitive (small hint card, inline annotation, narrow aside). **Component went flexible; tokens did not.** With no Chorus spec to deny you, every literal value you type is either a token resolution or a violation.
197
-
198
- The drift shape this section guards: a custom div where `background` is a token (because color rules are well-internalized) but `gap`, `padding`, `fontSize`, `lineHeight`, `borderRadius` are raw px (because the "off-scale px" rule lives in prose, not in concrete examples). Five literals, five violations — *"I used the color token"* is not a partial pass.
199
-
200
- ### Raw → token map for custom surfaces
201
-
202
- | You typed | Likely intent | Pick |
203
- | --- | --- | --- |
204
- | `fontSize: 13` | compact body / meta line | `className="sys-typo-body-md"` (16) or `sys-typo-label-md` (14). 13 is off-scale — pick the next rung. |
205
- | `fontSize: 14` | strong label / row primary | `className="sys-typo-label-lg"` (16, 600 — strong) or `sys-typo-label-md` (14, 500 — quiet) |
206
- | `fontSize: 11` or `10` | "tiny" caption | **Forbidden for visible copy.** Floor is `sys.typo.label.sm` (12) — see [anti-patterns.md #7](anti-patterns.md). |
207
- | `fontWeight: 600` (set alone) | bold label | `className="sys-typo-*"` — every typo rung bundles its canonical weight. Don't set weight separately. |
208
- | `lineHeight: 1.4` | "comfortable" line height | **Don't set it.** Typo tokens already carry line-height; setting it overrides the rung's contract. |
209
- | `gap: 6` | tight inline cluster | `gap: var(--sys-layout-inline-xs)` (4) or `var(--sys-layout-inline-sm)` (8) — pick a rung, not halfway. |
210
- | `gap: 10` | medium horizontal cluster | `var(--sys-layout-inline-sm)` (8) or `var(--sys-layout-inline-md)` (12) |
211
- | `gap: 6` (vertical between siblings) | tight vertical stack | `gap: var(--sys-layout-stack-2xs)` (4) or `stack.xs` (8). Vertical uses `stack.*`, not `inline.*`. |
212
- | `padding: "10px 12px"` | dense surface interior | `var(--sys-layout-container-xs) var(--sys-layout-container-sm)` (8 12). Two rung tokens on one line is fine. |
213
- | `padding: 16` | default surface interior | `var(--sys-layout-container-md)` |
214
- | `paddingInline: 16` (page level) | shell gutter | `var(--sys-layout-page-md)` — paid once at the shell, never on a full-bleed child. |
215
- | `borderRadius: 6` | small radius | `var(--sys-radius-sm)` (4) or `var(--sys-radius-md)` (8) — no in-between. |
216
- | `borderRadius: 10` | medium radius | `var(--sys-radius-md)` (8) or `var(--sys-radius-lg)` (12) |
217
- | `borderRadius: 999` on a chip-shaped surface | pill | `var(--sys-radius-full)` |
218
- | `border: "1px solid #..."` | hairline outline | `box-shadow: inset 0 0 0 var(--sys-borderWidth-hairline) var(--sys-color-outlineVariant)` (no-layout stroke — `border:` reflows the box; see [anti-patterns.md #2](anti-patterns.md)). |
219
- | `background: "#fff"` / `"#FFF"` | page surface | `var(--sys-color-surface)` |
220
- | `color: "#1A1A1A"` / `"#333"` | body text | `var(--sys-color-onSurface)` (primary) / `var(--sys-color-onSurfaceVariant)` (meta) |
221
-
222
- > **Typography is a class, not a `font` shorthand.** A type rung is five properties (family + size + weight + line-height + tracking). There is **no `font: var(--sys-typo-*)` token** — and the CSS `font` shorthand can't carry letter-spacing anyway, so that declaration voids and the text falls back to a system font. Apply the bundled utility class instead: `className="sys-typo-body-md"` (roles `display`/`heading`/`body`/`label` × rungs `lg`/`md`/`sm`, plus the rung-less `sys-typo-caption`). If a class genuinely can't be attached, set all four `var(--sys-typo-<role>-<rung>-{size,weight,line,tracking})` vars — dropping one drifts the rung. Never set `lineHeight`/`fontWeight` separately alongside.
223
-
224
- ### The three authorized literal exceptions (per `DESIGN.md`)
225
-
226
- Anything outside these is a violation, even on a custom surface:
227
-
228
- 1. **Intrinsic geometry** naming component anatomy — Thumbnail rung `48px`, Tooltip `min-height: 32px`, icon `16px` (slot contract, not layout).
229
- 2. **Computed compositions** combining tokens in `calc()` — `calc(48px + var(--sys-layout-inline-lg))` to anchor a divider to an avatar's trailing edge.
230
- 3. **Structural `0` / `100%` / `auto`** — no axis to resolve.
231
-
232
- ### What to do when no rung fits
233
-
234
- A "Chorus gap" report — one line, stop. Example: *"Building `<HintCard>` — `sys.layout.inline.xs` (4) reads too tight for the leading icon ↔ body gap, `inline.sm` (8) too loose. Proposing a new `inline.2xs` (6) rung or accept the `inline.sm` reading. Not inlining `gap: 6`."* Then wait. **Never** inline as a workaround.
235
-
236
- ### Mental check before shipping a custom surface
237
-
238
- > *"Scan every numeric literal in my style/className. For each: is it (a) a token call, (b) one of the three exceptions, or (c) a violation? If (c) — substitute the rung above, or file a Chorus gap. No fourth option."*
239
-
240
- Pair with the [§E pre-flight custom-primitive checkbox](LOVABLE.md) before declaring done.
package/agents/images.md DELETED
@@ -1,66 +0,0 @@
1
- # Images — generate vs. placeholder
2
-
3
- How to fill Chorus image slots (`Thumbnail.src`, `Feed.thumbnail`, `FeedAd.media`, `ProfileHeader` avatar/cover, `ProfileCarousel.items[].cover`, every `"assetType": "image"` slot) so a screen reads as finished — not a wall of placeholders, and not hallucinated real brands.
4
-
5
- This is the **method** behind AGENTS.md rule #9 ("when the composition gives a clear subject, swap the placeholder for a context-appropriate image"). Rule #9 names the goal; this file is the decision tree, the sanctioned generation path, and the handoff report.
6
-
7
- ## The decision tree (run per image slot)
8
-
9
- ```
10
- Is a real asset given (data field, URL, upload)?
11
- └─ yes → use it. Done.
12
- Does the composition imply a CLEAR subject?
13
- ├─ no → /placeholder.png. (No subject to depict — the branded placeholder is honest, not unfinished-by-mistake.)
14
- └─ yes → is the subject BRAND-SAFE TO SYNTHESIZE?
15
- ├─ yes (abstract avatar, generic cover band, topic/content thumbnail,
16
- │ illustrative scene) → GENERATE it (see below).
17
- └─ no (a real company's actual logo, a real named person's face,
18
- a copyrighted mark) → /placeholder.png + TODO.
19
- Never synthesize a real brand/person — a plausible fake is worse
20
- than an honest placeholder.
21
- ```
22
-
23
- **The hybrid rule in one line:** generate where the subject is clear *and* safe to invent (avatars, covers, post thumbnails, illustrations); placeholder where the subject is real-world-specific (actual logos, real people) or absent — and always *report* what stayed on placeholder.
24
-
25
- ## Generate ≠ invent a stock URL
26
-
27
- anti-patterns §13 forbids "an invented stock URL" / inline-SVG wordmark / `display:none` as a placeholder workaround. That prohibition stands. It is **not** the sanctioned path here. Sanctioned generation is: produce the asset with your image-generation tool (e.g. `imagegen`), upload it through your asset pipeline (e.g. `lovable-assets`), and store the returned URL in the data field. The difference: a generated, uploaded, self-hosted asset is a real asset the app owns; a pasted `images.unsplash.com/…` URL is an unowned, breakable, off-contract reference. Generate-and-host = allowed; link-to-someone-else's-image = forbidden.
28
-
29
- ### Prompt shape per slot
30
-
31
- - **Avatar (person / anonymous member)** — abstract or illustrative profile, no real face: "minimal flat illustrative avatar, neutral studio background, no text." Square. Feed/List/Metadata leading.
32
- - **Channel / org logo** — only if generic/fictional. A real company → placeholder + TODO. Generic → "simple geometric monogram logo, flat, single accent color, transparent or neutral fill."
33
- - **Cover band (ProfileHeader / Carousel cover)** — "calm abstract gradient/texture banner, on-brand neutral palette, no text, wide aspect." Wide.
34
- - **Post thumbnail** — match the body topic: "editorial photo of <topic>, natural light, uncluttered." 80×80 crop-safe.
35
-
36
- Keep generated imagery calm and on-palette so it sits under Chorus tokens; the slot's footprint/chrome never changes — **only the `src` URL changes**.
37
-
38
- ## Make the data model image-ready (the upstream fix)
39
-
40
- The most common cause of placeholder-everywhere is a mock/data model with **no image field** — so the screen has nothing to bind, and every slot hardcodes `/placeholder.png`. Fix it at the type, once:
41
-
42
- ```ts
43
- // ❌ Drift root — no image field, so every Thumbnail/Feed slot hardcodes the placeholder
44
- type Author = { id: string; handle: string; role: string };
45
- type Post = { id: string; title: string; body: string };
46
-
47
- // ✅ Image-ready — every entity that feeds an image slot carries the field,
48
- // initialized to the placeholder. Swapping in a real/generated image is then
49
- // a one-field edit, never a schema migration.
50
- type Author = { id: string; handle: string; role: string; avatarUrl: string }; // = "/placeholder.png" until filled
51
- type Post = { id: string; title: string; body: string; thumbnailUrl: string };
52
- ```
53
-
54
- When you scaffold mock data, **add the image field and seed it** (placeholder, or a generated URL per the tree above) — don't omit the field and hardcode `/placeholder.png` at the call site. A seeded field is swappable; a hardcoded `src` is a find-and-replace across screens.
55
-
56
- ## Report what stayed on placeholder (the handoff)
57
-
58
- A placeholder left silently reads as "done." After composing a screen (or a batch), **post an image-status line** so remaining slots are a visible TODO, not a mistake:
59
-
60
- > `Images: 4 real · 6 generated · 3 on placeholder — TODO: <ChannelLogo (real Acme logo), 2× real-author avatars>. Say the word to generate/swap.`
61
-
62
- List the placeholder slots and *why* each stayed (real-brand/real-person → needs a real asset; no subject → intentionally blank). This makes the hybrid policy auditable and lets the user green-light the real-asset items in one turn.
63
-
64
- ---
65
-
66
- See also: [anti-patterns.md §13](anti-patterns.md) (placeholder 404 / forbidden workarounds), AGENTS.md rule #9 (the image-slot contract), and each `*/*.spec.json` image-slot description.