@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 🇰🇷 한국어: [`i18n/ko/schema/components/banner/banner.md`](../../../i18n/ko/schema/components/banner/banner.md)
|
|
4
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.
|
|
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).
|
|
6
6
|
|
|
7
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)).
|
|
8
8
|
|
|
@@ -25,9 +25,7 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
25
25
|
</Banner>
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
### Accent
|
|
28
|
+
## Accent
|
|
31
29
|
|
|
32
30
|
The primary-tinted appearance. Body and action both paint in the primary family — reach for it when the aside should pull more attention.
|
|
33
31
|
|
|
@@ -44,7 +42,26 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
44
42
|
</Banner>
|
|
45
43
|
```
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
## Neutral body
|
|
46
|
+
|
|
47
|
+
The `accent` fill kept, but the copy re-toned to the **Default** appearance's neutral foreground — title + body in `sys.color.text.default`, action stepping to `sys.color.background.primary`. Pass `neutralBody`. This decouples the background tone from the text tone: the `primaryContainer` tint still pulls the eye, but the copy reads as quiet, high-legibility body text rather than tonal `onPrimaryContainer`. Reach for it on longer explainers or denser asides where primary-family body copy would tire the reader. No effect on `default` (already `onSurface`) or `destructive` (the warning tone must carry through the copy).
|
|
48
|
+
|
|
49
|
+
```preview
|
|
50
|
+
banner/accent-neutral-body
|
|
51
|
+
---
|
|
52
|
+
import { Banner } from '@teamblind-chorus/ui';
|
|
53
|
+
|
|
54
|
+
<Banner
|
|
55
|
+
appearance="accent"
|
|
56
|
+
neutralBody
|
|
57
|
+
title="Level up faster"
|
|
58
|
+
action={{ label: 'How levels work', href: '#level' }}
|
|
59
|
+
>
|
|
60
|
+
Stay active in the community to level up and unlock more of what the app offers.
|
|
61
|
+
</Banner>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Destructive
|
|
48
65
|
|
|
49
66
|
The error-tinted appearance — `errorContainer` fill with `onErrorContainer` foreground. Reach for it when the aside is a blocking error or rejection (failed approvals, integration outages, billing). Use sparingly.
|
|
50
67
|
|
|
@@ -61,7 +78,7 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
61
78
|
</Banner>
|
|
62
79
|
```
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
## Thumbnail
|
|
65
82
|
|
|
66
83
|
A leading [Thumbnail](../thumbnail/thumbnail.md) at the top-left — reach for it when the aside is anchored to a channel, author, or sub-brand image. Thumbnail owns its diameter and corner shape; the slot only top-aligns it next to the content column.
|
|
67
84
|
|
|
@@ -79,9 +96,9 @@ import { Banner, Thumbnail } from '@teamblind-chorus/ui';
|
|
|
79
96
|
</Banner>
|
|
80
97
|
```
|
|
81
98
|
|
|
82
|
-
|
|
99
|
+
## Outlined
|
|
83
100
|
|
|
84
|
-
An optional `sys.borderWidth.hairline` (1) inset stroke toned to the appearance's color family and kept deliberately faint, so the outline reads as a soft edge of the same tint rather than a frame — the subtle gray hairline (`sys.color.
|
|
101
|
+
An optional `sys.borderWidth.hairline` (1) inset stroke toned to the appearance's color family and kept deliberately faint, so the outline reads as a soft edge of the same tint rather than a frame — the subtle gray hairline (`sys.color.border.default`) on `default`'s gray-tinted scrim, `primary` at 40% on `accent`'s blue-tinted container, `error` at 40% on `destructive`. Painted as an inset box-shadow, never a real border, so toggling it cannot change the banner's footprint. Reach for it when the tinted fill alone doesn't separate the banner from its host surface.
|
|
85
102
|
|
|
86
103
|
```preview
|
|
87
104
|
banner/outlined
|
|
@@ -99,7 +116,7 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
99
116
|
</div>
|
|
100
117
|
```
|
|
101
118
|
|
|
102
|
-
|
|
119
|
+
## Title
|
|
103
120
|
|
|
104
121
|
An optional heading line above the body — `label.md` (14 / Semibold) in the container's foreground, separated from the body by `sys.layout.stack.2xs` (4) so the pair reads as one passage. Reach for it when the aside needs a scannable lead-in; omit it for single-thought asides where the body carries itself.
|
|
105
122
|
|
|
@@ -117,7 +134,7 @@ import { Banner } from '@teamblind-chorus/ui';
|
|
|
117
134
|
</Banner>
|
|
118
135
|
```
|
|
119
136
|
|
|
120
|
-
|
|
137
|
+
## Trailing icon
|
|
121
138
|
|
|
122
139
|
A 16 × 16 (`sys.icon.md`) glyph at the trailing edge, vertically centred against the whole block and painted in `currentColor`. Reach for it when the banner leads somewhere — a forward affordance such as `ForwardCircleFillIcon` signals the aside opens a destination.
|
|
123
140
|
|
|
@@ -136,7 +153,38 @@ import { ForwardCircleFillIcon } from '@teamblind-chorus/ui/icons';
|
|
|
136
153
|
</Banner>
|
|
137
154
|
```
|
|
138
155
|
|
|
139
|
-
|
|
156
|
+
## Trailing action
|
|
157
|
+
|
|
158
|
+
A [Text Button](../button/text.md) (`<Button variant="text">`) in the trailing slot, vertically centred against the block — a compact inline commit beside the copy (*Dismiss*, *Enable*, *Undo*), distinct from `action` (the follow-through link below the body). The button keeps full control of its own `size` and `appearance` per the button/text spec, but **default the appearance to the banner's colour family** so the commit reads as part of the tinted block — `accent` banner → `appearance="accent"`, `default` banner → `appearance="default"`, `destructive` banner → the Text Button `destructive` flavor. It also keeps the button/text `leadingIcon` / `trailingIcon` slots, so the commit can carry an in-button glyph — e.g. a trailing `ChevronRightIcon` on a *Enable* / *Continue* commit. When both `trailingAction` and the banner-level `trailingIcon` are passed, the action wins the slot.
|
|
159
|
+
|
|
160
|
+
```preview
|
|
161
|
+
banner/with-trailing-action
|
|
162
|
+
---
|
|
163
|
+
import { Banner, Button } from '@teamblind-chorus/ui';
|
|
164
|
+
import { ChevronRightIcon } from '@teamblind-chorus/ui/icons';
|
|
165
|
+
|
|
166
|
+
// vertical 8 between sibling banners is the parent column's job (safe zone)
|
|
167
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sys-layout-stack-xs)' }}>
|
|
168
|
+
<Banner
|
|
169
|
+
appearance="accent"
|
|
170
|
+
trailingAction={(
|
|
171
|
+
<Button variant="text" appearance="accent" size="small" trailingIcon={<ChevronRightIcon />}>
|
|
172
|
+
Enable
|
|
173
|
+
</Button>
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
Turn on notifications to hear back the moment someone replies.
|
|
177
|
+
</Banner>
|
|
178
|
+
<Banner
|
|
179
|
+
appearance="default"
|
|
180
|
+
trailingAction={<Button variant="text" size="small">Dismiss</Button>}
|
|
181
|
+
>
|
|
182
|
+
Stay active in the community to level up and unlock more of what the app offers.
|
|
183
|
+
</Banner>
|
|
184
|
+
</div>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Icon
|
|
140
188
|
|
|
141
189
|
A 16 × 16 (`sys.icon.md`) glyph at the leading edge, painted in `currentColor`. The slot is sized to the body's first-line height so the glyph centres on the first line — multi-line bodies keep the icon anchored to the first-line cap, not the block centre. Reach for it when the aside leads with a meaning-bearing glyph rather than a brand image.
|
|
142
190
|
|
|
@@ -161,9 +209,10 @@ Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner c
|
|
|
161
209
|
|
|
162
210
|
| Appearance | Container fill | Body / action color | Outline (when `outlined`) | When to use |
|
|
163
211
|
|---------------|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------|----------------------------|------------------------------------------------------------------------------|
|
|
164
|
-
| `default` | `sys.color.
|
|
165
|
-
| `accent` | `sys.color.
|
|
166
|
-
| `
|
|
212
|
+
| `default` | `sys.color.background.neutral` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | body in `sys.color.text.default`, action steps to `sys.color.background.primary` | `sys.color.border.default` (subtle gray) | Supplementary asides the reader can pass over without missing the main flow. |
|
|
213
|
+
| `accent` | `sys.color.background.selected` | body in `onPrimaryContainer`, action inherits | `sys.color.background.primary` at 40% (soft blue) | Asides worth pulling the eye toward — new-feature explainers, capability nudges. |
|
|
214
|
+
| `accent` + `neutralBody` | `sys.color.background.selected` | title + body in `onSurface`, action steps to `sys.color.background.primary` | `sys.color.background.primary` at 40% (soft blue) | Accent tint pulls the eye, but the copy stays quiet, high-legibility body text — longer explainers, dense asides. |
|
|
215
|
+
| `destructive` | `sys.color.background.danger` | body in `onErrorContainer`, action inherits | `sys.color.text.danger` at 40% | Blocking errors or rejections — failed approvals, outages, billing. |
|
|
167
216
|
|
|
168
217
|
## Slots
|
|
169
218
|
|
|
@@ -175,6 +224,7 @@ Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner c
|
|
|
175
224
|
- **body** — explanation copy. `body.sm` / Regular / inherits container foreground. Required.
|
|
176
225
|
- **action** *(optional)* — follow-through link below the body. `label.md` / Semibold / underlined.
|
|
177
226
|
- **trailingIcon** *(optional)* — 16 × 16 glyph at the trailing edge, vertically centred against the container. Paints in `currentColor`.
|
|
227
|
+
- **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`.
|
|
178
228
|
|
|
179
229
|
## Anatomy
|
|
180
230
|
|
|
@@ -186,8 +236,9 @@ Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner c
|
|
|
186
236
|
| content | Flex column, `flex: 1 1 auto`, `sys.layout.stack.xs` (8) body↔action gap, `sys.layout.stack.2xs` (4) title↔body gap |
|
|
187
237
|
| title | `sys.typo.label.md` (14 / Semibold 600), color inherits |
|
|
188
238
|
| body | `sys.typo.body.sm` (14 / Regular), color inherits |
|
|
189
|
-
| action | `sys.typo.label.md` (14 / Semibold), underlined. Steps to `sys.color.primary` in `default`; inherits in `accent` / `destructive`. |
|
|
239
|
+
| action | `sys.typo.label.md` (14 / Semibold), underlined. Steps to `sys.color.background.primary` in `default`; inherits in `accent` / `destructive`. |
|
|
190
240
|
| trailingIcon | `sys.icon.md` (16 × 16) glyph, `align-self: center` against the container, `color: currentColor` |
|
|
241
|
+
| trailingAction | [Text Button](../button/text.md) (`<Button variant="text">`), `flex: 0 0 auto`, `align-self: center`. Size + appearance owned by the Button; default appearance to the banner's colour family. Wins the slot over `trailingIcon` |
|
|
191
242
|
|
|
192
243
|
## States
|
|
193
244
|
|
|
@@ -19,7 +19,14 @@
|
|
|
19
19
|
"type": "boolean",
|
|
20
20
|
"optional": true,
|
|
21
21
|
"default": false,
|
|
22
|
-
"description": "Paints a `sys.borderWidth.hairline` (1) inset stroke around the container, toned to the appearance's color family and kept deliberately faint so it reads as a soft edge of the same tint, not a frame — the subtle gray hairline (`sys.color.
|
|
22
|
+
"description": "Paints a `sys.borderWidth.hairline` (1) inset stroke around the container, toned to the appearance's color family and kept deliberately faint so it reads as a soft edge of the same tint, not a frame — the subtle gray hairline (`sys.color.border.default`) on `default`'s gray-tinted fill, `primary` at 40% (`color-mix(sys.color.background.primary, 40%)`) on `accent`'s blue-tinted fill, `error` at 40% on `destructive`. Rendered as an inset box-shadow, never a real border, so toggling it cannot change the banner's footprint (see DESIGN.md → Border & Stroke). Reach for it when the tinted fill alone doesn't separate the banner from its host surface."
|
|
23
|
+
},
|
|
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.text.default`) and steps the action to `sys.color.background.primary` — i.e. the **Default appearance's** foreground treatment laid over the accent fill, decoupling the background tone from the text tone. Reach for it when the `primaryContainer` tint should still pull the eye but the copy should read as quiet, high-legibility body text rather than tonal `onPrimaryContainer` primary-family text (long-form explainers, dense asides). No effect on `default` (already `onSurface`) or `destructive` (the warning tone must carry through the copy)."
|
|
23
30
|
},
|
|
24
31
|
"title": {
|
|
25
32
|
"type": "node",
|
|
@@ -46,6 +53,11 @@
|
|
|
46
53
|
"optional": true,
|
|
47
54
|
"description": "{ label, href? , onClick? } — a follow-through link rendered as a block child below the body."
|
|
48
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
|
+
},
|
|
49
61
|
"children": {
|
|
50
62
|
"type": "node",
|
|
51
63
|
"required": true,
|
|
@@ -104,6 +116,13 @@
|
|
|
104
116
|
"accepts": [
|
|
105
117
|
"icon"
|
|
106
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
|
+
]
|
|
107
126
|
}
|
|
108
127
|
},
|
|
109
128
|
"sizing": {
|
|
@@ -138,33 +157,37 @@
|
|
|
138
157
|
},
|
|
139
158
|
"appearances": {
|
|
140
159
|
"default": {
|
|
141
|
-
"background": "sys.color.
|
|
142
|
-
"foreground": "sys.color.
|
|
143
|
-
"actionColor": "sys.color.
|
|
144
|
-
"outlineColor": "sys.color.
|
|
145
|
-
"note": "Body sits in `onSurface`; the action link steps to primary so it carries the only chromatic emphasis. Background is the translucent inverse-tone scrim (`sys.color.
|
|
160
|
+
"background": "sys.color.background.neutral",
|
|
161
|
+
"foreground": "sys.color.text.default",
|
|
162
|
+
"actionColor": "sys.color.text.link",
|
|
163
|
+
"outlineColor": "sys.color.border.default",
|
|
164
|
+
"note": "Body sits in `onSurface`; the action link steps to primary so it carries the only chromatic emphasis. Background is the translucent inverse-tone scrim (`sys.color.background.neutral` — ~8% black light / ~8% white dark) so the banner stays harmonious on any underlying surface — body, raised card, BottomSheet, Dialog — by tinting one step darker (light mode) or lighter (dark mode) instead of pinning to a fixed neutral step that can collide with the surface ladder. Same scrim used by Chip / Tag default, Progress track, StatusTag neutral, and Skeleton."
|
|
146
165
|
},
|
|
147
166
|
"accent": {
|
|
148
|
-
"background": "sys.color.
|
|
149
|
-
"foreground": "sys.color.
|
|
167
|
+
"background": "sys.color.background.selected",
|
|
168
|
+
"foreground": "sys.color.text.link",
|
|
150
169
|
"actionColor": "inherit",
|
|
151
|
-
"outlineColor": "color-mix(sys.color.primary, 40%)",
|
|
152
|
-
"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.background.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."
|
|
153
172
|
},
|
|
154
173
|
"destructive": {
|
|
155
|
-
"background": "sys.color.
|
|
156
|
-
"foreground": "sys.color.
|
|
174
|
+
"background": "sys.color.background.danger",
|
|
175
|
+
"foreground": "sys.color.text.danger",
|
|
157
176
|
"actionColor": "inherit",
|
|
158
|
-
"outlineColor": "color-mix(sys.color.
|
|
177
|
+
"outlineColor": "color-mix(sys.color.text.danger, 40%)",
|
|
159
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."
|
|
160
179
|
}
|
|
161
180
|
},
|
|
162
181
|
"behavior": {
|
|
163
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`.",
|
|
164
185
|
"role": "Container carries role='note' so screen readers announce the banner as an aside."
|
|
165
186
|
},
|
|
166
187
|
"forbidden": [
|
|
167
|
-
"
|
|
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",
|
|
190
|
+
"default banner background painted with a brand-tinted fill — informational banners use sys.color.background.selected; promotional banners use sys.color.surface.sunken",
|
|
168
191
|
"banner thumbnail slot omitted when banner role carries imagery — empty image area is forbidden, fall back to /placeholder.png",
|
|
169
192
|
"banner used for transient confirmations — that role is the `toast` family (locked)",
|
|
170
193
|
"banner CTA rendered as raw <a> / <button> — use button/text inside the action slot",
|
|
@@ -33,9 +33,7 @@ const [open, setOpen] = useState(false);
|
|
|
33
33
|
</>
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
##
|
|
37
|
-
|
|
38
|
-
### Overflow
|
|
36
|
+
## Overflow
|
|
39
37
|
|
|
40
38
|
When content exceeds the card's `max-height`, the content slot scrolls internally — handle and actions stay pinned, footer gains its `is-elevated` upward shadow.
|
|
41
39
|
|
|
@@ -62,7 +60,7 @@ const [open, setOpen] = useState(false);
|
|
|
62
60
|
</>
|
|
63
61
|
```
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
## Keyboard
|
|
66
64
|
|
|
67
65
|
When the sheet hosts an input that summons the virtual keyboard, the card lifts above the keyboard's top edge so the actions footer stays reachable. Handle and footer pinned; content scrolls to keep the focused input in view.
|
|
68
66
|
|
|
@@ -88,7 +86,7 @@ const [open, setOpen] = useState(false);
|
|
|
88
86
|
</>
|
|
89
87
|
```
|
|
90
88
|
|
|
91
|
-
|
|
89
|
+
## Nested step
|
|
92
90
|
|
|
93
91
|
The sheet can host a **drill-in step** without spawning a second modal. The consumer swaps title, content, and primary action between renders; passing `onBack` paints a leading back chevron — an Icon Button rendering `ChevronLeftIcon` at `sys.icon.lg` (24), with `sys.layout.inline.md` (8) between glyph and title. Card chrome, scrim, drag handle, and actions footer stay identical across steps.
|
|
94
92
|
|
|
@@ -151,7 +149,7 @@ const [value, setValue] = useState('');
|
|
|
151
149
|
| drag handle | 48 × 4px pill, `onSurfaceVariant @ 40%`, `sys.radius.full`, 8px vertical gutter |
|
|
152
150
|
| content | Flex column, 16px padding, 16px between children, vertical scroll on overflow |
|
|
153
151
|
| title | `sys.typo.heading.lg` (24 / Semibold), `onSurface` |
|
|
154
|
-
| back chevron | [Icon Button](../button/icon.md) → `ChevronLeftIcon` at `sys.icon.lg` (24), `sys.color.
|
|
152
|
+
| back chevron | [Icon Button](../button/icon.md) → `ChevronLeftIcon` at `sys.icon.lg` (24), `sys.color.text.default`. Glyph aligns to the title's leading edge via Icon Button optical-alignment. `sys.layout.inline.md` (8) glyph → title gap. |
|
|
155
153
|
| body | `sys.typo.body.md` (16 / Regular), `onSurfaceVariant` |
|
|
156
154
|
| actions | Flex column, 8px between buttons, 16px padding on all four sides |
|
|
157
155
|
| primary CTA | [Button](../button/button.md) `appearance="primary"`, `size="large"`, `fullWidth` |
|
|
@@ -104,14 +104,14 @@
|
|
|
104
104
|
},
|
|
105
105
|
"sizing": {
|
|
106
106
|
"scrimTint": "ref.palette.black.600",
|
|
107
|
-
"containerFill": "sys.color.
|
|
107
|
+
"containerFill": "sys.color.surface.default",
|
|
108
108
|
"containerRadiusTop": "sys.radius.xl",
|
|
109
109
|
"containerRadiusBottom": "0",
|
|
110
110
|
"elevation": "sys.elevation.sheet",
|
|
111
111
|
"maxWidth": "480px",
|
|
112
112
|
"maxHeight": "90vh",
|
|
113
113
|
"dragHandleSize": "48 × 4px",
|
|
114
|
-
"dragHandleFill": "sys.color.
|
|
114
|
+
"dragHandleFill": "sys.color.text.subtle @ 40%",
|
|
115
115
|
"dragHandleRadius": "sys.radius.full",
|
|
116
116
|
"dragHandleGutter": "sys.layout.container.xs",
|
|
117
117
|
"contentPadding": "sys.layout.container.md",
|
|
@@ -119,13 +119,13 @@
|
|
|
119
119
|
"actionsStackGap": "sys.layout.stack.xs",
|
|
120
120
|
"actionsPadding": "sys.layout.container.md",
|
|
121
121
|
"titleTypo": "sys.typo.heading.lg",
|
|
122
|
-
"titleColor": "sys.color.
|
|
122
|
+
"titleColor": "sys.color.text.default",
|
|
123
123
|
"backIcon": "ChevronLeftIcon",
|
|
124
124
|
"backIconSize": "sys.icon.lg",
|
|
125
|
-
"backIconColor": "sys.color.
|
|
125
|
+
"backIconColor": "sys.color.icon.default",
|
|
126
126
|
"backToTitleGap": "sys.layout.inline.md",
|
|
127
127
|
"bodyTypo": "sys.typo.body.md",
|
|
128
|
-
"bodyColor": "sys.color.
|
|
128
|
+
"bodyColor": "sys.color.text.subtle"
|
|
129
129
|
},
|
|
130
130
|
"states": {
|
|
131
131
|
"open": {
|
|
@@ -8,7 +8,7 @@ A small persistent annotation pill with a tail pointing at an anchor — a chat-
|
|
|
8
8
|
|
|
9
9
|
**Layout inset.** `inline` — the bubble ships no positioning. The host anchors it to the target element by *visual alignment* (CSS anchor positioning, or a positioned wrapper around the anchor): the tail's TIP sits flush on the anchor's content edge (padding excluded) — the bubble body is set back from that edge by the tail's own protrusion (`ref.space.50 / √2`) so the tail meets the anchor cleanly rather than overlapping it — and the bubble centres on the anchor's visual centre so the tail sits at the anchor's bottom-centre. The bubble caps its own `max-width` so it always keeps an 8-token margin from every viewport edge; position-clamping is the host's job. Only when centring would push the bubble past that safe margin does the host shift it inward and flip `tailAlign` so the tail still points at the anchor — `start` for left-edge anchors, `end` for right-edge anchors, `center` whenever the anchor has room on both sides (the default).
|
|
10
10
|
|
|
11
|
-
**Colour tuning.** Default fill `sys.color.primary` / label `sys.color.
|
|
11
|
+
**Colour tuning.** Default fill `sys.color.background.primary` / label `sys.color.text.onFill` — both theme-stable. Operations re-tint per campaign by setting `--bubble-fill` and `--bubble-ink` on inline style; the tail's `background: inherit` follows automatically.
|
|
12
12
|
|
|
13
13
|
## Default
|
|
14
14
|
|
|
@@ -20,9 +20,7 @@ import { Bubble } from '@teamblind-chorus/ui';
|
|
|
20
20
|
<Bubble>5 new messages + gift</Bubble>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
### Anchored to a top-bar icon
|
|
23
|
+
## Anchored icon
|
|
26
24
|
|
|
27
25
|
A [Navigation bar (home)](../navigation-bar/main.md) with three trailing actions, bubble anchored to the chat glyph itself. The glyph carries `anchor-name: --chat-icon`; the bubble pins to the glyph's bottom — padding excluded — set back by the tail's own protrusion (`top: calc(anchor(bottom) + var(--bubble-tail-protrusion))`, the system token = `ref.space.50 / √2`) so the tail's top vertex lands *flush* on the glyph's bottom edge rather than poking into it, and centres on the glyph's visual centre (`left: anchor(center)` + `translateX(-50%)`). The tail tip thus sits on the chat icon's bottom-centre regardless of where the bar reflows — no hardcoded pixel offsets, `tailAlign="center"`.
|
|
28
26
|
|
|
@@ -70,7 +68,7 @@ import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
|
|
|
70
68
|
</div>
|
|
71
69
|
```
|
|
72
70
|
|
|
73
|
-
|
|
71
|
+
## Tail alignment
|
|
74
72
|
|
|
75
73
|
Three tail positions stacked so the offset reads at a glance — pick by where the anchor sits.
|
|
76
74
|
|
|
@@ -86,7 +84,7 @@ import { Bubble } from '@teamblind-chorus/ui';
|
|
|
86
84
|
</div>
|
|
87
85
|
```
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
## Operations re-tint
|
|
90
88
|
|
|
91
89
|
Brand red instead of primary blue — the tail inherits the fill, so a single declaration covers both surfaces.
|
|
92
90
|
|
|
@@ -95,12 +93,12 @@ bubble/recoloured
|
|
|
95
93
|
---
|
|
96
94
|
import { Bubble } from '@teamblind-chorus/ui';
|
|
97
95
|
|
|
98
|
-
<Bubble style={{ '--bubble-fill': 'var(--sys-color-brand)', '--bubble-ink': 'var(--sys-color-
|
|
96
|
+
<Bubble style={{ '--bubble-fill': 'var(--sys-color-text-brand)', '--bubble-ink': 'var(--sys-color-text-onFill)' }}>
|
|
99
97
|
Free daily tarot
|
|
100
98
|
</Bubble>
|
|
101
99
|
```
|
|
102
100
|
|
|
103
|
-
|
|
101
|
+
## Long copy
|
|
104
102
|
|
|
105
103
|
Copy that exceeds the host width truncates with an ellipsis. If the message can't fit on one line, it belongs in a [Banner](../banner/banner.md) instead.
|
|
106
104
|
|
|
@@ -124,8 +122,8 @@ import { Bubble } from '@teamblind-chorus/ui';
|
|
|
124
122
|
|
|
125
123
|
| Slot | Token bindings |
|
|
126
124
|
|-----------|----------------|
|
|
127
|
-
| container | Fill `--bubble-fill` (default `sys.color.primary`), ink `--bubble-ink` (default `sys.color.
|
|
128
|
-
| body | `sys.typo.
|
|
125
|
+
| container | Fill `--bubble-fill` (default `sys.color.background.primary`), ink `--bubble-ink` (default `sys.color.text.onFill`), `sys.layout.container.2xs` padding-block, `ref.space.75` padding-inline, `sys.radius.full`, viewport-safe `max-width` cap |
|
|
126
|
+
| body | `sys.typo.label.xs` |
|
|
129
127
|
| tail | `ref.space.50` square, rotated 45° |
|
|
130
128
|
|
|
131
129
|
## Behavior
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "../../spec.schema.json",
|
|
3
3
|
"name": "Bubble",
|
|
4
4
|
"family": "bubble",
|
|
5
|
-
"description": "Always-on annotation bubble — a pill-shaped label with a small tail that points at the anchor UI element. Ships with a default brand-blue fill (`sys.color.primary` / `sys.color.
|
|
5
|
+
"description": "Always-on annotation bubble — a pill-shaped label with a small tail that points at the anchor UI element. Ships with a default brand-blue fill (`sys.color.background.primary` / `sys.color.text.onFill` — both theme-stable so the bubble reads identically in light and dark mode) and exposes two CSS custom properties (`--bubble-fill`, `--bubble-ink`) so operations can re-tint per campaign without forking the component. Distinct from Tooltip on three axes: (1) persistent rather than transient — Bubble stays in view as part of the UI's resting state; (2) lower visual priority — no elevation shadow, smaller padding, single-line truncation; (3) does NOT occlude its neighbours — the host positions it inline next to the anchor, never as a portal-mounted overlay.",
|
|
6
6
|
"element": "div",
|
|
7
7
|
"props": {
|
|
8
8
|
"children": {
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"slots": {
|
|
37
37
|
"container": {
|
|
38
38
|
"required": true,
|
|
39
|
-
"description": "The pill body. `position: relative` so the tail can pin to its edge; `display: inline-flex` so the bubble shrink-wraps its body. Carries the fill (`--bubble-fill` defaulting to `sys.color.primary`), ink (`--bubble-ink` defaulting to `sys.color.
|
|
39
|
+
"description": "The pill body. `position: relative` so the tail can pin to its edge; `display: inline-flex` so the bubble shrink-wraps its body. Carries the fill (`--bubble-fill` defaulting to `sys.color.background.primary`), ink (`--bubble-ink` defaulting to `sys.color.text.onFill`), 4 / 6 padding, and pill radius. `role='note'` so the annotation reads as supplementary rather than as a main UI control.",
|
|
40
40
|
"intrinsic": true
|
|
41
41
|
},
|
|
42
42
|
"body": {
|
|
43
43
|
"required": true,
|
|
44
|
-
"description": "Bubble copy in `sys.typo.
|
|
44
|
+
"description": "Bubble copy in `sys.typo.label.xs` (10 / Semibold). Single line — `white-space: nowrap` + `text-overflow: ellipsis` truncate overflow rather than wrap. Inherits the container's ink colour.",
|
|
45
45
|
"accepts": ["text"]
|
|
46
46
|
},
|
|
47
47
|
"tail": {
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"sizing": {
|
|
54
|
-
"background": "sys.color.primary",
|
|
55
|
-
"foreground": "sys.color.
|
|
56
|
-
"labelTypo": "sys.typo.
|
|
54
|
+
"background": "sys.color.background.primary",
|
|
55
|
+
"foreground": "sys.color.text.onFill",
|
|
56
|
+
"labelTypo": "sys.typo.label.xs",
|
|
57
57
|
"paddingBlock": "sys.layout.container.2xs",
|
|
58
58
|
"paddingInline": "ref.space.75",
|
|
59
59
|
"radius": "sys.radius.full",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"viewportSafeArea": "sys.layout.container.xs"
|
|
63
63
|
},
|
|
64
64
|
"appearance": {
|
|
65
|
-
"background": "sys.color.primary",
|
|
66
|
-
"foreground": "sys.color.
|
|
67
|
-
"note": "Single canonical appearance — Bubble has no `default` / `accent` / `destructive` axis. Default fill is `sys.color.primary` and label is `sys.color.
|
|
65
|
+
"background": "sys.color.background.primary",
|
|
66
|
+
"foreground": "sys.color.text.onFill",
|
|
67
|
+
"note": "Single canonical appearance — Bubble has no `default` / `accent` / `destructive` axis. Default fill is `sys.color.background.primary` and label is `sys.color.text.onFill`; both are theme-stable so the bubble reads identically in light and dark mode. Operations re-tint by setting `--bubble-fill` and `--bubble-ink` on the bubble's inline style or a wrapping class — the tail inherits the fill via `background: inherit`, so a colour swap on the container covers the whole bubble in one declaration. Re-tints should keep the contrast above WCAG AA against `--bubble-ink` (4.5:1); the system does not enforce this at runtime."
|
|
68
68
|
},
|
|
69
69
|
"behavior": {
|
|
70
70
|
"ariaRole": "Container carries `role='note'` so screen readers announce the bubble as a supplementary note rather than as a main UI control. The decorative tail carries `aria-hidden='true'`.",
|
|
@@ -74,12 +74,12 @@
|
|
|
74
74
|
"zeroGapToAnchor": "**The tail's TIP sits flush on the anchor's CONTENT edge (padding excluded) — no overlap, no gap.** This is the canonical home of the set-back math. Align to the anchor's content box, not its padding box: the tail-bearing edge (top for `tailSide='top'`, bottom for `tailSide='bottom'`) is set BACK from the anchor by the tail's own protrusion so the tail's outer vertex lands exactly on the anchor's content edge. The tail is a `ref.space.50` (4) square rotated 45°, so its vertex pokes `ref.space.50 / √2` ≈ 2.83 past the bubble edge; the system exposes this at :root as `--bubble-tail-protrusion`, and the host adds exactly that set-back (e.g. `top: calc(anchor(bottom) + var(--bubble-tail-protrusion))`). Overlapping the anchor reads as a collision; a gap larger than the protrusion leaves the tail unattached. For more breathing room, widen the *anchor's* padding — never enlarge the set-back. This √2 set-back is the single derived constant in the contract; the horizontal axis is pure visual alignment.",
|
|
75
75
|
"tailAlignSelection": "`tailAlign='center'` is the default and preferred case — the bubble centres on the anchor so the tail sits dead-centre and its tip falls on the anchor's centreX by construction (see tailTipCentredOnAnchor). It flips off-centre only as an edge fallback: when the anchor sits so near a viewport edge that centring would push the bubble past the 8-token safe margin, the bubble shifts AWAY from the edge and `tailAlign` flips to the SAME side as the anchor — `end` at the right edge (bubble extends leftward), `start` at the left edge (bubble extends rightward). In every case the bubble's nearest edge stays ≥8 from the viewport edge and the tip stays on the anchor's centreline; visibility is the contract, tail position is the lever.",
|
|
76
76
|
"positioning": "Bubble is presentational — the *host* owns positioning, and the canonical pattern binds bubble to anchor by VISUAL ALIGNMENT (CSS anchor positioning, or a positioned wrapper around the anchor) rather than DOM proximity, so the tail tracks the anchor through reflows. Four coupled decisions, each detailed in its own rule: (a) `tailSide` — `top` when the bubble sits below the anchor, `bottom` when above; (b) the vertical set-back so the tail tip is flush on the anchor's content edge — see zeroGapToAnchor; (c) horizontal centring so the tip lands on the anchor's centreX — see tailTipCentredOnAnchor; (d) `tailAlign`, `center` by default and flipped only at a viewport edge — see tailAlignSelection. Get any one wrong and the tail points at empty space.",
|
|
77
|
-
"colorTuning": "Two CSS custom properties — `--bubble-fill` (background) and `--bubble-ink` (label) — are the supported runtime override surface. Default to `sys.color.primary` / `sys.color.
|
|
77
|
+
"colorTuning": "Two CSS custom properties — `--bubble-fill` (background) and `--bubble-ink` (label) — are the supported runtime override surface. Default to `sys.color.background.primary` / `sys.color.text.onFill`. Operations campaigns set them on inline style; static themes set them on a wrapper class. The tail's `background: inherit` follows automatically."
|
|
78
78
|
},
|
|
79
79
|
"forbidden": [
|
|
80
80
|
"bubble used as a transient hover/focus tooltip — that role is the `tooltip` family; bubble must remain in view at the UI's resting state",
|
|
81
81
|
"bubble portal-mounted at document root with a fixed z-index that overlays surrounding chrome — bubble must NEVER occlude neighbour elements; that contract is what separates it from Tooltip",
|
|
82
|
-
"bubble painted with an elevation shadow (`sys.elevation.
|
|
82
|
+
"bubble painted with an elevation shadow (`sys.elevation.floating` / `overlay` / `sheet`) — the lift signal is what makes Tooltip 'jump'; Bubble is intentionally flat",
|
|
83
83
|
"bubble copy that wraps to multiple lines — overflow truncates with an ellipsis rather than wrapping; if the message can't fit on one line, it belongs in a Banner or a Tooltip with action",
|
|
84
84
|
"bubble retinted with hardcoded hex / rgba on `background` or `color` instead of `--bubble-fill` / `--bubble-ink` — campaign tunes must route through the documented custom-property surface so the tail colour tracks the body",
|
|
85
85
|
"bubble used to convey *required* meaning (error blocking a submit, account-locked notice) — that role is Banner / Dialog; bubble is a soft annotation only",
|
|
@@ -20,7 +20,7 @@ Transparent-rest forms ([Icon Button](./icon.md), [Text Button](./text.md), [Ter
|
|
|
20
20
|
|
|
21
21
|
### Focus ring
|
|
22
22
|
|
|
23
|
-
All Button family components draw the same outward
|
|
23
|
+
All Button family components draw the same outward single ring as a `position: absolute` pseudo-element — never a layout-affecting border. Suppressed while `disabled`. Trigger: `:focus-visible`. See [DESIGN.md → Focus ring composition](../../DESIGN.md#focus-ring-composition).
|
|
24
24
|
|
|
25
25
|
## Sub-components
|
|
26
26
|
|
|
@@ -35,11 +35,9 @@ function Demo() {
|
|
|
35
35
|
<Demo />
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Accent
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Brand-blue label — `sys.color.primary`. Use sparingly; never two accent Check Buttons in the same row.
|
|
40
|
+
Brand-blue label — `sys.color.background.primary`. Use sparingly; never two accent Check Buttons in the same row.
|
|
43
41
|
|
|
44
42
|
```preview
|
|
45
43
|
button/check/accent
|
|
@@ -51,9 +49,9 @@ import { Button } from '@teamblind-chorus/ui';
|
|
|
51
49
|
</Button>
|
|
52
50
|
```
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
## Inverse
|
|
55
53
|
|
|
56
|
-
Mirror for inverse hosts (Toast, coach-mark, snackbar). Label paints `sys.color.
|
|
54
|
+
Mirror for inverse hosts (Toast, coach-mark, snackbar). Label paints `sys.color.text.inverse` against the host's `inverseSurface` fill.
|
|
57
55
|
|
|
58
56
|
```preview
|
|
59
57
|
button/check/inverse
|
|
@@ -65,7 +63,7 @@ import { Button } from '@teamblind-chorus/ui';
|
|
|
65
63
|
</Button>
|
|
66
64
|
```
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
## Checked
|
|
69
67
|
|
|
70
68
|
Same row with `checked={true}` — checkbox glyph flips to the filled square. State overlays follow the label color.
|
|
71
69
|
|
|
@@ -79,7 +77,7 @@ import { Button } from '@teamblind-chorus/ui';
|
|
|
79
77
|
</Button>
|
|
80
78
|
```
|
|
81
79
|
|
|
82
|
-
|
|
80
|
+
## Middle icon
|
|
83
81
|
|
|
84
82
|
Optional 16px icon between checkbox and label. Use sparingly — most rows don't need it. Canonical case: an item-use row where the middle glyph names the item being consumed.
|
|
85
83
|
|
|
@@ -100,9 +98,9 @@ Three appearances. `default` is the base neutral toggle; `accent` paints the lab
|
|
|
100
98
|
|
|
101
99
|
| Appearance | Background (rest) | Label color | When to reach for it |
|
|
102
100
|
|-----------|-------------------|------------------------------|-----------------------------------------------------------------------|
|
|
103
|
-
| `default` | `transparent` | `sys.color.
|
|
104
|
-
| `accent` | `transparent` | `sys.color.primary` | One option per row that needs commit-rank emphasis. |
|
|
105
|
-
| `inverse` | `transparent` | `sys.color.
|
|
101
|
+
| `default` | `transparent` | `sys.color.text.subtle` | The base neutral toggle — option rows next to a primary commit. |
|
|
102
|
+
| `accent` | `transparent` | `sys.color.background.primary` | One option per row that needs commit-rank emphasis. |
|
|
103
|
+
| `inverse` | `transparent` | `sys.color.text.inverse` | Inside an inverse host (Toast, coach-mark, snackbar). |
|
|
106
104
|
|
|
107
105
|
## Slots
|
|
108
106
|
|
|
@@ -92,19 +92,19 @@
|
|
|
92
92
|
"default": {
|
|
93
93
|
"background": "transparent",
|
|
94
94
|
"border": null,
|
|
95
|
-
"label": "sys.color.
|
|
95
|
+
"label": "sys.color.text.subtle",
|
|
96
96
|
"note": "The base neutral inline toggle — the canonical Check Button. Quiet enough to live next to typographic content without claiming commit-rank attention."
|
|
97
97
|
},
|
|
98
98
|
"accent": {
|
|
99
99
|
"background": "transparent",
|
|
100
100
|
"border": null,
|
|
101
|
-
"label": "sys.color.
|
|
101
|
+
"label": "sys.color.text.link",
|
|
102
102
|
"note": "Brand-blue label for the inline toggle. Use sparingly — never two `accent` Check Buttons in the same row."
|
|
103
103
|
},
|
|
104
104
|
"inverse": {
|
|
105
105
|
"background": "transparent",
|
|
106
106
|
"border": null,
|
|
107
|
-
"label": "sys.color.
|
|
107
|
+
"label": "sys.color.text.inverse",
|
|
108
108
|
"note": "Mirror for use inside an inverse host (Toast, coach-mark, snackbar). Label paints in `inverseOnSurface` so it reads against the host's `inverseSurface` fill; state overlays mix from the same token so the recipe carries over without per-host tuning."
|
|
109
109
|
}
|
|
110
110
|
},
|
|
@@ -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.border.focused"
|
|
138
|
+
},
|
|
139
|
+
"outerRing": {
|
|
140
|
+
"width": "sys.borderWidth.thin",
|
|
141
|
+
"color": "sys.color.border.focused"
|
|
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",
|
|
@@ -134,16 +153,14 @@
|
|
|
134
153
|
"focusIndicator": {
|
|
135
154
|
"description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the button is in. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
|
|
136
155
|
"composition": "outward",
|
|
137
|
-
"compositionReason": "Action affordance with breathing room around it; the
|
|
156
|
+
"compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
|
|
138
157
|
"overlay": {
|
|
139
158
|
"color": "label",
|
|
140
159
|
"opacity": "sys.state.focus"
|
|
141
160
|
},
|
|
142
161
|
"ring": {
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
146
|
-
"insetColor": "sys.color.focusInset"
|
|
162
|
+
"width": "sys.borderWidth.hairline",
|
|
163
|
+
"color": "sys.color.border.focused"
|
|
147
164
|
},
|
|
148
165
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
149
166
|
},
|