@teamblind-chorus/ui 1.1.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 +10 -8
- package/agents/components/avatar-rail/avatar-rail.md +2 -4
- package/agents/components/avatar-rail/avatar-rail.spec.json +27 -12
- 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.family.json +3 -1
- package/agents/components/banner/banner.md +66 -15
- package/agents/components/banner/banner.spec.json +37 -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 +25 -8
- package/agents/components/button/fab.md +7 -9
- package/agents/components/button/fab.spec.json +27 -10
- package/agents/components/button/group.spec.json +4 -4
- package/agents/components/button/icon.md +21 -23
- package/agents/components/button/icon.spec.json +29 -12
- package/agents/components/button/standard.md +40 -42
- package/agents/components/button/standard.spec.json +37 -20
- package/agents/components/button/text.md +21 -23
- package/agents/components/button/text.spec.json +30 -13
- package/agents/components/button/toggle.md +7 -9
- package/agents/components/button/toggle.spec.json +27 -10
- 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 +34 -11
- package/agents/components/chip/tag.md +22 -24
- package/agents/components/chip/tag.spec.json +36 -13
- 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.family.json +28 -0
- package/agents/components/empty-state/empty-state.md +69 -0
- package/agents/components/empty-state/empty-state.spec.json +87 -0
- 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 +39 -31
- package/agents/components/form-field/search.md +2 -4
- package/agents/components/form-field/search.spec.json +24 -16
- package/agents/components/form-field/select.md +18 -20
- package/agents/components/form-field/select.spec.json +36 -27
- package/agents/components/form-field/textarea.md +3 -5
- package/agents/components/form-field/textarea.spec.json +37 -29
- 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 +26 -17
- package/agents/components/list/entry.md +59 -81
- package/agents/components/list/entry.spec.json +37 -21
- package/agents/components/list/list.md +2 -2
- package/agents/components/list/radio.md +13 -20
- package/agents/components/list/radio.spec.json +33 -18
- package/agents/components/list/standard.md +88 -64
- package/agents/components/list/standard.spec.json +52 -20
- 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 +25 -16
- 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/page-shell/page-shell.family.json +1 -1
- package/agents/components/page-shell/page-shell.md +33 -0
- package/agents/components/page-shell/page-shell.spec.json +85 -0
- 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 +27 -0
- package/agents/components/spinner/spinner.md +96 -0
- package/agents/components/spinner/spinner.spec.json +82 -0
- 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 +23 -15
- package/agents/components/tab-bar/tab-bar.md +9 -11
- package/agents/components/tab-bar/tab-bar.spec.json +37 -23
- package/agents/components/tabs/rounded.md +6 -8
- package/agents/components/tabs/rounded.spec.json +34 -13
- 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 +31 -14
- 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/manifest.json +8 -6
- package/agents/tokens.usage.json +71 -226
- package/agents/usage.json +12 -0
- package/dist/index.cjs +531 -262
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -13
- package/dist/index.d.ts +57 -13
- package/dist/index.js +530 -263
- package/dist/index.js.map +1 -1
- package/dist/styles.css +560 -379
- 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
|
@@ -10,7 +10,7 @@ Reverse index from natural-language intent to family + sub-component. Read *befo
|
|
|
10
10
|
|
|
11
11
|
Each family declares `visualReuse: "open" | "locked"` in its `<family>.family.json`. The catalog respects that flag.
|
|
12
12
|
|
|
13
|
-
- **Open (default,
|
|
13
|
+
- **Open (default, 30 families)** — `avatar-rail`, `badge`, `banner`, `bubble`, `button`, `carousel`, `chip`, `directory-list`, `divider`, `empty-state`, `feed`, `header`, `list`, `metadata`, `nav-card`, `nav-list`, `navigation-bar`, `page-shell`, `pagination`, `profile-header`, `progress`, `side-sheet`, `skeleton`, `spinner`, `status-tag`, `suggestion-list`, `switch`, `tab-bar`, `tabs`, `thumbnail`. The intent table is the *first* suggestion, but the agent MAY reach for these on visual-fit grounds even when the brief's intent does not match the row verbatim — e.g. `<Feed>` as a generic article-card surface, `<Banner>` as a tonal aside outside a literal "notice", `<Carousel>` as any labelled editorial block. Anatomy invariants (slot grammar, token bindings, intrinsic geometry) still apply.
|
|
14
14
|
- **Locked (5 families)** — `dialog`, `bottom-sheet`, `toast`, `tooltip`, `form-field`. MUST only be used when the brief's intent matches a row. Their contract IS the interaction — focus trap, auto-dismiss, ARIA live region, form semantics, hover/focus trigger. Borrowing the visual shape without the role breaks behavior. Marked *(locked)* below.
|
|
15
15
|
|
|
16
16
|
When in doubt: open families are recipes, locked families are rules.
|
|
@@ -71,13 +71,13 @@ When in doubt: open families are recipes, locked families are rules.
|
|
|
71
71
|
| avatar-anchored rows (channels, DMs) | `list / entry` | `full-bleed` |
|
|
72
72
|
| drill-in rows with trailing chevron | `list / standard` (or `/ entry`, `/ radio`) with `nav: true` | `full-bleed` |
|
|
73
73
|
| standalone drill-in card (single row) | `nav-card` | **`inline`** |
|
|
74
|
-
| expandable titled sections (FAQ, T&C) | `accordion`
|
|
74
|
+
| expandable titled sections (FAQ, T&C) | `list / accordion` | `full-bleed` |
|
|
75
75
|
| authored content stream (posts, comments) | `feed / feed` | `full-bleed` |
|
|
76
76
|
| follow suggestions block (swipeable peek) | `suggestion-list` | `full-bleed` |
|
|
77
77
|
| follow directory (full vertical scroll) | `directory-list` | `full-bleed` |
|
|
78
78
|
| label-only nav list with chevron rows | `nav-list` | `full-bleed` |
|
|
79
79
|
|
|
80
|
-
**Disambiguate**: `feed` = authored content (author, body, footer). `list` = menus/settings/pickers (stacked rows, hairline divider). `nav-card` = a SINGLE drill-in row as its own bounded outlined card — reach for it when one drill-in needs to read as its own affordance, not one entry in a stack. `accordion` = stacked rows that EXPAND in place rather than drill-in — for short content the user opens to read (FAQ, T&C, expandable filter). `suggestion-list` = labelled swipeable block of follow-suggestions (channels, people, companies, topics — same anatomy). `directory-list` = the same row anatomy at the `large` (48) rung but rendered as a full vertical scroll (no pager) — reach for it when the surface should expose the whole follow-able set. `nav-list` = a labelled vertical list of label-only chevron rows; same wrapper-of-Header + List composition, but for route navigation rather than follow.
|
|
80
|
+
**Disambiguate**: `feed` = authored content (author, body, footer). `list` = menus/settings/pickers (stacked rows, hairline divider). `nav-card` = a SINGLE drill-in row as its own bounded outlined card — reach for it when one drill-in needs to read as its own affordance, not one entry in a stack. `list / accordion` = stacked rows that EXPAND in place rather than drill-in — for short content the user opens to read (FAQ, T&C, expandable filter). `suggestion-list` = labelled swipeable block of follow-suggestions (channels, people, companies, topics — same anatomy). `directory-list` = the same row anatomy at the `large` (48) rung but rendered as a full vertical scroll (no pager) — reach for it when the surface should expose the whole follow-able set. `nav-list` = a labelled vertical list of label-only chevron rows; same wrapper-of-Header + List composition, but for route navigation rather than follow.
|
|
81
81
|
|
|
82
82
|
### Entity directory rows + author attribution
|
|
83
83
|
|
|
@@ -165,16 +165,18 @@ Each row resolves to a typed React component — `<FormField variant="search" pl
|
|
|
165
165
|
|
|
166
166
|
| Intent | Family + sub |
|
|
167
167
|
| ----------------------------------------------------- | --------------------- |
|
|
168
|
+
| indeterminate sub-second load (no known ratio) | `spinner` |
|
|
168
169
|
| in-flight content placeholder (mirrors content shape) | `skeleton` |
|
|
170
|
+
| surface with no data yet (empty feed / search / inbox) | `empty-state` |
|
|
169
171
|
| linear progress for a known long-running task | `progress` |
|
|
170
172
|
|
|
171
|
-
**Disambiguate**: `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. `progress` = slim track for a *long-running, identifiable* task with a known ratio (upload, onboarding step, background sync).
|
|
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`.
|
|
172
174
|
|
|
173
|
-
## Shadcn
|
|
175
|
+
## Shadcn name translation
|
|
174
176
|
|
|
175
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.
|
|
176
178
|
|
|
177
|
-
| Shadcn
|
|
179
|
+
| Shadcn name | Chorus surface to render | Notes |
|
|
178
180
|
|-----------------------------------|------------------------------------------------------------------------------------|-------|
|
|
179
181
|
| `<Alert variant="default">` | `<Banner appearance="default">` | Already aliased — `import { Alert } from '@teamblind-chorus/ui'`. |
|
|
180
182
|
| `<Alert variant="destructive">` | `<Banner appearance="destructive">` | Use the destructive appearance. |
|
|
@@ -203,7 +205,7 @@ When an AI agent (or designer paging in from shadcn) reaches for a shadcn-named
|
|
|
203
205
|
|
|
204
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.
|
|
205
207
|
|
|
206
|
-
| Shadcn
|
|
208
|
+
| Shadcn name | Mobile use case (when it WOULD be needed) | Status |
|
|
207
209
|
|-----------------------|--------------------------------------------|--------|
|
|
208
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. |
|
|
209
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. |
|
|
@@ -214,7 +216,7 @@ These shadcn primitives have no Chorus equivalent and no on-pattern mobile subst
|
|
|
214
216
|
|
|
215
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.
|
|
216
218
|
|
|
217
|
-
| Shadcn
|
|
219
|
+
| Shadcn name | Out of scope because | Mobile substitute |
|
|
218
220
|
|-----------------------|----------------------|--------------------|
|
|
219
221
|
| `<Breadcrumb>` | Mobile is deep-link / drill-in; no horizontal trail. | `<NavigationBar variant="sub">` (back + title). |
|
|
220
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.",
|
|
@@ -119,6 +119,25 @@
|
|
|
119
119
|
},
|
|
120
120
|
"note": "Underline persists."
|
|
121
121
|
},
|
|
122
|
+
"focused": {
|
|
123
|
+
"overlay": {
|
|
124
|
+
"color": "label",
|
|
125
|
+
"opacity": "sys.state.focus"
|
|
126
|
+
},
|
|
127
|
+
"focusRing": {
|
|
128
|
+
"composition": "inward",
|
|
129
|
+
"layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
|
|
130
|
+
"innerCounterRing": {
|
|
131
|
+
"width": "sys.borderWidth.hairline",
|
|
132
|
+
"color": "sys.color.border.focused"
|
|
133
|
+
},
|
|
134
|
+
"outerRing": {
|
|
135
|
+
"width": "sys.borderWidth.thin",
|
|
136
|
+
"color": "sys.color.border.focused"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
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."
|
|
140
|
+
},
|
|
122
141
|
"disabled": {
|
|
123
142
|
"containerOpacity": "sys.state.disabled",
|
|
124
143
|
"suppressUnderline": true,
|
|
@@ -134,12 +153,8 @@
|
|
|
134
153
|
"opacity": "sys.state.focus"
|
|
135
154
|
},
|
|
136
155
|
"ring": {
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"outerLayerPosition": "depth 0..2px from the item edge (the outer stroke)",
|
|
140
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
141
|
-
"insetColor": "sys.color.focusInset",
|
|
142
|
-
"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",
|
|
143
158
|
"implementation": "inset box-shadow on the item's `::after` overlay. Constrained strictly inside the item's footprint and never exceeds it."
|
|
144
159
|
},
|
|
145
160
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
@@ -152,7 +167,7 @@
|
|
|
152
167
|
"trailingActionFloats": "The optional trailingAction stays at the end of the rail content; when the rail overflows, scrolling reveals it."
|
|
153
168
|
},
|
|
154
169
|
"forbidden": [
|
|
155
|
-
"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",
|
|
156
171
|
"rail wrapped in a horizontal-padding div — avatar-rail is full-bleed by family declaration",
|
|
157
172
|
"label below 12px — rail labels use sys.typo.label.sm (12px), never finer",
|
|
158
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)"
|