@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.
- package/README.md +3 -3
- package/agents/AGENTS.md +6 -6
- package/agents/DESIGN.md +245 -244
- package/agents/LOVABLE.md +40 -11
- package/agents/catalog.md +4 -4
- package/agents/components/avatar-rail/avatar-rail.md +2 -4
- package/agents/components/avatar-rail/avatar-rail.spec.json +10 -14
- package/agents/components/badge/role.md +7 -9
- package/agents/components/badge/role.spec.json +6 -6
- package/agents/components/badge/update.md +6 -8
- package/agents/components/badge/update.spec.json +5 -5
- package/agents/components/banner/banner.md +16 -18
- package/agents/components/banner/banner.spec.json +14 -14
- package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
- package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
- package/agents/components/bubble/bubble.md +8 -10
- package/agents/components/bubble/bubble.spec.json +11 -11
- package/agents/components/button/button.md +1 -1
- package/agents/components/button/check.md +9 -11
- package/agents/components/button/check.spec.json +8 -10
- package/agents/components/button/fab.md +7 -9
- package/agents/components/button/fab.spec.json +10 -12
- package/agents/components/button/group.spec.json +4 -4
- package/agents/components/button/icon.md +21 -23
- package/agents/components/button/icon.spec.json +12 -14
- package/agents/components/button/standard.md +40 -42
- package/agents/components/button/standard.spec.json +20 -22
- package/agents/components/button/text.md +21 -23
- package/agents/components/button/text.spec.json +13 -15
- package/agents/components/button/toggle.md +7 -9
- package/agents/components/button/toggle.spec.json +10 -12
- package/agents/components/button/toolbar.md +24 -26
- package/agents/components/button/toolbar.spec.json +10 -12
- package/agents/components/carousel/carousel.md +1 -1
- package/agents/components/carousel/post.md +15 -21
- package/agents/components/carousel/post.spec.json +17 -17
- package/agents/components/carousel/profile.md +9 -45
- package/agents/components/carousel/profile.spec.json +17 -17
- package/agents/components/chip/chip.md +1 -1
- package/agents/components/chip/filter.md +22 -24
- package/agents/components/chip/filter.spec.json +17 -13
- package/agents/components/chip/tag.md +22 -24
- package/agents/components/chip/tag.spec.json +19 -15
- package/agents/components/dialog/dialog.md +1 -3
- package/agents/components/dialog/dialog.spec.json +3 -3
- package/agents/components/directory-list/directory-list.md +1 -3
- package/agents/components/directory-list/directory-list.spec.json +2 -2
- package/agents/components/divider/divider.family.json +1 -1
- package/agents/components/divider/divider.md +12 -14
- package/agents/components/divider/divider.spec.json +8 -8
- package/agents/components/empty-state/empty-state.md +9 -9
- package/agents/components/empty-state/empty-state.spec.json +14 -14
- package/agents/components/feed/ad.md +2 -4
- package/agents/components/feed/ad.spec.json +10 -10
- package/agents/components/feed/post.md +41 -43
- package/agents/components/feed/post.spec.json +35 -39
- package/agents/components/form-field/form-field.md +1 -1
- package/agents/components/form-field/input.md +32 -34
- package/agents/components/form-field/input.spec.json +34 -33
- package/agents/components/form-field/search.md +2 -4
- package/agents/components/form-field/search.spec.json +19 -18
- package/agents/components/form-field/select.md +18 -20
- package/agents/components/form-field/select.spec.json +30 -29
- package/agents/components/form-field/textarea.md +3 -5
- package/agents/components/form-field/textarea.spec.json +32 -31
- package/agents/components/header/main.md +4 -6
- package/agents/components/header/main.spec.json +3 -3
- package/agents/components/header/sub.md +6 -8
- package/agents/components/header/sub.spec.json +3 -3
- package/agents/components/list/accordion.md +34 -45
- package/agents/components/list/accordion.spec.json +20 -20
- package/agents/components/list/entry.md +59 -81
- package/agents/components/list/entry.spec.json +20 -23
- package/agents/components/list/list.md +2 -2
- package/agents/components/list/radio.md +13 -20
- package/agents/components/list/radio.spec.json +16 -20
- package/agents/components/list/standard.md +50 -72
- package/agents/components/list/standard.spec.json +18 -21
- package/agents/components/metadata/compact.md +4 -6
- package/agents/components/metadata/compact.spec.json +6 -6
- package/agents/components/metadata/metadata.md +1 -1
- package/agents/components/metadata/standard.md +12 -14
- package/agents/components/metadata/standard.spec.json +10 -10
- package/agents/components/nav-card/nav-card.md +25 -27
- package/agents/components/nav-card/nav-card.spec.json +19 -19
- package/agents/components/nav-list/nav-list.md +2 -8
- package/agents/components/nav-list/nav-list.spec.json +3 -3
- package/agents/components/navigation-bar/main.md +9 -11
- package/agents/components/navigation-bar/main.spec.json +6 -6
- package/agents/components/navigation-bar/search.md +6 -8
- package/agents/components/navigation-bar/search.spec.json +9 -9
- package/agents/components/navigation-bar/sub.md +9 -11
- package/agents/components/navigation-bar/sub.spec.json +7 -7
- package/agents/components/pagination/pagination.family.json +1 -1
- package/agents/components/pagination/pagination.md +3 -3
- package/agents/components/pagination/pagination.spec.json +5 -5
- package/agents/components/profile-header/profile-header.md +9 -11
- package/agents/components/profile-header/profile-header.spec.json +9 -9
- package/agents/components/progress/progress.family.json +1 -1
- package/agents/components/progress/progress.md +5 -5
- package/agents/components/progress/progress.spec.json +8 -8
- package/agents/components/side-sheet/side-sheet.md +11 -13
- package/agents/components/side-sheet/side-sheet.spec.json +3 -3
- package/agents/components/skeleton/skeleton.md +7 -9
- package/agents/components/skeleton/skeleton.spec.json +5 -5
- package/agents/components/spinner/spinner.family.json +1 -1
- package/agents/components/spinner/spinner.md +8 -10
- package/agents/components/spinner/spinner.spec.json +9 -9
- package/agents/components/status-tag/status-tag.md +7 -9
- package/agents/components/status-tag/status-tag.spec.json +5 -5
- package/agents/components/suggestion-list/suggestion-list.md +3 -7
- package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
- package/agents/components/switch/switch.md +12 -14
- package/agents/components/switch/switch.spec.json +17 -18
- package/agents/components/tab-bar/tab-bar.md +9 -11
- package/agents/components/tab-bar/tab-bar.spec.json +25 -27
- package/agents/components/tabs/rounded.md +6 -8
- package/agents/components/tabs/rounded.spec.json +17 -15
- package/agents/components/tabs/segmented.md +4 -6
- package/agents/components/tabs/segmented.spec.json +4 -8
- package/agents/components/tabs/underline.md +9 -11
- package/agents/components/tabs/underline.spec.json +14 -16
- package/agents/components/thumbnail/thumbnail.md +5 -7
- package/agents/components/thumbnail/thumbnail.spec.json +8 -8
- package/agents/components/toast/toast.md +5 -7
- package/agents/components/toast/toast.spec.json +3 -3
- package/agents/components/tooltip/tooltip.md +6 -8
- package/agents/components/tooltip/tooltip.spec.json +4 -4
- package/agents/tokens.usage.json +71 -226
- package/dist/index.cjs +212 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +212 -223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +386 -387
- package/eslint/rules.js +7 -7
- package/package.json +2 -3
- package/agents/anti-patterns.md +0 -533
- package/agents/compose.md +0 -240
- package/agents/images.md +0 -66
package/agents/LOVABLE.md
CHANGED
|
@@ -58,7 +58,7 @@ On *any* first message (full paste, truncated mid-stream, blank template, existi
|
|
|
58
58
|
|
|
59
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`?*
|
|
60
60
|
|
|
61
|
-
**Rail self-diagnostic:** before calling it "done", paste the console snippet from
|
|
61
|
+
**Rail self-diagnostic:** before calling it "done", paste the console snippet from **§E Rail self-diagnostic** (below) — it fails if any full-bleed child's left/right edge disagrees by >1px. Run with Dialog/BottomSheet open *and* closed. Misalignment → discard + regenerate.
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
@@ -104,8 +104,6 @@ Don't abbreviate the bracketed evidence; don't post readiness if any file is unr
|
|
|
104
104
|
| `patterns/<name>.md` | Per-screen recipe (intent / layout / tokens / components); PNG on GitHub, same slug. |
|
|
105
105
|
| `tokens.usage.json` | Per-token `value`/`role`/`usedFor`/`notFor`/`pairsWith`/`allowedComponents`. **Read before composing.** |
|
|
106
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
107
|
| `AGENTS.md` | Hard agent contract (principles + hard rules). |
|
|
110
108
|
| `DESIGN.md` | Token model & foundations. **~1300 lines — fetch by anchor only** (below). |
|
|
111
109
|
|
|
@@ -130,7 +128,7 @@ Patterns are layout-level ground truth (`.md` = textual contract, GitHub `.png`
|
|
|
130
128
|
### A.3 Component contract lookup (every component)
|
|
131
129
|
|
|
132
130
|
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"` (
|
|
131
|
+
1. **Locate family + sub** via `manifest.json`; multi-sub families → match `useCases` in `family.json`. **Check `visualReuse`:** `"open"` (30) allows visual-fit pick; `"locked"` (5) is canonical-role only.
|
|
134
132
|
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
133
|
3. **Read `.md`** for when / why + anatomy invariants.
|
|
136
134
|
4. **Honor slot kind.** `intrinsic:true` → component paints it, don't fill. `accepts:["thumbnail"]` → content is a Chorus component, not raw `<img>` / div.
|
|
@@ -179,7 +177,7 @@ A brief like *"convert this page to Chorus"* / *"replace this UI"* is a **presen
|
|
|
179
177
|
## B. Design principles
|
|
180
178
|
|
|
181
179
|
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"` (
|
|
180
|
+
2. **Extrapolate, don't fork** — respect anatomy invariants (slot grammar, sizing tokens, state contract), flex composition. `"open"` (30) may be picked on visual fit; `"locked"` (5) canonical-role only.
|
|
183
181
|
3. **New surfaces stay token-true** — every color / spacing / type / radius / border-width / elevation resolves through tokens + DESIGN.md. **Component flexible, tokens never.**
|
|
184
182
|
4. **Lego-block composition** — combine and extend Chorus Lego-style.
|
|
185
183
|
5. **UX-pattern consistency** — Dialog / BottomSheet / Toast / Tooltip / FormField own focus trap / auto-dismiss / ARIA live / hover trigger / `<input>` semantics; never borrow for shape.
|
|
@@ -197,10 +195,10 @@ A brief like *"convert this page to Chorus"* / *"replace this UI"* is a **presen
|
|
|
197
195
|
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)`).
|
|
198
196
|
|
|
199
197
|
### 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;
|
|
198
|
+
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; full contract `AGENTS.md` #9).
|
|
201
199
|
|
|
202
200
|
### Token strictness (no literals)
|
|
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
|
|
201
|
+
- **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 applies (`gap:6`→nearest `stack`/`inline` rung, `border:1px`→inset shadow, off-ladder radius→nearest 4/8/12/16/full rung).
|
|
204
202
|
- **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
203
|
- **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.
|
|
206
204
|
|
|
@@ -220,7 +218,7 @@ Each family's **`spec.json#forbidden`** is the authoritative negative list — r
|
|
|
220
218
|
| avatar / logo / leading image | `Thumbnail` | requires `src` |
|
|
221
219
|
| unread count / numeric pill | `Badge` | — |
|
|
222
220
|
|
|
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
|
|
221
|
+
**`"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 30 allow visual-fit reuse as long as anatomy invariants hold. Specialised families — `FeedAd`, `DirectoryList`, `NavList`, `SuggestionList`, `AvatarRail`, `Bubble`, `Divider` — see `catalog.md`.
|
|
224
222
|
|
|
225
223
|
### CTAs
|
|
226
224
|
- **Primary commit:** `<Button>` (standard, filled). **"See all" / links:** `<Button variant="text" appearance="accent">`. **Icon-only:** `<Button variant="icon">`.
|
|
@@ -231,7 +229,7 @@ Each family's **`spec.json#forbidden`** is the authoritative negative list — r
|
|
|
231
229
|
|
|
232
230
|
### Images & thumbnails
|
|
233
231
|
- 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
|
|
232
|
+
- **Fill order:** (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
233
|
- **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.
|
|
236
234
|
|
|
237
235
|
### Tone-adjective disarming
|
|
@@ -245,7 +243,7 @@ Each family's **`spec.json#forbidden`** is the authoritative negative list — r
|
|
|
245
243
|
|
|
246
244
|
## D. Pre-flight checklist (before presenting — any hit → discard + regenerate)
|
|
247
245
|
|
|
248
|
-
High-value gate
|
|
246
|
+
High-value gate covering the most common failure shapes. Does NOT punish `"open"` families for visual-fit reuse.
|
|
249
247
|
|
|
250
248
|
- [ ] **Raw action** — `<button>` / `<a>` / styled `<div>` as a CTA; Text CTA without `appearance="accent"`; icons as text glyphs (`+`, `×`, `→`, `★`, `•`).
|
|
251
249
|
- [ ] **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.
|
|
@@ -259,7 +257,38 @@ High-value gate; the full 18-shape audit is **`anti-patterns.md`**. Does NOT pun
|
|
|
259
257
|
- [ ] **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
258
|
- [ ] **Fixed bars scroll with the page** — missing the 100dvh + `<main>{min-height:0; overflow-y:auto}` contract (§A.4).
|
|
261
259
|
|
|
262
|
-
Then run the **rail self-diagnostic** (
|
|
260
|
+
Then run the **rail self-diagnostic** (§E below). Misalignment → discard + regenerate.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## E. Rail self-diagnostic
|
|
265
|
+
|
|
266
|
+
Visual alignment contracts are checkable. After rendering, paste this into the dev-preview 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.**
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
(() => {
|
|
270
|
+
const sels = [
|
|
271
|
+
'.chorus-navigation-bar', '.chorus-tab-bar', '.chorus-tabs',
|
|
272
|
+
'.chorus-carousel', '.chorus-feed', '.chorus-feed-ad',
|
|
273
|
+
'.chorus-list', '.chorus-suggestion-list', '.chorus-directory-list',
|
|
274
|
+
'.chorus-nav-list', '.chorus-avatar-rail',
|
|
275
|
+
];
|
|
276
|
+
const rows = sels.flatMap(sel =>
|
|
277
|
+
[...document.querySelectorAll(sel)].map(el => {
|
|
278
|
+
const r = el.getBoundingClientRect();
|
|
279
|
+
return { sel, left: Math.round(r.left), right: Math.round(window.innerWidth - r.right) };
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
if (!rows.length) { console.log('No full-bleed children on this page.'); return; }
|
|
283
|
+
const L = new Set(rows.map(r => r.left)), R = new Set(rows.map(r => r.right));
|
|
284
|
+
console.table(rows);
|
|
285
|
+
if (L.size > 1 || R.size > 1) {
|
|
286
|
+
console.error(`❌ Rail misalignment — left: [${[...L].join(', ')}], right: [${[...R].join(', ')}]. Every full-bleed child should share one rail. Fix per § Layout-Type & Padding Contract.`);
|
|
287
|
+
} else {
|
|
288
|
+
console.log(`✅ Rail aligned — left=${[...L][0]}px, right=${[...R][0]}px.`);
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
```
|
|
263
292
|
|
|
264
293
|
---
|
|
265
294
|
|
package/agents/catalog.md
CHANGED
|
@@ -172,11 +172,11 @@ Each row resolves to a typed React component — `<FormField variant="search" pl
|
|
|
172
172
|
|
|
173
173
|
**Disambiguate**: `spinner` = rotating arc for a *brief, indeterminate* wait (under ~1s, no measurable ratio) — a button submit, an inline action, a first-paint loader. Reserve one per view. `skeleton` = *in-flight* tonal block previewing where content will land. For loading data the host would otherwise paint as empty. NOT for empty states (no data yet) — those use illustration + body copy. `empty-state` = the durable *no-data* surface — a centered illustration + headline + body + one primary CTA, painted where the real content would go (an empty feed, a search with no results, a fresh inbox); reach for it instead of leaving a no-data surface blank. `progress` = slim track for a *long-running, identifiable* task with a known ratio (upload, onboarding step, background sync). Pick by what you know: nothing measurable → `spinner`; the content's shape → `skeleton`; no data yet → `empty-state`; a ratio → `progress`.
|
|
174
174
|
|
|
175
|
-
## Shadcn
|
|
175
|
+
## Shadcn name translation
|
|
176
176
|
|
|
177
177
|
When an AI agent (or designer paging in from shadcn) reaches for a shadcn-named component, this is the Chorus surface to render. Direct React `export` aliases live in `packages/ui/src/index.js` where the name doesn't already collide (`Sheet`, `Drawer`, `Alert`, `Avatar`, `AppBar`, `BottomNav`); the table below covers the rest — including where Chorus splits one shadcn shape into multiple intent-bound surfaces.
|
|
178
178
|
|
|
179
|
-
| Shadcn
|
|
179
|
+
| Shadcn name | Chorus surface to render | Notes |
|
|
180
180
|
|-----------------------------------|------------------------------------------------------------------------------------|-------|
|
|
181
181
|
| `<Alert variant="default">` | `<Banner appearance="default">` | Already aliased — `import { Alert } from '@teamblind-chorus/ui'`. |
|
|
182
182
|
| `<Alert variant="destructive">` | `<Banner appearance="destructive">` | Use the destructive appearance. |
|
|
@@ -205,7 +205,7 @@ When an AI agent (or designer paging in from shadcn) reaches for a shadcn-named
|
|
|
205
205
|
|
|
206
206
|
These shadcn primitives have no Chorus equivalent and no on-pattern mobile substitution. When a brief demands one, **stop and flag a "Chorus gap"** — do NOT improvise a wrapper, re-introduce shadcn, or hardcode raw values. Maintainers add the missing family; agents wait or work around.
|
|
207
207
|
|
|
208
|
-
| Shadcn
|
|
208
|
+
| Shadcn name | Mobile use case (when it WOULD be needed) | Status |
|
|
209
209
|
|-----------------------|--------------------------------------------|--------|
|
|
210
210
|
| `<Calendar>` (date picker) | Birthday, schedule, calendar event picker. | **Gap**. Workaround: pair `<FormField variant="select">` with native `<input type="date">` inside a `<BottomSheet>`; flag the gap. |
|
|
211
211
|
| `<Chart>` (data viz) | Analytics, finance, fitness charts. | **Gap**. Workaround: external `recharts`/`chart.js`, but every color/typography MUST resolve through Chorus tokens (`var(--sys-*)`); flag the gap. |
|
|
@@ -216,7 +216,7 @@ These shadcn primitives have no Chorus equivalent and no on-pattern mobile subst
|
|
|
216
216
|
|
|
217
217
|
These shadcn primitives are desktop-first or web-OS conventions Chorus deliberately omits because the mobile equivalent is a different shape. Agents MUST substitute the listed pattern instead of re-introducing the shadcn primitive.
|
|
218
218
|
|
|
219
|
-
| Shadcn
|
|
219
|
+
| Shadcn name | Out of scope because | Mobile substitute |
|
|
220
220
|
|-----------------------|----------------------|--------------------|
|
|
221
221
|
| `<Breadcrumb>` | Mobile is deep-link / drill-in; no horizontal trail. | `<NavigationBar variant="sub">` (back + title). |
|
|
222
222
|
| `<Menubar>` | Persistent app menubar is desktop chrome. | `<NavigationBar>` for top chrome + `<BottomSheet>` for action overflow. |
|
|
@@ -29,9 +29,7 @@ import { AvatarRail } from '@teamblind-chorus/ui';
|
|
|
29
29
|
/>
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
### With overflow
|
|
32
|
+
## Overflow
|
|
35
33
|
|
|
36
34
|
When the rail carries more items than the container fits, it scrolls horizontally.
|
|
37
35
|
|
|
@@ -90,7 +88,7 @@ Container has no interactive state. Each item is a text-link affordance obeying
|
|
|
90
88
|
| `pressed` | `sys.state.pressed` | Underline persists; pressed overlay tints. |
|
|
91
89
|
| `disabled` | overlay suppressed | Item at `sys.state.disabled` opacity; underline suppressed. |
|
|
92
90
|
|
|
93
|
-
The trailing action is a `small` Text Button (rendered as `<a>` when `href` is set) — Text Button hover overlay + standard
|
|
91
|
+
The trailing action is a `small` Text Button (rendered as `<a>` when `href` is set) — Text Button hover overlay + standard single focus ring. Mirrors the Channel List header action; the only difference is the rung.
|
|
94
92
|
|
|
95
93
|
## Focus indicator
|
|
96
94
|
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"sizing": {
|
|
69
|
-
"containerFill": "sys.color.surface",
|
|
69
|
+
"containerFill": "sys.color.surface.default",
|
|
70
70
|
"containerPaddingBlock": "sys.layout.container.sm",
|
|
71
71
|
"containerPaddingInline": "sys.layout.container.md",
|
|
72
72
|
"containerActionGap": "sys.layout.inline.xl",
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
"itemAvatarLabelGap": "sys.layout.stack.xs",
|
|
76
76
|
"avatarSize": 48,
|
|
77
77
|
"labelTypo": "sys.typo.label.sm",
|
|
78
|
-
"labelColor": "sys.color.
|
|
78
|
+
"labelColor": "sys.color.text.default",
|
|
79
79
|
"labelMaxWidth": "ref.space.1000",
|
|
80
|
-
"trailingActionRendersAs": "Button variant='text' size='small' appearance='accent' — label paints in sys.color.primary via the Text Button accent token.",
|
|
80
|
+
"trailingActionRendersAs": "Button variant='text' size='small' appearance='accent' — label paints in sys.color.background.primary via the Text Button accent token.",
|
|
81
81
|
"trailingActionTypo": "sys.typo.label.md",
|
|
82
|
-
"trailingActionColor": "sys.color.
|
|
82
|
+
"trailingActionColor": "sys.color.text.link"
|
|
83
83
|
},
|
|
84
84
|
"itemProps": {
|
|
85
85
|
"value": {
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"states": {
|
|
104
104
|
"default": {
|
|
105
105
|
"decoration": "none",
|
|
106
|
-
"label": "sys.color.
|
|
106
|
+
"label": "sys.color.text.default"
|
|
107
107
|
},
|
|
108
108
|
"hovered": {
|
|
109
109
|
"description": "1px same-color underline under the label. Color does not change — the underline is the affordance.",
|
|
@@ -129,11 +129,11 @@
|
|
|
129
129
|
"layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
|
|
130
130
|
"innerCounterRing": {
|
|
131
131
|
"width": "sys.borderWidth.hairline",
|
|
132
|
-
"color": "sys.color.
|
|
132
|
+
"color": "sys.color.border.focused"
|
|
133
133
|
},
|
|
134
134
|
"outerRing": {
|
|
135
135
|
"width": "sys.borderWidth.thin",
|
|
136
|
-
"color": "sys.color.
|
|
136
|
+
"color": "sys.color.border.focused"
|
|
137
137
|
}
|
|
138
138
|
},
|
|
139
139
|
"note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the item is in; never via plain mouse click."
|
|
@@ -153,12 +153,8 @@
|
|
|
153
153
|
"opacity": "sys.state.focus"
|
|
154
154
|
},
|
|
155
155
|
"ring": {
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"outerLayerPosition": "depth 0..2px from the item edge (the outer stroke)",
|
|
159
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
160
|
-
"insetColor": "sys.color.focusInset",
|
|
161
|
-
"insetLayerPosition": "depth 2..3px from the item edge (the counter-ring just inside the outer stroke)",
|
|
156
|
+
"width": "sys.borderWidth.hairline",
|
|
157
|
+
"color": "sys.color.border.focused",
|
|
162
158
|
"implementation": "inset box-shadow on the item's `::after` overlay. Constrained strictly inside the item's footprint and never exceeds it."
|
|
163
159
|
},
|
|
164
160
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
@@ -171,7 +167,7 @@
|
|
|
171
167
|
"trailingActionFloats": "The optional trailingAction stays at the end of the rail content; when the rail overflows, scrolling reveals it."
|
|
172
168
|
},
|
|
173
169
|
"forbidden": [
|
|
174
|
-
"rail item rendered with sys.color.brand as the fill — avatar-rail items inherit the underlying Thumbnail family contract",
|
|
170
|
+
"rail item rendered with sys.color.text.brand as the fill — avatar-rail items inherit the underlying Thumbnail family contract",
|
|
175
171
|
"rail wrapped in a horizontal-padding div — avatar-rail is full-bleed by family declaration",
|
|
176
172
|
"label below 12px — rail labels use sys.typo.label.sm (12px), never finer",
|
|
177
173
|
"rail items reflowing — the rail snap-scrolls horizontally with a fixed item width"
|
|
@@ -20,11 +20,9 @@ import { Badge } from '@teamblind-chorus/ui';
|
|
|
20
20
|
<Badge variant="role">Verified</Badge>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Inverse
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`appearance="inverse"` swaps the fill to the high-contrast inverse pair (`sys.color.inverseSurface` / `sys.color.inverseOnSurface` — theme-aware, flips in dark mode). Reserved for **PRO**, the mark on paid professional users; never reach for it as a styling option on ordinary roles. Same geometry as default — only the fill pair changes.
|
|
25
|
+
`appearance="inverse"` swaps the fill to the high-contrast inverse pair (`sys.color.background.inverse` / `sys.color.text.inverse` — theme-aware, flips in dark mode). Reserved for **PRO**, the mark on paid professional users; never reach for it as a styling option on ordinary roles. Same geometry as default — only the fill pair changes.
|
|
28
26
|
|
|
29
27
|
```preview
|
|
30
28
|
badge/role/inverse
|
|
@@ -34,7 +32,7 @@ import { Badge } from '@teamblind-chorus/ui';
|
|
|
34
32
|
<Badge variant="role" appearance="inverse">PRO</Badge>
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
## Canonical labels
|
|
38
36
|
|
|
39
37
|
The product-canonical roles — **Channel owner** (채널장), **Verified** (현직자, the verified professional), and **PRO** (the paid professional, always on the inverse appearance). Labels display in English; any short role / title fits the same pill. Keep it to 1–2 words and one badge per person.
|
|
40
38
|
|
|
@@ -50,7 +48,7 @@ import { Badge } from '@teamblind-chorus/ui';
|
|
|
50
48
|
</div>
|
|
51
49
|
```
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
## Metadata host
|
|
54
52
|
|
|
55
53
|
The canonical host — the trailing **nickname** item of [Metadata](../metadata/metadata.md)'s meta row (second line; the nickname displays bare, no @ prefix). Pass the pill through the meta item's `badge` field so it renders after the nickname link, *outside* the `<a>` (4px `inline.sm` gap — never link content). The badge annotates the person the way the timestamp annotates the post. **Exactly one badge rides the nickname** — when a user qualifies for several roles, pick the contextually dominant one; never stack.
|
|
56
54
|
|
|
@@ -78,8 +76,8 @@ Two appearances on a single emphasis axis — both theme-aware, no separate dark
|
|
|
78
76
|
|
|
79
77
|
| Appearance | Background | Label | Reserved for |
|
|
80
78
|
|------------|------------|-------|--------------|
|
|
81
|
-
| `default` | `sys.color.
|
|
82
|
-
| `inverse` | `sys.color.
|
|
79
|
+
| `default` | `sys.color.background.selected` | `sys.color.text.link` | Ordinary role marks — Channel owner, Verified |
|
|
80
|
+
| `inverse` | `sys.color.background.inverse` | `sys.color.text.inverse` | The **PRO** paid-expert mark only |
|
|
83
81
|
|
|
84
82
|
Never the brand pair — brand on a badge is the [Update](./update.md) activity marker, and a brand-filled role mark would read as an alert.
|
|
85
83
|
|
|
@@ -93,7 +91,7 @@ Single rung.
|
|
|
93
91
|
|
|
94
92
|
| Size | Min-height / width | Padding (block × inline) | Label |
|
|
95
93
|
|--------|------------------------|---------------------------------------|--------------------------------------|
|
|
96
|
-
| medium | 16px (`ref.space.200`) | 2 × 6 (`ref.space.25` × `ref.space.75`) | 10 / Semibold (`sys.typo.
|
|
94
|
+
| medium | 16px (`ref.space.200`) | 2 × 6 (`ref.space.25` × `ref.space.75`) | 10 / Semibold (`sys.typo.label.xs`) |
|
|
97
95
|
|
|
98
96
|
`ref.space.25` (2px) and `ref.space.75` (6px) bind raw because `sys.*` exposes no steps there — in lockstep with [Update](./update.md)'s rungs.
|
|
99
97
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Badge",
|
|
4
4
|
"family": "badge",
|
|
5
5
|
"subcomponent": "role",
|
|
6
|
-
"description": "Role badge — a tonal primary-container pill naming a user's role or title, riding the bare nickname (no @ prefix) at the end of the user-metadata meta row. Labels display in English; canonical: 'Channel owner' (채널장), 'Verified' (현직자, the verified professional). Two appearances: `default` (pale `sys.color.
|
|
6
|
+
"description": "Role badge — a tonal primary-container pill naming a user's role or title, riding the bare nickname (no @ prefix) at the end of the user-metadata meta row. Labels display in English; canonical: 'Channel owner' (채널장), 'Verified' (현직자, the verified professional). Two appearances: `default` (pale `sys.color.background.selected` fill with `sys.color.text.link` label) and `inverse` (`sys.color.background.inverse` / `sys.color.text.inverse` — reserved for the PRO mark on paid professional users). Shared geometry, 10px (`caption`) text, 16-rung min-height, `radius.full` corner. Identity, not state: it says who the person *is* — for workflow state (pending / rejected / draft) reach for StatusTag instead. Presentational; never interactive. Reaches the meta row through Metadata's meta-item `badge` field so the pill renders outside the nickname's <a>.",
|
|
7
7
|
"element": "span",
|
|
8
8
|
"props": {
|
|
9
9
|
"variant": {
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
},
|
|
36
36
|
"appearances": {
|
|
37
37
|
"default": {
|
|
38
|
-
"background": "sys.color.
|
|
39
|
-
"label": "sys.color.
|
|
38
|
+
"background": "sys.color.background.selected",
|
|
39
|
+
"label": "sys.color.text.link",
|
|
40
40
|
"radius": "sys.radius.full",
|
|
41
41
|
"default": true,
|
|
42
42
|
"note": "Tonal informational pair — pale primary container fill with the deep primary-container label. Theme-aware sys tokens, so no separate dark binding. Never the brand pair: brand is reserved for the Update sub's activity marker, and a brand-filled role mark would read as an alert, not an identity."
|
|
43
43
|
},
|
|
44
44
|
"inverse": {
|
|
45
|
-
"background": "sys.color.
|
|
46
|
-
"label": "sys.color.
|
|
45
|
+
"background": "sys.color.background.inverse",
|
|
46
|
+
"label": "sys.color.text.inverse",
|
|
47
47
|
"radius": "sys.radius.full",
|
|
48
48
|
"note": "Inverse pair — the strongest mark in the row: near-black pill with light label (theme-aware; flips in dark mode). Reserved for the PRO mark on paid professional users. Same geometry as default; only the fill pair changes."
|
|
49
49
|
}
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"minWidth": "ref.space.200",
|
|
55
55
|
"paddingBlock": "ref.space.25",
|
|
56
56
|
"paddingInline": "ref.space.75",
|
|
57
|
-
"labelTypo": "sys.typo.
|
|
57
|
+
"labelTypo": "sys.typo.label.xs",
|
|
58
58
|
"labelLineHeight": "1.2",
|
|
59
59
|
"note": "Single 16-rung (`ref.space.200`) — sized to ride inline beside `caption`/`label` metadata text without stretching the row. 2 × 6 padding (`ref.space.25` × `ref.space.75`); the reference steps bind raw where `sys.*` exposes no step, in lockstep with the Update sub's rungs. Label line-height is the structural `1.2` (same device as StatusTag): 10px × 1.2 + 2 × 2px padding = exactly 16px — `caption`'s 15px running-text line would push the pill to 19."
|
|
60
60
|
}
|
|
@@ -32,9 +32,7 @@ import { Badge } from '@teamblind-chorus/ui';
|
|
|
32
32
|
<Badge size="dot-md" />
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
### Digit cases
|
|
35
|
+
## Digit cases
|
|
38
36
|
|
|
39
37
|
Single digit collapses to a circle (`min-width = min-height`); two digits stretch via `padding-inline`; counts past 99 cap at `99+`.
|
|
40
38
|
|
|
@@ -50,7 +48,7 @@ import { Badge } from '@teamblind-chorus/ui';
|
|
|
50
48
|
</div>
|
|
51
49
|
```
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
## Thumbnail dot
|
|
54
52
|
|
|
55
53
|
The canonical hosted form — Dot at a [Thumbnail](../thumbnail/thumbnail.md)'s top-right. Thumbnail picks `dot-md` at 32 / 40 / 48 and `dot-sm` at 16 / 20 / 24. The dot rides above the image without enlarging the bounding box.
|
|
56
54
|
|
|
@@ -62,7 +60,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
|
|
|
62
60
|
<Thumbnail size={48} src="…" alt="Channel" updateDot />
|
|
63
61
|
```
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
## Icon dot
|
|
66
64
|
|
|
67
65
|
Dot painted at an icon's top-right (notification bell, chat, mention). Always `dot-sm` regardless of icon size — a 6 × 6 dot reads as a highlight against the icon's drawing area. The 2px `surface`-color outline keeps it discrete from the icon stroke; the icon's `icon.md` / `icon.lg` footprint never changes.
|
|
68
66
|
|
|
@@ -78,7 +76,7 @@ import { BellIcon } from '@teamblind-chorus/ui/icons';
|
|
|
78
76
|
</span>
|
|
79
77
|
```
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
## List row host
|
|
82
80
|
|
|
83
81
|
Numeric badge attached inside the label cell of a thumbnail `List` row — the canonical product use. Badge sits flush against the channel name (8px inline gap).
|
|
84
82
|
|
|
@@ -106,7 +104,7 @@ const labelWithBadge = (text, count) => (
|
|
|
106
104
|
|
|
107
105
|
## Appearance
|
|
108
106
|
|
|
109
|
-
Single appearance — the **brand** token pair (`sys.color.brand` background, `sys.color.
|
|
107
|
+
Single appearance — the **brand** token pair (`sys.color.text.brand` background, `sys.color.text.onFill` label). Brand is one tonal step brighter than `error` and reserved for short-label attention pins. Do not reach for `error` or `brandContainer`.
|
|
110
108
|
|
|
111
109
|
## Slots
|
|
112
110
|
|
|
@@ -119,7 +117,7 @@ Four rungs split across the two types — two per type. All rungs share `sys.rad
|
|
|
119
117
|
| Type | Size | Min-height / width | Padding (block × inline) | Label | Halo |
|
|
120
118
|
|----------|---------|--------------------------|------------------------------------------|--------------------------------------|----------------------------|
|
|
121
119
|
| Numeric | medium | 20px (`ref.space.250` ‡) | 0 × 6 (`0` × `ref.space.75` ‡) | 12 / Semibold (`sys.typo.label.sm`) | — |
|
|
122
|
-
| Numeric | small | 16px (`ref.space.200`) | 0 × 4 (`0` × `sys.layout.container.2xs`) | 10 / Semibold (`sys.typo.
|
|
120
|
+
| Numeric | small | 16px (`ref.space.200`) | 0 × 4 (`0` × `sys.layout.container.2xs`) | 10 / Semibold (`sys.typo.label.xs`) | — |
|
|
123
121
|
| Dot | dot-md | 8px (`ref.space.100`) | 0 × 0 | — (labelless) | 2px `sys.color.surface` ⁋ |
|
|
124
122
|
| Dot | dot-sm | 6px (`ref.space.75`) | 0 × 0 | — (labelless) | 2px `sys.color.surface` ⁋ |
|
|
125
123
|
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"appearance": {
|
|
61
|
-
"background": "sys.color.brand",
|
|
62
|
-
"label": "sys.color.
|
|
61
|
+
"background": "sys.color.text.brand",
|
|
62
|
+
"label": "sys.color.text.onFill",
|
|
63
63
|
"radius": "sys.radius.full",
|
|
64
64
|
"dotOutline": {
|
|
65
|
-
"color": "sys.color.surface",
|
|
65
|
+
"color": "sys.color.surface.default",
|
|
66
66
|
"width": "sys.borderWidth.thin",
|
|
67
67
|
"rendering": "box-shadow",
|
|
68
68
|
"note": "Update Dot rungs paint a 2px `surface`-color outline as a `box-shadow` so the dot stays a discrete chip on any host (image, icon, row) without enlarging its bounding box. The outline carves the dot out of whatever sits beside it; without it the brand fill blends into surrounding fills with similar luminance."
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"minWidth": "ref.space.200",
|
|
82
82
|
"paddingBlock": "0",
|
|
83
83
|
"paddingInline": "sys.layout.container.2xs",
|
|
84
|
-
"labelTypo": "sys.typo.
|
|
84
|
+
"labelTypo": "sys.typo.label.xs"
|
|
85
85
|
},
|
|
86
86
|
"dot-md": {
|
|
87
87
|
"minHeight": "ref.space.100",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"overflow": "When the count overflows to '99+', the accessible name should read the intent (e.g. 'over 99 unread'), not the literal glyph."
|
|
107
107
|
},
|
|
108
108
|
"forbidden": [
|
|
109
|
-
"badge painted with sys.color.brand outside the HOT / NEW / unread-count canon — brand on a badge is the marker, not a tint",
|
|
109
|
+
"badge painted with sys.color.text.brand outside the HOT / NEW / unread-count canon — brand on a badge is the marker, not a tint",
|
|
110
110
|
"badge rendered as a raw <span> with Tailwind — badge chrome owns the radius / padding / typography",
|
|
111
111
|
"more than one badge on the same anchor — the host slot is single-badge by anatomy",
|
|
112
112
|
"badge label below 12px (label.sm is the floor)"
|
|
@@ -25,9 +25,7 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
25
25
|
</Banner>
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
### Accent
|
|
28
|
+
## Accent
|
|
31
29
|
|
|
32
30
|
The primary-tinted appearance. Body and action both paint in the primary family — reach for it when the aside should pull more attention.
|
|
33
31
|
|
|
@@ -44,9 +42,9 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
44
42
|
</Banner>
|
|
45
43
|
```
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
## Neutral body
|
|
48
46
|
|
|
49
|
-
The `accent` fill kept, but the copy re-toned to the **Default** appearance's neutral foreground — title + body in `sys.color.
|
|
47
|
+
The `accent` fill kept, but the copy re-toned to the **Default** appearance's neutral foreground — title + body in `sys.color.text.default`, action stepping to `sys.color.background.primary`. Pass `neutralBody`. This decouples the background tone from the text tone: the `primaryContainer` tint still pulls the eye, but the copy reads as quiet, high-legibility body text rather than tonal `onPrimaryContainer`. Reach for it on longer explainers or denser asides where primary-family body copy would tire the reader. No effect on `default` (already `onSurface`) or `destructive` (the warning tone must carry through the copy).
|
|
50
48
|
|
|
51
49
|
```preview
|
|
52
50
|
banner/accent-neutral-body
|
|
@@ -63,7 +61,7 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
63
61
|
</Banner>
|
|
64
62
|
```
|
|
65
63
|
|
|
66
|
-
|
|
64
|
+
## Destructive
|
|
67
65
|
|
|
68
66
|
The error-tinted appearance — `errorContainer` fill with `onErrorContainer` foreground. Reach for it when the aside is a blocking error or rejection (failed approvals, integration outages, billing). Use sparingly.
|
|
69
67
|
|
|
@@ -80,7 +78,7 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
80
78
|
</Banner>
|
|
81
79
|
```
|
|
82
80
|
|
|
83
|
-
|
|
81
|
+
## Thumbnail
|
|
84
82
|
|
|
85
83
|
A leading [Thumbnail](../thumbnail/thumbnail.md) at the top-left — reach for it when the aside is anchored to a channel, author, or sub-brand image. Thumbnail owns its diameter and corner shape; the slot only top-aligns it next to the content column.
|
|
86
84
|
|
|
@@ -98,9 +96,9 @@ import { Banner, Thumbnail } from '@teamblind-chorus/ui';
|
|
|
98
96
|
</Banner>
|
|
99
97
|
```
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
## Outlined
|
|
102
100
|
|
|
103
|
-
An optional `sys.borderWidth.hairline` (1) inset stroke toned to the appearance's color family and kept deliberately faint, so the outline reads as a soft edge of the same tint rather than a frame — the subtle gray hairline (`sys.color.
|
|
101
|
+
An optional `sys.borderWidth.hairline` (1) inset stroke toned to the appearance's color family and kept deliberately faint, so the outline reads as a soft edge of the same tint rather than a frame — the subtle gray hairline (`sys.color.border.default`) on `default`'s gray-tinted scrim, `primary` at 40% on `accent`'s blue-tinted container, `error` at 40% on `destructive`. Painted as an inset box-shadow, never a real border, so toggling it cannot change the banner's footprint. Reach for it when the tinted fill alone doesn't separate the banner from its host surface.
|
|
104
102
|
|
|
105
103
|
```preview
|
|
106
104
|
banner/outlined
|
|
@@ -118,7 +116,7 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
118
116
|
</div>
|
|
119
117
|
```
|
|
120
118
|
|
|
121
|
-
|
|
119
|
+
## Title
|
|
122
120
|
|
|
123
121
|
An optional heading line above the body — `label.md` (14 / Semibold) in the container's foreground, separated from the body by `sys.layout.stack.2xs` (4) so the pair reads as one passage. Reach for it when the aside needs a scannable lead-in; omit it for single-thought asides where the body carries itself.
|
|
124
122
|
|
|
@@ -136,7 +134,7 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
136
134
|
</Banner>
|
|
137
135
|
```
|
|
138
136
|
|
|
139
|
-
|
|
137
|
+
## Trailing icon
|
|
140
138
|
|
|
141
139
|
A 16 × 16 (`sys.icon.md`) glyph at the trailing edge, vertically centred against the whole block and painted in `currentColor`. Reach for it when the banner leads somewhere — a forward affordance such as `ForwardCircleFillIcon` signals the aside opens a destination.
|
|
142
140
|
|
|
@@ -155,7 +153,7 @@ import { ForwardCircleFillIcon } from '@teamblind-chorus/ui/icons';
|
|
|
155
153
|
</Banner>
|
|
156
154
|
```
|
|
157
155
|
|
|
158
|
-
|
|
156
|
+
## Trailing action
|
|
159
157
|
|
|
160
158
|
A [Text Button](../button/text.md) (`<Button variant="text">`) in the trailing slot, vertically centred against the block — a compact inline commit beside the copy (*Dismiss*, *Enable*, *Undo*), distinct from `action` (the follow-through link below the body). The button keeps full control of its own `size` and `appearance` per the button/text spec, but **default the appearance to the banner's colour family** so the commit reads as part of the tinted block — `accent` banner → `appearance="accent"`, `default` banner → `appearance="default"`, `destructive` banner → the Text Button `destructive` flavor. It also keeps the button/text `leadingIcon` / `trailingIcon` slots, so the commit can carry an in-button glyph — e.g. a trailing `ChevronRightIcon` on a *Enable* / *Continue* commit. When both `trailingAction` and the banner-level `trailingIcon` are passed, the action wins the slot.
|
|
161
159
|
|
|
@@ -186,7 +184,7 @@ import { ChevronRightIcon } from '@teamblind-chorus/ui/icons';
|
|
|
186
184
|
</div>
|
|
187
185
|
```
|
|
188
186
|
|
|
189
|
-
|
|
187
|
+
## Icon
|
|
190
188
|
|
|
191
189
|
A 16 × 16 (`sys.icon.md`) glyph at the leading edge, painted in `currentColor`. The slot is sized to the body's first-line height so the glyph centres on the first line — multi-line bodies keep the icon anchored to the first-line cap, not the block centre. Reach for it when the aside leads with a meaning-bearing glyph rather than a brand image.
|
|
192
190
|
|
|
@@ -211,10 +209,10 @@ Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner c
|
|
|
211
209
|
|
|
212
210
|
| Appearance | Container fill | Body / action color | Outline (when `outlined`) | When to use |
|
|
213
211
|
|---------------|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------|----------------------------|------------------------------------------------------------------------------|
|
|
214
|
-
| `default` | `sys.color.
|
|
215
|
-
| `accent` | `sys.color.
|
|
216
|
-
| `accent` + `neutralBody` | `sys.color.
|
|
217
|
-
| `destructive` | `sys.color.
|
|
212
|
+
| `default` | `sys.color.background.neutral` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | body in `sys.color.text.default`, action steps to `sys.color.background.primary` | `sys.color.border.default` (subtle gray) | Supplementary asides the reader can pass over without missing the main flow. |
|
|
213
|
+
| `accent` | `sys.color.background.selected` | body in `onPrimaryContainer`, action inherits | `sys.color.background.primary` at 40% (soft blue) | Asides worth pulling the eye toward — new-feature explainers, capability nudges. |
|
|
214
|
+
| `accent` + `neutralBody` | `sys.color.background.selected` | title + body in `onSurface`, action steps to `sys.color.background.primary` | `sys.color.background.primary` at 40% (soft blue) | Accent tint pulls the eye, but the copy stays quiet, high-legibility body text — longer explainers, dense asides. |
|
|
215
|
+
| `destructive` | `sys.color.background.danger` | body in `onErrorContainer`, action inherits | `sys.color.text.danger` at 40% | Blocking errors or rejections — failed approvals, outages, billing. |
|
|
218
216
|
|
|
219
217
|
## Slots
|
|
220
218
|
|
|
@@ -238,7 +236,7 @@ Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner c
|
|
|
238
236
|
| content | Flex column, `flex: 1 1 auto`, `sys.layout.stack.xs` (8) body↔action gap, `sys.layout.stack.2xs` (4) title↔body gap |
|
|
239
237
|
| title | `sys.typo.label.md` (14 / Semibold 600), color inherits |
|
|
240
238
|
| body | `sys.typo.body.sm` (14 / Regular), color inherits |
|
|
241
|
-
| action | `sys.typo.label.md` (14 / Semibold), underlined. Steps to `sys.color.primary` in `default`; inherits in `accent` / `destructive`. |
|
|
239
|
+
| action | `sys.typo.label.md` (14 / Semibold), underlined. Steps to `sys.color.background.primary` in `default`; inherits in `accent` / `destructive`. |
|
|
242
240
|
| trailingIcon | `sys.icon.md` (16 × 16) glyph, `align-self: center` against the container, `color: currentColor` |
|
|
243
241
|
| trailingAction | [Text Button](../button/text.md) (`<Button variant="text">`), `flex: 0 0 auto`, `align-self: center`. Size + appearance owned by the Button; default appearance to the banner's colour family. Wins the slot over `trailingIcon` |
|
|
244
242
|
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"type": "boolean",
|
|
20
20
|
"optional": true,
|
|
21
21
|
"default": false,
|
|
22
|
-
"description": "Paints a `sys.borderWidth.hairline` (1) inset stroke around the container, toned to the appearance's color family and kept deliberately faint so it reads as a soft edge of the same tint, not a frame — the subtle gray hairline (`sys.color.
|
|
22
|
+
"description": "Paints a `sys.borderWidth.hairline` (1) inset stroke around the container, toned to the appearance's color family and kept deliberately faint so it reads as a soft edge of the same tint, not a frame — the subtle gray hairline (`sys.color.border.default`) on `default`'s gray-tinted fill, `primary` at 40% (`color-mix(sys.color.background.primary, 40%)`) on `accent`'s blue-tinted fill, `error` at 40% on `destructive`. Rendered as an inset box-shadow, never a real border, so toggling it cannot change the banner's footprint (see DESIGN.md → Border & Stroke). Reach for it when the tinted fill alone doesn't separate the banner from its host surface."
|
|
23
23
|
},
|
|
24
24
|
"neutralBody": {
|
|
25
25
|
"type": "boolean",
|
|
26
26
|
"optional": true,
|
|
27
27
|
"default": false,
|
|
28
28
|
"appliesTo": "accent",
|
|
29
|
-
"description": "On `accent`, paints the title + body in the neutral default foreground (`sys.color.
|
|
29
|
+
"description": "On `accent`, paints the title + body in the neutral default foreground (`sys.color.text.default`) and steps the action to `sys.color.background.primary` — i.e. the **Default appearance's** foreground treatment laid over the accent fill, decoupling the background tone from the text tone. Reach for it when the `primaryContainer` tint should still pull the eye but the copy should read as quiet, high-legibility body text rather than tonal `onPrimaryContainer` primary-family text (long-form explainers, dense asides). No effect on `default` (already `onSurface`) or `destructive` (the warning tone must carry through the copy)."
|
|
30
30
|
},
|
|
31
31
|
"title": {
|
|
32
32
|
"type": "node",
|
|
@@ -157,24 +157,24 @@
|
|
|
157
157
|
},
|
|
158
158
|
"appearances": {
|
|
159
159
|
"default": {
|
|
160
|
-
"background": "sys.color.
|
|
161
|
-
"foreground": "sys.color.
|
|
162
|
-
"actionColor": "sys.color.
|
|
163
|
-
"outlineColor": "sys.color.
|
|
164
|
-
"note": "Body sits in `onSurface`; the action link steps to primary so it carries the only chromatic emphasis. Background is the translucent inverse-tone scrim (`sys.color.
|
|
160
|
+
"background": "sys.color.background.neutral",
|
|
161
|
+
"foreground": "sys.color.text.default",
|
|
162
|
+
"actionColor": "sys.color.text.link",
|
|
163
|
+
"outlineColor": "sys.color.border.default",
|
|
164
|
+
"note": "Body sits in `onSurface`; the action link steps to primary so it carries the only chromatic emphasis. Background is the translucent inverse-tone scrim (`sys.color.background.neutral` — ~8% black light / ~8% white dark) so the banner stays harmonious on any underlying surface — body, raised card, BottomSheet, Dialog — by tinting one step darker (light mode) or lighter (dark mode) instead of pinning to a fixed neutral step that can collide with the surface ladder. Same scrim used by Chip / Tag default, Progress track, StatusTag neutral, and Skeleton."
|
|
165
165
|
},
|
|
166
166
|
"accent": {
|
|
167
|
-
"background": "sys.color.
|
|
168
|
-
"foreground": "sys.color.
|
|
167
|
+
"background": "sys.color.background.selected",
|
|
168
|
+
"foreground": "sys.color.text.link",
|
|
169
169
|
"actionColor": "inherit",
|
|
170
|
-
"outlineColor": "color-mix(sys.color.primary, 40%)",
|
|
170
|
+
"outlineColor": "color-mix(sys.color.background.primary, 40%)",
|
|
171
171
|
"note": "Both body and action paint in the primary family so the whole banner reads as one highlighted block. Reach for `accent` when the aside should pull more attention — feature explainers, capability nudges. Pass `neutralBody` to keep the accent fill but swap the copy to the Default appearance's neutral foreground (`onSurface` body, `primary` action) when the tint should pull the eye while the text stays quiet, high-legibility body copy."
|
|
172
172
|
},
|
|
173
173
|
"destructive": {
|
|
174
|
-
"background": "sys.color.
|
|
175
|
-
"foreground": "sys.color.
|
|
174
|
+
"background": "sys.color.background.danger",
|
|
175
|
+
"foreground": "sys.color.text.danger",
|
|
176
176
|
"actionColor": "inherit",
|
|
177
|
-
"outlineColor": "color-mix(sys.color.
|
|
177
|
+
"outlineColor": "color-mix(sys.color.text.danger, 40%)",
|
|
178
178
|
"note": "Body and action paint in the error family so the whole banner reads as one warning block. Reach for `destructive` when the aside is a blocking error or rejection — failed approvals, integration outages, billing problems. Use sparingly — every destructive banner on a screen competes with the others for the user's alarm budget."
|
|
179
179
|
}
|
|
180
180
|
},
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"forbidden": [
|
|
188
188
|
"banner trailing-edge commit rendered as a raw <a> / <button> or a filled/outlined Button — the trailing action is button/text, defaulted to the banner's color family",
|
|
189
189
|
"neutralBody applied to default or destructive — it only decouples the accent fill from its foreground; default is already onSurface and destructive must carry the warning tone through the copy",
|
|
190
|
-
"default banner background painted with
|
|
190
|
+
"default banner background painted with a brand-tinted fill — informational banners use sys.color.background.selected; promotional banners use sys.color.surface.sunken",
|
|
191
191
|
"banner thumbnail slot omitted when banner role carries imagery — empty image area is forbidden, fall back to /placeholder.png",
|
|
192
192
|
"banner used for transient confirmations — that role is the `toast` family (locked)",
|
|
193
193
|
"banner CTA rendered as raw <a> / <button> — use button/text inside the action slot",
|