@teamblind-chorus/ui 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/agents/AGENTS.md +4 -6
  2. package/agents/DESIGN.md +2 -0
  3. package/agents/LOVABLE.md +167 -373
  4. package/agents/anti-patterns.md +2 -2
  5. package/agents/catalog.md +12 -6
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -0
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +19 -0
  8. package/agents/components/badge/badge.md +2 -0
  9. package/agents/components/badge/role.md +2 -0
  10. package/agents/components/badge/update.md +2 -0
  11. package/agents/components/banner/banner.family.json +3 -1
  12. package/agents/components/banner/banner.md +125 -9
  13. package/agents/components/banner/banner.spec.json +64 -3
  14. package/agents/components/bottom-sheet/bottom-sheet.md +2 -0
  15. package/agents/components/bubble/bubble.md +2 -0
  16. package/agents/components/button/button.family.json +8 -2
  17. package/agents/components/button/button.md +2 -0
  18. package/agents/components/button/check.md +2 -0
  19. package/agents/components/button/check.spec.json +19 -0
  20. package/agents/components/button/fab.md +2 -0
  21. package/agents/components/button/fab.spec.json +19 -0
  22. package/agents/components/button/group.spec.json +65 -0
  23. package/agents/components/button/icon.md +2 -0
  24. package/agents/components/button/icon.spec.json +19 -0
  25. package/agents/components/button/standard.md +45 -19
  26. package/agents/components/button/standard.spec.json +19 -0
  27. package/agents/components/button/text.md +2 -0
  28. package/agents/components/button/text.spec.json +19 -0
  29. package/agents/components/button/toggle.md +2 -0
  30. package/agents/components/button/toggle.spec.json +19 -0
  31. package/agents/components/button/toolbar.md +2 -0
  32. package/agents/components/carousel/carousel.md +2 -0
  33. package/agents/components/carousel/post.md +5 -3
  34. package/agents/components/carousel/post.spec.json +4 -6
  35. package/agents/components/carousel/profile.md +4 -2
  36. package/agents/components/carousel/profile.spec.json +4 -6
  37. package/agents/components/chip/chip.md +2 -0
  38. package/agents/components/chip/filter.md +2 -0
  39. package/agents/components/chip/filter.spec.json +19 -0
  40. package/agents/components/chip/tag.md +2 -0
  41. package/agents/components/chip/tag.spec.json +19 -0
  42. package/agents/components/dialog/dialog.md +2 -0
  43. package/agents/components/directory-list/directory-list.md +2 -0
  44. package/agents/components/divider/divider.md +2 -0
  45. package/agents/components/empty-state/empty-state.family.json +28 -0
  46. package/agents/components/empty-state/empty-state.md +69 -0
  47. package/agents/components/empty-state/empty-state.spec.json +87 -0
  48. package/agents/components/feed/ad.md +2 -0
  49. package/agents/components/feed/feed.md +2 -0
  50. package/agents/components/feed/post.md +2 -0
  51. package/agents/components/form-field/form-field.md +3 -1
  52. package/agents/components/form-field/input.md +2 -0
  53. package/agents/components/form-field/input.spec.json +10 -2
  54. package/agents/components/form-field/search.md +2 -0
  55. package/agents/components/form-field/search.spec.json +10 -2
  56. package/agents/components/form-field/select.md +2 -0
  57. package/agents/components/form-field/select.spec.json +9 -1
  58. package/agents/components/form-field/textarea.md +2 -0
  59. package/agents/components/form-field/textarea.spec.json +10 -2
  60. package/agents/components/header/header.md +2 -0
  61. package/agents/components/header/main.md +2 -0
  62. package/agents/components/header/sub.md +2 -0
  63. package/agents/components/list/accordion.md +2 -0
  64. package/agents/components/list/accordion.spec.json +9 -0
  65. package/agents/components/list/entry.md +2 -0
  66. package/agents/components/list/entry.spec.json +21 -1
  67. package/agents/components/list/list.md +3 -1
  68. package/agents/components/list/radio.md +2 -0
  69. package/agents/components/list/radio.spec.json +19 -0
  70. package/agents/components/list/standard.md +48 -0
  71. package/agents/components/list/standard.spec.json +39 -3
  72. package/agents/components/metadata/compact.md +13 -7
  73. package/agents/components/metadata/compact.spec.json +19 -6
  74. package/agents/components/metadata/metadata.family.json +3 -3
  75. package/agents/components/metadata/metadata.md +4 -2
  76. package/agents/components/metadata/standard.md +24 -0
  77. package/agents/components/nav-card/nav-card.md +2 -0
  78. package/agents/components/nav-card/nav-card.spec.json +9 -0
  79. package/agents/components/nav-list/nav-list.md +2 -0
  80. package/agents/components/navigation-bar/main.md +2 -0
  81. package/agents/components/navigation-bar/navigation-bar.md +2 -0
  82. package/agents/components/navigation-bar/search.md +2 -0
  83. package/agents/components/navigation-bar/sub.md +2 -0
  84. package/agents/components/page-shell/page-shell.family.json +1 -1
  85. package/agents/components/page-shell/page-shell.md +35 -0
  86. package/agents/components/page-shell/page-shell.spec.json +85 -0
  87. package/agents/components/pagination/pagination.family.json +26 -0
  88. package/agents/components/pagination/pagination.md +40 -0
  89. package/agents/components/pagination/pagination.spec.json +54 -0
  90. package/agents/components/profile-header/profile-header.md +2 -0
  91. package/agents/components/progress/progress.md +2 -0
  92. package/agents/components/side-sheet/side-sheet.md +2 -0
  93. package/agents/components/skeleton/skeleton.md +2 -0
  94. package/agents/components/spinner/spinner.family.json +27 -0
  95. package/agents/components/spinner/spinner.md +98 -0
  96. package/agents/components/spinner/spinner.spec.json +82 -0
  97. package/agents/components/status-tag/status-tag.md +2 -0
  98. package/agents/components/suggestion-list/suggestion-list.md +2 -0
  99. package/agents/components/switch/switch.md +2 -0
  100. package/agents/components/switch/switch.spec.json +9 -0
  101. package/agents/components/tab-bar/tab-bar.md +2 -0
  102. package/agents/components/tab-bar/tab-bar.spec.json +16 -0
  103. package/agents/components/tabs/rounded.md +2 -0
  104. package/agents/components/tabs/rounded.spec.json +19 -0
  105. package/agents/components/tabs/segmented.md +2 -0
  106. package/agents/components/tabs/tabs.md +2 -0
  107. package/agents/components/tabs/underline.md +2 -0
  108. package/agents/components/tabs/underline.spec.json +19 -0
  109. package/agents/components/thumbnail/thumbnail.md +2 -0
  110. package/agents/components/toast/toast.md +2 -0
  111. package/agents/components/tooltip/tooltip.md +2 -0
  112. package/agents/compose.md +3 -3
  113. package/agents/manifest.json +9 -6
  114. package/agents/patterns/README.md +2 -0
  115. package/agents/patterns/actions.md +2 -0
  116. package/agents/patterns/browsing.md +2 -0
  117. package/agents/patterns/communications.md +2 -0
  118. package/agents/patterns/layout.md +2 -0
  119. package/agents/patterns/modals.md +2 -0
  120. package/agents/patterns/visual.md +2 -0
  121. package/agents/usage.json +27 -3
  122. package/dist/index.cjs +433 -97
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.d.cts +74 -3
  125. package/dist/index.d.ts +74 -3
  126. package/dist/index.js +430 -98
  127. package/dist/index.js.map +1 -1
  128. package/dist/styles.css +365 -41
  129. package/package.json +1 -2
  130. package/agents/reconstruct.md +0 -55
  131. package/agents/scoped-adoption.md +0 -111
@@ -1,6 +1,6 @@
1
1
  # anti-patterns.md — common Lovable / agent failure modes
2
2
 
3
- A catalogue of the wrong-shaped output Chorus consumers most often produce, paired with the right-shaped fix. Each entry names the rule, the broken snippet, and the corrected one. **Read at least once before composing a new screen** — most agent violations come from these ~16 recurring shapes. Pair with [`compose.md`](compose.md) and [`tokens.usage.json`](tokens.usage.json).
3
+ A catalogue of the wrong-shaped output Chorus consumers most often produce, paired with the right-shaped fix. Each entry names the rule, the broken snippet, and the corrected one. **Read at least once before composing a new screen** — most agent violations come from these 18 recurring shapes. Pair with [`compose.md`](compose.md) and [`tokens.usage.json`](tokens.usage.json).
4
4
 
5
5
  When in doubt: if your output matches a "❌ wrong" snippet below, discard and regenerate.
6
6
 
@@ -383,7 +383,7 @@ The cognitive trap: *"No Chorus family fits a hint card / inline annotation / sm
383
383
 
384
384
  **Off-scale = pick the next ladder rung, NOT halfway.** 13px is forbidden; 12 or 14 are tokens. 6px gap is forbidden; 4 or 8 are tokens. If no token feels right, that's a Chorus gap report ("`spacing.inline.xs` reads too tight, `inline.sm` too loose for this slot — proposing a new rung") — not a license to invent. See the full raw → token map in [compose.md § When you go custom](compose.md).
385
385
 
386
- Side note: this entry catches the values. Whether you should have gone custom at all is a separate question — the visual-reuse table in [LOVABLE.md §C](LOVABLE.md) covers 13 `"open"` families that you can borrow on visual-fit grounds (`<Feed>` as a generic article-card surface, `<Section>` as any labelled block). Reach for that first; #14 only fires after you've genuinely exhausted the LEGO ladder.
386
+ Side note: this entry catches the values. Whether you should have gone custom at all is a separate question — the visual-reuse table in [LOVABLE.md §C](LOVABLE.md) covers the `"open"` families that you can borrow on visual-fit grounds (`<Feed>` as a generic article-card surface, `<Section>` as any labelled block). Reach for that first; #14 only fires after you've genuinely exhausted the LEGO ladder.
387
387
 
388
388
  ---
389
389
 
package/agents/catalog.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Catalog — intent → component
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/catalog.md`](../i18n/ko/schema/catalog.md)
4
+
3
5
  Reverse index from natural-language intent to family + sub-component. Read *before* opening any spec to narrow candidates. Authoritative shape lives in `schema/components/<family>/<sub>.spec.json`; this file is a routing layer.
4
6
 
5
7
  > **How to import / use a family → `usage.json`.** This catalog names families as `family / sub`. The React access pattern differs per family: a named import (`profile-header` → `import { ProfileHeader }`), a `variant=` prop (`button / fab` → `<Button variant="fab">` — there is **no** `<Fab>` export), a separate export (`carousel / post` → `PostCarousel`), or a data array (`list / entry` → `<List items={[…]}>` — there is **no** `<ListItem>`). `usage.json` is the authoritative, build-time **parity-checked** map of every family to its exact usage. **A failed `import { X }` for one component never means the catalog is unreliable or a family is "missing at runtime" — look it up in `usage.json` and use the access it specifies. Never fall back to assembling a family from token primitives.**
@@ -8,7 +10,7 @@ Reverse index from natural-language intent to family + sub-component. Read *befo
8
10
 
9
11
  Each family declares `visualReuse: "open" | "locked"` in its `<family>.family.json`. The catalog respects that flag.
10
12
 
11
- - **Open (default, 27 families)** — `avatar-rail`, `badge`, `banner`, `bubble`, `button`, `carousel`, `chip`, `directory-list`, `divider`, `feed`, `header`, `list`, `metadata`, `nav-card`, `nav-list`, `navigation-bar`, `page-shell`, `profile-header`, `progress`, `side-sheet`, `skeleton`, `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.
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.
12
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.
13
15
 
14
16
  When in doubt: open families are recipes, locked families are rules.
@@ -41,8 +43,9 @@ When in doubt: open families are recipes, locked families are rules.
41
43
  | reversible state (mute/unmute, follow) | `button / toggle` |
42
44
  | dense action inside a toolbar | `button / toolbar` |
43
45
  | destructive primary commit | `button / standard` with `flavor: "destructive"` (inside a Dialog) |
46
+ | grouped buttons / docked bottom action bar | `button / group` → `<ButtonGroup>` (named export) |
44
47
 
45
- **Disambiguate**: FAB = the **single** canonical commit per screen. Destructive primary commits go in `dialog`/`bottom-sheet`, not on a FAB.
48
+ **Disambiguate**: FAB = the **single** canonical commit per screen. Destructive primary commits go in `dialog`/`bottom-sheet`, not on a FAB. **Two or more Buttons that belong together** (Cancel + Save, or a bottom-pinned action bar) go in `<ButtonGroup>` — a **named export**, not `<Button variant="group">`. Use `variant="docked"` for the bottom action bar (full-bleed surface + 16px inset + 8px gap + upward `sys.elevation.sheet` shadow + optional `label`); never hand-roll the wrapper div or give it a top border. Like the other bars it renders in flow — `page-shell` owns the pinning.
46
49
 
47
50
  ## Structure & layout
48
51
 
@@ -68,13 +71,13 @@ When in doubt: open families are recipes, locked families are rules.
68
71
  | avatar-anchored rows (channels, DMs) | `list / entry` | `full-bleed` |
69
72
  | drill-in rows with trailing chevron | `list / standard` (or `/ entry`, `/ radio`) with `nav: true` | `full-bleed` |
70
73
  | standalone drill-in card (single row) | `nav-card` | **`inline`** |
71
- | expandable titled sections (FAQ, T&C) | `accordion` | `full-bleed` |
74
+ | expandable titled sections (FAQ, T&C) | `list / accordion` | `full-bleed` |
72
75
  | authored content stream (posts, comments) | `feed / feed` | `full-bleed` |
73
76
  | follow suggestions block (swipeable peek) | `suggestion-list` | `full-bleed` |
74
77
  | follow directory (full vertical scroll) | `directory-list` | `full-bleed` |
75
78
  | label-only nav list with chevron rows | `nav-list` | `full-bleed` |
76
79
 
77
- **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.
78
81
 
79
82
  ### Entity directory rows + author attribution
80
83
 
@@ -152,8 +155,9 @@ Each row resolves to a typed React component — `<FormField variant="search" pl
152
155
  | instant-commit on/off toggle | `switch` |
153
156
  | inline status mark next to a row label | `status-tag` |
154
157
  | always-on annotation pill anchored to a UI element | `bubble` |
158
+ | dot position indicator under a snap pager | `pagination` |
155
159
 
156
- **Disambiguate**: `switch` = single binary on/off that commits the moment it flips (notifications, privacy). For multi-option pickers use `list / radio`; for actions needing confirmation use `button` + `dialog`; for "selected" facet state in a chip row use `chip / filter`. `status-tag` = SMALL (16-rung, 10px text) decorative status pill inline next to a row label — "pending", "rejected", "draft". For a 32-rung interactive chip use `chip / tag`; for a numeric count attached to an icon/thumbnail use `badge / update`; for a role / title mark beside a user's nickname (Channel owner / Verified / PRO) use `badge / role` — identity, where `status-tag` is workflow state. `bubble` = a labelled pill with a tail that points at a specific element to flag attention *at rest* (new-feature nudge, keyword/campaign promo next to a top-bar icon or search bar) — vs `badge` (numeric count / dot, no tail) and `status-tag` (status mark on a row label).
160
+ **Disambiguate**: `switch` = single binary on/off that commits the moment it flips (notifications, privacy). For multi-option pickers use `list / radio`; for actions needing confirmation use `button` + `dialog`; for "selected" facet state in a chip row use `chip / filter`. `status-tag` = SMALL (16-rung, 10px text) decorative status pill inline next to a row label — "pending", "rejected", "draft". For a 32-rung interactive chip use `chip / tag`; for a numeric count attached to an icon/thumbnail use `badge / update`; for a role / title mark beside a user's nickname (Channel owner / Verified / PRO) use `badge / role` — identity, where `status-tag` is workflow state. `bubble` = a labelled pill with a tail that points at a specific element to flag attention *at rest* (new-feature nudge, keyword/campaign promo next to a top-bar icon or search bar) — vs `badge` (numeric count / dot, no tail) and `status-tag` (status mark on a row label). `pagination` = decorative dot position row under a one-page-at-a-time pager (carousel, gallery) — non-interactive (`aria-hidden`), the host owns the active index; for tap-to-switch sections use `tabs`, for task-completion ratio use `progress`.
157
161
 
158
162
  ## Loading & Placeholder
159
163
 
@@ -161,10 +165,12 @@ Each row resolves to a typed React component — `<FormField variant="search" pl
161
165
 
162
166
  | Intent | Family + sub |
163
167
  | ----------------------------------------------------- | --------------------- |
168
+ | indeterminate sub-second load (no known ratio) | `spinner` |
164
169
  | in-flight content placeholder (mirrors content shape) | `skeleton` |
170
+ | surface with no data yet (empty feed / search / inbox) | `empty-state` |
165
171
  | linear progress for a known long-running task | `progress` |
166
172
 
167
- **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). For sub-300ms or opaque waits use neither (a brief blank surface or a spinner reads better).
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`.
168
174
 
169
175
  ## Shadcn / Lovable name translation
170
176
 
@@ -1,5 +1,7 @@
1
1
  # Avatar rail
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/avatar-rail/avatar-rail.md`](../../../i18n/ko/schema/components/avatar-rail/avatar-rail.md)
4
+
3
5
  A horizontal strip of channel entry points — each item routes to a channel or company page. Composes a [Thumbnail](../thumbnail/thumbnail.md) (optional `updateDot`) and a single-line label; an optional trailing action lives at the end.
4
6
 
5
7
  **Reach for this when** the row is a label-only nav strip of subscribed or saved entities — *Subscribed channels*, *Saved companies*, *My topics*. **Skip when** rows carry follower counts or descriptions (use [DirectoryList](../directory-list/directory-list.md) / [SuggestionList](../suggestion-list/suggestion-list.md)), the surface is fixed-width profile cards ([Profile carousel](../carousel/profile.md)), or rows commit in place rather than route ([Toggle Button](../button/toggle.md) cluster).
@@ -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.focusInset"
133
+ },
134
+ "outerRing": {
135
+ "width": "sys.borderWidth.thin",
136
+ "color": "sys.color.focus"
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,
@@ -1,5 +1,7 @@
1
1
  # Badge
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/badge/badge.md`](../../../i18n/ko/schema/components/badge/badge.md)
4
+
3
5
  A small, non-interactive mark anchored to a host label — badge-shaped annotation for two roles. **Update** is the brand-tone activity indicator: a numeric count pill or a labelless corner dot flagging unread / new activity (3 unread, `99+`, a dot on a [Thumbnail](../thumbnail/thumbnail.md)). **Role** is the tonal identity pill: a pale `primaryContainer` mark naming the user's role or title — Channel owner (채널장), Verified (현직자) — riding the bare nickname at the end of their metadata's meta row. The two share the anchored anatomy, the `radius.full` corner, and the never-interactive contract; they diverge on tone (brand vs primary-container) and on what they say about the host (*what's new* vs *who this is*). Both are reached through the single `Badge` export — `variant="update"` (default) / `variant="role"`.
4
6
 
5
7
  **Layout inset.** `inline` — slot atom. A Badge has no page-rail responsibility; the host places it (Thumbnail corner, List row label, [Metadata](../metadata/metadata.md) trailing edge). Never a sibling of `full-bleed` page rows.
@@ -1,5 +1,7 @@
1
1
  # Role
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/badge/role.md`](../../../i18n/ko/schema/components/badge/role.md)
4
+
3
5
  A tonal identity pill naming a user's role or title — anchored at the trailing edge of their metadata. Pale `primaryContainer` fill with a deep `onPrimaryContainer` label: informational, quiet, and clearly distinct from the [Update](./update.md) sub's brand-tone alert. Always rides beside a user identity — canonically the bare nickname at the end of the metadata row's second line, **exactly one badge per nickname** — never interactive. Two appearances: `default` (tonal primaryContainer) and `inverse` (near-black inverseSurface pair, reserved for the paid-expert **PRO** mark).
4
6
 
5
7
  **Reach for this when** the mark says *who the person is* — Channel owner (채널장), Verified (현직자), Moderator. **Skip when** the mark signals workflow state — pending / approved / rejected belong to [Status tag](../status-tag/status-tag.md) — or new-activity on a host — that's [Update](./update.md).
@@ -1,5 +1,7 @@
1
1
  # Update
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/badge/update.md`](../../../i18n/ko/schema/components/badge/update.md)
4
+
3
5
  A small brand-tone indicator attached to a host label — flagging *new or unread activity*. Two types share the brand fill and `radius.full` corner: **Numeric** (a labelled count pill) and **Dot** (a labelless update dot used by [Thumbnail](../thumbnail/thumbnail.md)). Always anchored to a host; never interactive.
4
6
 
5
7
  **Reach for Numeric when** the count itself carries meaning — 3 unread, 12 mentions, `99+` notifications. **Reach for Dot when** the presence of activity is the whole signal — a corner flag without a magnitude. **Skip Update** when the mark names *who the person is* rather than *what is new* — use [Role](./role.md) — or when it is descriptive metadata — use [Tag](../chip/tag.md).
@@ -7,7 +7,9 @@
7
7
  "inline notice",
8
8
  "supplementary context",
9
9
  "tonal aside",
10
- "in-flow tip"
10
+ "in-flow tip",
11
+ "trailing text-button commit",
12
+ "accent tint with neutral body copy"
11
13
  ],
12
14
  "visualReuse": "open",
13
15
  "layoutInset": "inline",
@@ -1,6 +1,8 @@
1
1
  # Banner
2
2
 
3
- An in-body explanation block — a tinted card sitting within the reading flow with a short paragraph and an optional follow-through link. Two axes: **appearance** (`default` / `accent` / `destructive`), **leading slot** (`icon` / `thumbnail` / none).
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/banner/banner.md`](../../../i18n/ko/schema/components/banner/banner.md)
4
+
5
+ An in-body explanation block — a tinted card sitting within the reading flow with an optional heading line, a short paragraph, and an optional follow-through link. Five axes: **appearance** (`default` / `accent` / `destructive`), **foreground** (tonal, or `neutralBody` to lay the Default neutral text over the accent fill), **outline** (`outlined` / none), **leading slot** (`icon` / `thumbnail` / none), **trailing slot** (`trailingIcon` / `trailingAction` Text Button / none).
4
6
 
5
7
  **Reach for this when** a passage needs a brief aside the reader can scan or skip. **Skip when** the message demands a decision ([Dialog](../dialog/dialog.md) / [Bottom sheet](../bottom-sheet/bottom-sheet.md)) or confirms a recent user action ([Toast](../toast/toast.md)).
6
8
 
@@ -42,6 +44,25 @@ import { Banner } from '@teamblind-chorus/ui';
42
44
  </Banner>
43
45
  ```
44
46
 
47
+ ### Accent with neutral body
48
+
49
+ The `accent` fill kept, but the copy re-toned to the **Default** appearance's neutral foreground — title + body in `sys.color.onSurface`, action stepping to `sys.color.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
+
51
+ ```preview
52
+ banner/accent-neutral-body
53
+ ---
54
+ import { Banner } from '@teamblind-chorus/ui';
55
+
56
+ <Banner
57
+ appearance="accent"
58
+ neutralBody
59
+ title="Level up faster"
60
+ action={{ label: 'How levels work', href: '#level' }}
61
+ >
62
+ Stay active in the community to level up and unlock more of what the app offers.
63
+ </Banner>
64
+ ```
65
+
45
66
  ### Destructive
46
67
 
47
68
  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.
@@ -77,6 +98,94 @@ import { Banner, Thumbnail } from '@teamblind-chorus/ui';
77
98
  </Banner>
78
99
  ```
79
100
 
101
+ ### Outlined
102
+
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.outlineVariant`) 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
+
105
+ ```preview
106
+ banner/outlined
107
+ ---
108
+ import { Banner } from '@teamblind-chorus/ui';
109
+
110
+ // vertical 8 between sibling banners is the parent column's job (safe zone)
111
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sys-layout-stack-xs)' }}>
112
+ <Banner appearance="default" outlined>
113
+ Stay active in the community to level up and unlock more of what the app offers.
114
+ </Banner>
115
+ <Banner appearance="accent" outlined>
116
+ Stay active in the community to level up and unlock more of what the app offers.
117
+ </Banner>
118
+ </div>
119
+ ```
120
+
121
+ ### With title
122
+
123
+ 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
+
125
+ ```preview
126
+ banner/with-title
127
+ ---
128
+ import { Banner } from '@teamblind-chorus/ui';
129
+
130
+ <Banner
131
+ appearance="default"
132
+ title="Level up faster"
133
+ action={{ label: 'How levels work', href: '#level' }}
134
+ >
135
+ Stay active in the community to level up and unlock more of what the app offers.
136
+ </Banner>
137
+ ```
138
+
139
+ ### With trailing icon
140
+
141
+ 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
+
143
+ ```preview
144
+ banner/with-trailing-icon
145
+ ---
146
+ import { Banner } from '@teamblind-chorus/ui';
147
+ import { ForwardCircleFillIcon } from '@teamblind-chorus/ui/icons';
148
+
149
+ <Banner
150
+ appearance="default"
151
+ title="Level up faster"
152
+ trailingIcon={<ForwardCircleFillIcon size={16} />}
153
+ >
154
+ Stay active in the community to level up and unlock more of what the app offers.
155
+ </Banner>
156
+ ```
157
+
158
+ ### With trailing action (Text Button)
159
+
160
+ 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
+
162
+ ```preview
163
+ banner/with-trailing-action
164
+ ---
165
+ import { Banner, Button } from '@teamblind-chorus/ui';
166
+ import { ChevronRightIcon } from '@teamblind-chorus/ui/icons';
167
+
168
+ // vertical 8 between sibling banners is the parent column's job (safe zone)
169
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sys-layout-stack-xs)' }}>
170
+ <Banner
171
+ appearance="accent"
172
+ trailingAction={(
173
+ <Button variant="text" appearance="accent" size="small" trailingIcon={<ChevronRightIcon />}>
174
+ Enable
175
+ </Button>
176
+ )}
177
+ >
178
+ Turn on notifications to hear back the moment someone replies.
179
+ </Banner>
180
+ <Banner
181
+ appearance="default"
182
+ trailingAction={<Button variant="text" size="small">Dismiss</Button>}
183
+ >
184
+ Stay active in the community to level up and unlock more of what the app offers.
185
+ </Banner>
186
+ </div>
187
+ ```
188
+
80
189
  ### With icon
81
190
 
82
191
  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.
@@ -100,31 +209,38 @@ import { StarIcon } from '@teamblind-chorus/ui/icons';
100
209
 
101
210
  Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner carries no disabled state; only the optional action link follows the link state contract.
102
211
 
103
- | Appearance | Container fill | Body / action color | When to use |
104
- |---------------|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------------|
105
- | `default` | `sys.color.scrimSubtle` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | body in `sys.color.onSurface`, action steps to `sys.color.primary` | Supplementary asides the reader can pass over without missing the main flow. |
106
- | `accent` | `sys.color.primaryContainer` | body in `onPrimaryContainer`, action inherits | Asides worth pulling the eye toward — new-feature explainers, capability nudges. |
107
- | `destructive` | `sys.color.errorContainer` | body in `onErrorContainer`, action inherits | Blocking errors or rejections failed approvals, outages, billing. |
212
+ | Appearance | Container fill | Body / action color | Outline (when `outlined`) | When to use |
213
+ |---------------|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------|----------------------------|------------------------------------------------------------------------------|
214
+ | `default` | `sys.color.scrimSubtle` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | body in `sys.color.onSurface`, action steps to `sys.color.primary` | `sys.color.outlineVariant` (subtle gray) | Supplementary asides the reader can pass over without missing the main flow. |
215
+ | `accent` | `sys.color.primaryContainer` | body in `onPrimaryContainer`, action inherits | `sys.color.primary` at 40% (soft blue) | Asides worth pulling the eye toward — new-feature explainers, capability nudges. |
216
+ | `accent` + `neutralBody` | `sys.color.primaryContainer` | title + body in `onSurface`, action steps to `sys.color.primary` | `sys.color.primary` at 40% (soft blue) | Accent tint pulls the eye, but the copy stays quiet, high-legibility body text — longer explainers, dense asides. |
217
+ | `destructive` | `sys.color.errorContainer` | body in `onErrorContainer`, action inherits | `sys.color.error` at 40% | Blocking errors or rejections — failed approvals, outages, billing. |
108
218
 
109
219
  ## Slots
110
220
 
111
221
  - **container** — tinted block. Horizontal flex with `align-items: flex-start`; 12px inset, 8px sibling gap, 8px corner radius.
112
222
  - **icon** *(optional)* — 16 × 16 glyph. Slot height equals the `body.sm` line box so the glyph centres on the body's first line. Paints in `currentColor`.
113
223
  - **thumbnail** *(optional)* — leading [Thumbnail](../thumbnail/thumbnail.md). Takes precedence over `icon`; footprint and corner come from Thumbnail.
114
- - **content** — vertical column holding body and optional action; 8px stack gap; fills remaining inline space.
224
+ - **content** — vertical column holding optional title, body, and optional action; 8px stack gap (4px title↔body); fills remaining inline space.
225
+ - **title** *(optional)* — heading line above the body. `label.md` / Semibold / inherits container foreground. 4px (`sys.layout.stack.2xs`) gap to the body.
115
226
  - **body** — explanation copy. `body.sm` / Regular / inherits container foreground. Required.
116
227
  - **action** *(optional)* — follow-through link below the body. `label.md` / Semibold / underlined.
228
+ - **trailingIcon** *(optional)* — 16 × 16 glyph at the trailing edge, vertically centred against the container. Paints in `currentColor`.
229
+ - **trailingAction** *(optional)* — a [Text Button](../button/text.md) at the trailing edge, vertically centred. Owns its own size + appearance; default the appearance to the banner's colour family. Takes precedence over `trailingIcon`.
117
230
 
118
231
  ## Anatomy
119
232
 
120
233
  | Slot | Token bindings |
121
234
  |-----------|----------------|
122
- | container | Fill + foreground per appearance, `sys.radius.md` (8), `sys.layout.container.sm` (12) padding, `sys.layout.stack.xs` (8) sibling gap, `align-items: flex-start` |
235
+ | container | Fill + foreground per appearance, `sys.radius.md` (8), `sys.layout.container.sm` (12) padding, `sys.layout.stack.xs` (8) sibling gap, `align-items: flex-start`. With `outlined`: `sys.borderWidth.hairline` (1) inset stroke in the appearance's outline color |
123
236
  | icon | `sys.icon.md` (16 × 16) glyph inside a slot whose height equals the body's first-line box (`calc(sys.typo.body.sm.size * sys.typo.body.sm.line)`); `color: currentColor` |
124
237
  | thumbnail | Delegated to [Thumbnail](../thumbnail/thumbnail.md); footprint-preserving (`flex: 0 0 auto`) |
125
- | content | Flex column, `flex: 1 1 auto`, `sys.layout.stack.xs` (8) body↔action gap |
238
+ | content | Flex column, `flex: 1 1 auto`, `sys.layout.stack.xs` (8) body↔action gap, `sys.layout.stack.2xs` (4) title↔body gap |
239
+ | title | `sys.typo.label.md` (14 / Semibold 600), color inherits |
126
240
  | body | `sys.typo.body.sm` (14 / Regular), color inherits |
127
241
  | action | `sys.typo.label.md` (14 / Semibold), underlined. Steps to `sys.color.primary` in `default`; inherits in `accent` / `destructive`. |
242
+ | trailingIcon | `sys.icon.md` (16 × 16) glyph, `align-self: center` against the container, `color: currentColor` |
243
+ | 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` |
128
244
 
129
245
  ## States
130
246
 
@@ -15,11 +15,34 @@
15
15
  ],
16
16
  "default": "default"
17
17
  },
18
+ "outlined": {
19
+ "type": "boolean",
20
+ "optional": true,
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.outlineVariant`) on `default`'s gray-tinted fill, `primary` at 40% (`color-mix(sys.color.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
+ },
24
+ "neutralBody": {
25
+ "type": "boolean",
26
+ "optional": true,
27
+ "default": false,
28
+ "appliesTo": "accent",
29
+ "description": "On `accent`, paints the title + body in the neutral default foreground (`sys.color.onSurface`) and steps the action to `sys.color.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
+ },
31
+ "title": {
32
+ "type": "node",
33
+ "optional": true,
34
+ "description": "Optional heading line above the body. label.md (14 / Semibold 600) in the container's foreground, separated from the body by `sys.layout.stack.2xs` (4). Reach for it when the aside needs a scannable lead-in; omit for single-thought asides where the body carries itself."
35
+ },
18
36
  "icon": {
19
37
  "type": "node",
20
38
  "optional": true,
21
39
  "description": "A 16 × 16 (`sys.icon.md`) glyph at the container's leading edge. Inherits the banner's foreground (`currentColor`) so the mark reads as part of the body copy. The slot occupies the body.sm line-box height so the glyph centers on the **first line** of the body — multi-line bodies keep the icon anchored to the first-line cap, not the block center. Ignored when `thumbnail` is also passed."
22
40
  },
41
+ "trailingIcon": {
42
+ "type": "node",
43
+ "optional": true,
44
+ "description": "A 16 × 16 (`sys.icon.md`) glyph at the container's trailing edge, vertically centered against the whole block (`align-self: center`). Paints in `currentColor`. Reach for it when the banner leads somewhere — a forward affordance such as `ForwardCircleFillIcon` signaling the whole aside opens a destination."
45
+ },
23
46
  "thumbnail": {
24
47
  "type": "node",
25
48
  "optional": true,
@@ -30,6 +53,11 @@
30
53
  "optional": true,
31
54
  "description": "{ label, href? , onClick? } — a follow-through link rendered as a block child below the body."
32
55
  },
56
+ "trailingAction": {
57
+ "type": "node",
58
+ "optional": true,
59
+ "description": "A [Text Button](../button/text.md) (`<Button variant=\"text\">`) rendered at the container's trailing edge, vertically centered against the whole block (`align-self: center`). Distinct from `action` (a follow-through link below the body): `trailingAction` is a compact inline commit that sits beside the copy — Dismiss, Enable, Undo. The button keeps full control of its own `size` and `appearance` per the button/text spec; **by default pick the appearance whose color family matches the banner fill** 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. Override only when a denser rung (`size=\"small\"` / `\"xsmall\"`) or a different emphasis is deliberately wanted. The button also keeps its own `leadingIcon` / `trailingIcon` slots, so the commit can carry an in-button glyph (e.g. a trailing `ChevronRightIcon` on an *Enable* / *Continue* commit). Takes precedence over the banner-level `trailingIcon` when both are passed."
60
+ },
33
61
  "children": {
34
62
  "type": "node",
35
63
  "required": true,
@@ -58,9 +86,16 @@
58
86
  },
59
87
  "content": {
60
88
  "required": true,
61
- "description": "Vertical column holding body and (optional) action. flex: 1 1 auto.",
89
+ "description": "Vertical column holding (optional) title, body, and (optional) action. flex: 1 1 auto.",
62
90
  "intrinsic": true
63
91
  },
92
+ "title": {
93
+ "required": false,
94
+ "description": "Optional heading line above the body. label.md (14 / Semibold) / inherits container's foreground. Separated from the body by `sys.layout.stack.2xs` (4) — tighter than the body↔action gap so title and body read as one passage.",
95
+ "accepts": [
96
+ "text"
97
+ ]
98
+ },
64
99
  "body": {
65
100
  "required": true,
66
101
  "description": "Explanation copy. body.sm / Regular / inherits container's foreground. Single paragraph; wraps freely.",
@@ -74,6 +109,20 @@
74
109
  "accepts": [
75
110
  "button"
76
111
  ]
112
+ },
113
+ "trailingIcon": {
114
+ "required": false,
115
+ "description": "16 × 16 (`sys.icon.md`) glyph slot at the trailing edge. Vertically centered against the container (`align-self: center`), paints in `currentColor`. Typically a forward affordance (e.g. `ForwardCircleFillIcon`).",
116
+ "accepts": [
117
+ "icon"
118
+ ]
119
+ },
120
+ "trailingAction": {
121
+ "required": false,
122
+ "description": "Trailing-edge slot hosting a Text Button (`<Button variant=\"text\">`). Footprint-preserving (`flex: 0 0 auto`) and vertically centered against the container (`align-self: center`). The Button owns its own size + appearance; default the appearance to the banner's color family (accent → `accent`, default → `default`, destructive → `destructive` flavor). Takes precedence over `trailingIcon`.",
123
+ "accepts": [
124
+ "button"
125
+ ]
77
126
  }
78
127
  },
79
128
  "sizing": {
@@ -84,8 +133,13 @@
84
133
  "iconSlotHeight": "calc(sys.typo.body.sm.size * sys.typo.body.sm.line)",
85
134
  "iconColor": "currentColor",
86
135
  "contentStackGap": "sys.layout.stack.xs",
136
+ "titleTypo": "sys.typo.label.md",
137
+ "titleGap": "sys.layout.stack.2xs",
87
138
  "bodyTypo": "sys.typo.body.sm",
88
- "actionTypo": "sys.typo.label.md"
139
+ "actionTypo": "sys.typo.label.md",
140
+ "trailingIconSize": "sys.icon.md",
141
+ "trailingIconColor": "currentColor",
142
+ "outlineWidth": "sys.borderWidth.hairline"
89
143
  },
90
144
  "safeZone": {
91
145
  "inline": {
@@ -106,26 +160,33 @@
106
160
  "background": "sys.color.scrimSubtle",
107
161
  "foreground": "sys.color.onSurface",
108
162
  "actionColor": "sys.color.primary",
163
+ "outlineColor": "sys.color.outlineVariant",
109
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.scrimSubtle` — ~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."
110
165
  },
111
166
  "accent": {
112
167
  "background": "sys.color.primaryContainer",
113
168
  "foreground": "sys.color.onPrimaryContainer",
114
169
  "actionColor": "inherit",
115
- "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."
170
+ "outlineColor": "color-mix(sys.color.primary, 40%)",
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."
116
172
  },
117
173
  "destructive": {
118
174
  "background": "sys.color.errorContainer",
119
175
  "foreground": "sys.color.onErrorContainer",
120
176
  "actionColor": "inherit",
177
+ "outlineColor": "color-mix(sys.color.error, 40%)",
121
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."
122
179
  }
123
180
  },
124
181
  "behavior": {
125
182
  "actionLink": "When present, renders as an <a> and accepts either href (browser navigation) or onClick (consumer-controlled). Underline persists at rest so the link reads as actionable inside the muted block.",
183
+ "trailingAction": "A `<Button variant=\"text\">` in the trailing slot is a real interactive control (not aria-hidden, unlike the trailing icon). It carries its own size + appearance per the button/text spec; the default appearance follows the banner's color family so the commit reads as part of the tinted block (accent → accent, default → default, destructive → destructive flavor). When both `trailingAction` and `trailingIcon` are passed, the action wins the slot.",
184
+ "neutralForeground": "`neutralBody` re-tones only the accent appearance: the container foreground becomes `onSurface` (title + body) and the action steps to `primary`, matching the Default appearance's foreground treatment. Ignored on `default` and `destructive`.",
126
185
  "role": "Container carries role='note' so screen readers announce the banner as an aside."
127
186
  },
128
187
  "forbidden": [
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
+ "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",
129
190
  "default banner background painted with sys.color.brandContainer — informational banners use sys.color.primaryContainer; promotional banners use sys.color.surfaceContainerLow",
130
191
  "banner thumbnail slot omitted when banner role carries imagery — empty image area is forbidden, fall back to /placeholder.png",
131
192
  "banner used for transient confirmations — that role is the `toast` family (locked)",
@@ -1,5 +1,7 @@
1
1
  # Bottom sheet
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/bottom-sheet/bottom-sheet.md`](../../../i18n/ko/schema/components/bottom-sheet/bottom-sheet.md)
4
+
3
5
  An edge-anchored interruption — a panel that rises from the bottom of the viewport, sits over a scrim, and holds richer content than a Dialog can.
4
6
 
5
7
  **Reach for this when** you want to steer the user toward a preferred action *without severing the flow* — present enough to focus attention, light enough that dismissing returns them where they were. **Skip when** the decision must be committed before the flow can continue (use [Dialog](../dialog/dialog.md)).
@@ -1,5 +1,7 @@
1
1
  # Bubble
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/bubble/bubble.md`](../../../i18n/ko/schema/components/bubble/bubble.md)
4
+
3
5
  A small persistent annotation pill with a tail pointing at an anchor — a chat-icon "new messages" flag, a search-bar campaign nudge, a feature-flag callout. Sibling of [Tooltip](../tooltip/tooltip.md): Tooltip is transient and overlays neighbours on hover; Bubble stays in view at the resting state, sits lower in visual priority (no elevation, smaller padding, single line), and never occludes the surrounding chrome.
4
6
 
5
7
  **Reach for this when** the annotation must remain readable as part of the resting UI. **Skip when** the hint is invoked on demand (use [Tooltip](../tooltip/tooltip.md)), carries a decision or blocking meaning (use [Banner](../banner/banner.md) / [Dialog](../dialog/dialog.md)), or is just a numeric count beside an icon (use [Badge](../badge/badge.md)).
@@ -19,10 +19,11 @@
19
19
  "visualReuse": "open",
20
20
  "layoutInset": "inline",
21
21
  "usage": {
22
- "note": "All button roles are the single `Button` export selected by the `variant` prop — there is NO `<Fab>`, `<IconButton>`, `<ToggleButton>`, or `<Toolbar>` export. An `import { Fab }` failure does NOT mean the family is missing; reach for `<Button variant=\"fab\">`.",
22
+ "note": "All button ROLES are the single `Button` export selected by the `variant` prop — there is NO `<Fab>`, `<IconButton>`, `<ToggleButton>`, or `<Toolbar>` export. An `import { Fab }` failure does NOT mean the family is missing; reach for `<Button variant=\"fab\">`. The one separate named export is `ButtonGroup` (the `group` sub) — a layout wrapper for two or more Buttons, NOT a Button variant.",
23
23
  "subs": {
24
24
  "fab": { "variant": "fab", "example": "<Button variant=\"fab\" appearance=\"primary\" icon={<EditIcon />}>글쓰기</Button>" },
25
- "icon": { "variant": "icon", "example": "<Button variant=\"icon\" aria-label=\"…\" icon={<MoreIcon />} />" }
25
+ "icon": { "variant": "icon", "example": "<Button variant=\"icon\" aria-label=\"…\" icon={<MoreIcon />} />" },
26
+ "group": { "import": "ButtonGroup", "example": "<ButtonGroup variant=\"docked\" label={<>선택한 직군: <strong>SW개발</strong></>}><Button appearance=\"outlined\" size=\"large\">연봉 정보</Button><Button appearance=\"primary\" size=\"large\">추천 채용공고</Button></ButtonGroup>", "note": "Named export — NOT a Button variant (there is no `<Button variant=\"group\">`). Lays out adjacent Buttons; `variant=\"docked\"` is the bottom-pinned action bar (surface + 16px inset + 8px gap + upward `sys.elevation.sheet` shadow + optional `label`). Renders in flow — PageShell owns the pinning." }
26
27
  }
27
28
  },
28
29
  "spec": "button.md",
@@ -33,6 +34,11 @@
33
34
  "md": "button.md",
34
35
  "default": true
35
36
  },
37
+ {
38
+ "slug": "group",
39
+ "spec": "group.spec.json",
40
+ "md": "standard.md"
41
+ },
36
42
  {
37
43
  "slug": "fab",
38
44
  "spec": "fab.spec.json",
@@ -1,5 +1,7 @@
1
1
  # Button
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/button/button.md`](../../../i18n/ko/schema/components/button/button.md)
4
+
3
5
  The action-surface family. **Standard Button** is the default inline filled / outlined / tertiary control; the rest specialise by **shape · context · footprint**: floating canvas commit (FAB), glyph-only capsule (Icon Button), chromeless link-shaped commit (Text Button), dense capsule for toolbars and tabs (Toolbar Button), reversible commit at the Toolbar footprint (Toggle Button), option-toggle with leading checkbox (Check Button). Per-sub intent lives on each sub's page.
4
6
 
5
7
  **Layout inset.** `inline` — slot atom. No page-rail responsibility; the surrounding container places it. Lives inside another component's slot (List row trailing, Section header trailing, NavigationBar trailing, BottomSheet action stack) or inside a layout `<div>` that already pays the page gutter. The FAB sub is the one exception — pinned to the page viewport, not the row rail.
@@ -1,5 +1,7 @@
1
1
  # Check
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/button/check.md`](../../../i18n/ko/schema/components/button/check.md)
4
+
3
5
  Option-toggle commit — a [Text Button](./text.md) with a required leading checkbox glyph that flips outline → fill on `checked`, plus an optional middle icon. Two sizes (`medium` / `small`) where the checkbox footprint is the differentiator.
4
6
 
5
7
  **Reach for this when** an option is committed alongside the surface's main action — *Use this perk?*, *Apply offer*, *Keep me signed in*. **Skip when** the row is a form-bound checkbox input (out-of-system), the commit is a one-shot action ([Text Button](./text.md)), or the row needs a radio's single-select contract ([Radio list](../list/radio.md)).
@@ -124,6 +124,25 @@
124
124
  "opacity": "sys.state.pressed"
125
125
  }
126
126
  },
127
+ "focused": {
128
+ "overlay": {
129
+ "color": "label",
130
+ "opacity": "sys.state.focus"
131
+ },
132
+ "focusRing": {
133
+ "composition": "outward",
134
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
135
+ "innerCounterRing": {
136
+ "width": "sys.borderWidth.hairline",
137
+ "color": "sys.color.focusInset"
138
+ },
139
+ "outerRing": {
140
+ "width": "sys.borderWidth.thin",
141
+ "color": "sys.color.focus"
142
+ }
143
+ },
144
+ "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 button is in; never via plain mouse click."
145
+ },
127
146
  "disabled": {
128
147
  "overlay": null,
129
148
  "containerOpacity": "sys.state.disabled",
@@ -1,5 +1,7 @@
1
1
  # FAB
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/button/fab.md`](../../../i18n/ko/schema/components/button/fab.md)
4
+
3
5
  Floating action button — surface-elevated commit anchored to the canvas, reachable while content scrolls. Single fixed rung with a pill geometry (`sys.radius.full`) and floating elevation (`sys.elevation.floating`); label and icon both optional, at least one present.
4
6
 
5
7
  **Reach for this when** the canvas needs a single headline action that survives scroll — Compose, Add, Create. **Skip when** the action lives inline with content (use [Standard Button](./standard.md)) or the row is a dense toolbar ([Toolbar Button](./toolbar.md)).