@teamblind-chorus/ui 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/agents/AGENTS.md +6 -6
- package/agents/DESIGN.md +245 -244
- package/agents/LOVABLE.md +40 -11
- package/agents/catalog.md +4 -4
- package/agents/components/avatar-rail/avatar-rail.md +2 -4
- package/agents/components/avatar-rail/avatar-rail.spec.json +10 -14
- package/agents/components/badge/role.md +7 -9
- package/agents/components/badge/role.spec.json +6 -6
- package/agents/components/badge/update.md +6 -8
- package/agents/components/badge/update.spec.json +5 -5
- package/agents/components/banner/banner.md +16 -18
- package/agents/components/banner/banner.spec.json +14 -14
- package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
- package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
- package/agents/components/bubble/bubble.md +8 -10
- package/agents/components/bubble/bubble.spec.json +11 -11
- package/agents/components/button/button.md +1 -1
- package/agents/components/button/check.md +9 -11
- package/agents/components/button/check.spec.json +8 -10
- package/agents/components/button/fab.md +7 -9
- package/agents/components/button/fab.spec.json +10 -12
- package/agents/components/button/group.spec.json +4 -4
- package/agents/components/button/icon.md +21 -23
- package/agents/components/button/icon.spec.json +12 -14
- package/agents/components/button/standard.md +40 -42
- package/agents/components/button/standard.spec.json +20 -22
- package/agents/components/button/text.md +21 -23
- package/agents/components/button/text.spec.json +13 -15
- package/agents/components/button/toggle.md +7 -9
- package/agents/components/button/toggle.spec.json +10 -12
- package/agents/components/button/toolbar.md +24 -26
- package/agents/components/button/toolbar.spec.json +10 -12
- package/agents/components/carousel/carousel.md +1 -1
- package/agents/components/carousel/post.md +15 -21
- package/agents/components/carousel/post.spec.json +17 -17
- package/agents/components/carousel/profile.md +9 -45
- package/agents/components/carousel/profile.spec.json +17 -17
- package/agents/components/chip/chip.md +1 -1
- package/agents/components/chip/filter.md +22 -24
- package/agents/components/chip/filter.spec.json +17 -13
- package/agents/components/chip/tag.md +22 -24
- package/agents/components/chip/tag.spec.json +19 -15
- package/agents/components/dialog/dialog.md +1 -3
- package/agents/components/dialog/dialog.spec.json +3 -3
- package/agents/components/directory-list/directory-list.md +1 -3
- package/agents/components/directory-list/directory-list.spec.json +2 -2
- package/agents/components/divider/divider.family.json +1 -1
- package/agents/components/divider/divider.md +12 -14
- package/agents/components/divider/divider.spec.json +8 -8
- package/agents/components/empty-state/empty-state.md +9 -9
- package/agents/components/empty-state/empty-state.spec.json +14 -14
- package/agents/components/feed/ad.md +2 -4
- package/agents/components/feed/ad.spec.json +10 -10
- package/agents/components/feed/post.md +41 -43
- package/agents/components/feed/post.spec.json +35 -39
- package/agents/components/form-field/form-field.md +1 -1
- package/agents/components/form-field/input.md +32 -34
- package/agents/components/form-field/input.spec.json +34 -33
- package/agents/components/form-field/search.md +2 -4
- package/agents/components/form-field/search.spec.json +19 -18
- package/agents/components/form-field/select.md +18 -20
- package/agents/components/form-field/select.spec.json +30 -29
- package/agents/components/form-field/textarea.md +3 -5
- package/agents/components/form-field/textarea.spec.json +32 -31
- package/agents/components/header/main.md +4 -6
- package/agents/components/header/main.spec.json +3 -3
- package/agents/components/header/sub.md +6 -8
- package/agents/components/header/sub.spec.json +3 -3
- package/agents/components/list/accordion.md +34 -45
- package/agents/components/list/accordion.spec.json +20 -20
- package/agents/components/list/entry.md +59 -81
- package/agents/components/list/entry.spec.json +20 -23
- package/agents/components/list/list.md +2 -2
- package/agents/components/list/radio.md +13 -20
- package/agents/components/list/radio.spec.json +16 -20
- package/agents/components/list/standard.md +50 -72
- package/agents/components/list/standard.spec.json +18 -21
- package/agents/components/metadata/compact.md +4 -6
- package/agents/components/metadata/compact.spec.json +6 -6
- package/agents/components/metadata/metadata.md +1 -1
- package/agents/components/metadata/standard.md +12 -14
- package/agents/components/metadata/standard.spec.json +10 -10
- package/agents/components/nav-card/nav-card.md +25 -27
- package/agents/components/nav-card/nav-card.spec.json +19 -19
- package/agents/components/nav-list/nav-list.md +2 -8
- package/agents/components/nav-list/nav-list.spec.json +3 -3
- package/agents/components/navigation-bar/main.md +9 -11
- package/agents/components/navigation-bar/main.spec.json +6 -6
- package/agents/components/navigation-bar/search.md +6 -8
- package/agents/components/navigation-bar/search.spec.json +9 -9
- package/agents/components/navigation-bar/sub.md +9 -11
- package/agents/components/navigation-bar/sub.spec.json +7 -7
- package/agents/components/pagination/pagination.family.json +1 -1
- package/agents/components/pagination/pagination.md +3 -3
- package/agents/components/pagination/pagination.spec.json +5 -5
- package/agents/components/profile-header/profile-header.md +9 -11
- package/agents/components/profile-header/profile-header.spec.json +9 -9
- package/agents/components/progress/progress.family.json +1 -1
- package/agents/components/progress/progress.md +5 -5
- package/agents/components/progress/progress.spec.json +8 -8
- package/agents/components/side-sheet/side-sheet.md +11 -13
- package/agents/components/side-sheet/side-sheet.spec.json +3 -3
- package/agents/components/skeleton/skeleton.md +7 -9
- package/agents/components/skeleton/skeleton.spec.json +5 -5
- package/agents/components/spinner/spinner.family.json +1 -1
- package/agents/components/spinner/spinner.md +8 -10
- package/agents/components/spinner/spinner.spec.json +9 -9
- package/agents/components/status-tag/status-tag.md +7 -9
- package/agents/components/status-tag/status-tag.spec.json +5 -5
- package/agents/components/suggestion-list/suggestion-list.md +3 -7
- package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
- package/agents/components/switch/switch.md +12 -14
- package/agents/components/switch/switch.spec.json +17 -18
- package/agents/components/tab-bar/tab-bar.md +9 -11
- package/agents/components/tab-bar/tab-bar.spec.json +25 -27
- package/agents/components/tabs/rounded.md +6 -8
- package/agents/components/tabs/rounded.spec.json +17 -15
- package/agents/components/tabs/segmented.md +4 -6
- package/agents/components/tabs/segmented.spec.json +4 -8
- package/agents/components/tabs/underline.md +9 -11
- package/agents/components/tabs/underline.spec.json +14 -16
- package/agents/components/thumbnail/thumbnail.md +5 -7
- package/agents/components/thumbnail/thumbnail.spec.json +8 -8
- package/agents/components/toast/toast.md +5 -7
- package/agents/components/toast/toast.spec.json +3 -3
- package/agents/components/tooltip/tooltip.md +6 -8
- package/agents/components/tooltip/tooltip.spec.json +4 -4
- package/agents/tokens.usage.json +71 -226
- package/dist/index.cjs +212 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +212 -223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +386 -387
- package/eslint/rules.js +7 -7
- package/package.json +2 -3
- package/agents/anti-patterns.md +0 -533
- package/agents/compose.md +0 -240
- package/agents/images.md +0 -66
|
@@ -24,9 +24,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
|
24
24
|
</Tabs>
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
### With icon
|
|
27
|
+
## Leading icon
|
|
30
28
|
|
|
31
29
|
Canonical sort/filter row — each tab pairs a leading glyph (`sys.icon.md`, 16px) with its label. All glyphs draw from `@teamblind-chorus/ui/icons` so the row carries no inline SVG.
|
|
32
30
|
|
|
@@ -44,7 +42,7 @@ import { PulseIcon, StarIcon, HeartIcon, BookmarkIcon } from '@teamblind-chorus/
|
|
|
44
42
|
</Tabs>
|
|
45
43
|
```
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
## Icon only
|
|
48
46
|
|
|
49
47
|
Glyph-only tab — collapses to a clean 32×32 square (inline padding 12 → 8). Requires `aria-label`.
|
|
50
48
|
|
|
@@ -61,7 +59,7 @@ import { StarIcon, BookmarkIcon, HeartIcon } from '@teamblind-chorus/ui/icons';
|
|
|
61
59
|
</Tabs>
|
|
62
60
|
```
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
## Overflow
|
|
65
63
|
|
|
66
64
|
When natural width exceeds the column, the row scrolls horizontally. Trailing edge fade (48px / `ref.space.600`) paints via `mask-image` only while overflow is present.
|
|
67
65
|
|
|
@@ -82,7 +80,7 @@ import { PulseIcon, StarIcon, HeartIcon, BookmarkIcon, TagIcon, ProfileIcon, Men
|
|
|
82
80
|
</Tabs>
|
|
83
81
|
```
|
|
84
82
|
|
|
85
|
-
|
|
83
|
+
## Focused tab
|
|
86
84
|
|
|
87
85
|
Static specimen — pins the focus ring to the selected tab. See top-level [Focus indicator](#focus-indicator).
|
|
88
86
|
|
|
@@ -111,8 +109,8 @@ Selected/unselected pairs are inherited verbatim from [Filter chip's variants](.
|
|
|
111
109
|
|
|
112
110
|
| Prop / state | Container | Label color | Border (always 1px `sys.borderWidth.hairline`) |
|
|
113
111
|
|------------------------|------------------------------------|-----------------------------------|-------------------------------------------------------------------------|
|
|
114
|
-
| **Tab — unselected** | `transparent` | `sys.color.
|
|
115
|
-
| **Tab — selected** | `sys.color.
|
|
112
|
+
| **Tab — unselected** | `transparent` | `sys.color.text.default` | `sys.color.border.default` |
|
|
113
|
+
| **Tab — selected** | `sys.color.background.inverse` | `sys.color.text.inverse` | `transparent` — 1px width held so footprint never changes between states |
|
|
116
114
|
|
|
117
115
|
## Sizes
|
|
118
116
|
|
|
@@ -77,16 +77,16 @@
|
|
|
77
77
|
"selectionStates": {
|
|
78
78
|
"unselected": {
|
|
79
79
|
"background": "transparent",
|
|
80
|
-
"label": "sys.color.
|
|
80
|
+
"label": "sys.color.text.default",
|
|
81
81
|
"border": {
|
|
82
82
|
"width": "sys.borderWidth.hairline",
|
|
83
|
-
"color": "sys.color.
|
|
83
|
+
"color": "sys.color.border.default"
|
|
84
84
|
},
|
|
85
85
|
"note": "Transparent fill so the tab adopts whatever surface sits behind it — page, raised card, sheet — without pinning to a fixed neutral step. Inherited from Filter chip's unselected recipe."
|
|
86
86
|
},
|
|
87
87
|
"selected": {
|
|
88
|
-
"background": "sys.color.
|
|
89
|
-
"label": "sys.color.
|
|
88
|
+
"background": "sys.color.background.inverse",
|
|
89
|
+
"label": "sys.color.text.inverse",
|
|
90
90
|
"border": null
|
|
91
91
|
}
|
|
92
92
|
},
|
|
@@ -116,20 +116,26 @@
|
|
|
116
116
|
"layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
|
|
117
117
|
"innerCounterRing": {
|
|
118
118
|
"width": "sys.borderWidth.hairline",
|
|
119
|
-
"color": "sys.color.
|
|
119
|
+
"color": "sys.color.border.focused"
|
|
120
120
|
},
|
|
121
121
|
"outerRing": {
|
|
122
122
|
"width": "sys.borderWidth.thin",
|
|
123
|
-
"color": "sys.color.
|
|
123
|
+
"color": "sys.color.border.focused"
|
|
124
124
|
}
|
|
125
125
|
},
|
|
126
126
|
"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 tab is in; never via plain mouse click."
|
|
127
127
|
},
|
|
128
128
|
"disabled": {
|
|
129
129
|
"overlay": null,
|
|
130
|
-
"
|
|
130
|
+
"background": "sys.color.background.disabled",
|
|
131
|
+
"label": "sys.color.text.disabled",
|
|
132
|
+
"border": {
|
|
133
|
+
"width": "sys.borderWidth.hairline",
|
|
134
|
+
"color": "sys.color.border.bold"
|
|
135
|
+
},
|
|
131
136
|
"suppressFocusRing": true,
|
|
132
|
-
"cursor": "not-allowed"
|
|
137
|
+
"cursor": "not-allowed",
|
|
138
|
+
"note": "Explicit disabled (no opacity): neutral disabled fill + bold border so the shape reads on any surface, plus disabled label. Overrides the rest/selected appearance."
|
|
133
139
|
}
|
|
134
140
|
},
|
|
135
141
|
"focusIndicator": {
|
|
@@ -141,12 +147,8 @@
|
|
|
141
147
|
"opacity": "sys.state.focus"
|
|
142
148
|
},
|
|
143
149
|
"ring": {
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"outerLayerPosition": "depth 0..2px from the tab edge (the outer stroke)",
|
|
147
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
148
|
-
"insetColor": "sys.color.focusInset",
|
|
149
|
-
"insetLayerPosition": "depth 2..3px from the tab edge (the counter-ring just inside the outer stroke)",
|
|
150
|
+
"width": "sys.borderWidth.hairline",
|
|
151
|
+
"color": "sys.color.border.focused",
|
|
150
152
|
"implementation": "inset box-shadow on the tab's `::after` overlay. Constrained strictly inside the tab's footprint and never exceeds it."
|
|
151
153
|
},
|
|
152
154
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
@@ -154,6 +156,6 @@
|
|
|
154
156
|
"forbidden": [
|
|
155
157
|
"rounded tabs given raw <button>/<a> or bare-text children instead of <Tab value=…> elements — the chip chrome (chorus-chip--filter + chorus-chip--rounded), the selected state, and the aria-selected/data-value binding all live on <Tab>; raw children render unstyled with no selected state",
|
|
156
158
|
"rounded tab radius set to sys.radius.full (pill) — the rounded variant is the sys.radius.md (8) soft-rectangle; reach for Segmented when a capsule row is needed",
|
|
157
|
-
"active state painted with sys.color.brand — rounded tabs use sys.color.
|
|
159
|
+
"active state painted with sys.color.text.brand — rounded tabs use sys.color.background.inverse fill + sys.color.text.inverse label in selected state"
|
|
158
160
|
]
|
|
159
161
|
}
|
|
@@ -23,9 +23,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
|
23
23
|
</Tabs>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
### With icon
|
|
26
|
+
## Leading icon
|
|
29
27
|
|
|
30
28
|
Leading glyph in each segment — useful when the verb alone could read as anything.
|
|
31
29
|
|
|
@@ -41,7 +39,7 @@ import { CheckedIcon, PlusIcon } from '@teamblind-chorus/ui/icons';
|
|
|
41
39
|
</Tabs>
|
|
42
40
|
```
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
## Overflow
|
|
45
43
|
|
|
46
44
|
When natural width exceeds the column, the row scrolls horizontally — no `fullWidth` (equal-width segments would break the shared-density contract with Filter chips). Trailing **Edge fade** (rightmost **48px** / `ref.space.600`) paints via `mask-image` only while overflow is present.
|
|
47
45
|
|
|
@@ -61,7 +59,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
|
61
59
|
</Tabs>
|
|
62
60
|
```
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
## Focused segment
|
|
65
63
|
|
|
66
64
|
Static specimen — pins the focus ring to a selected segment. See top-level [Focus indicator](#focus-indicator).
|
|
67
65
|
|
|
@@ -83,7 +81,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
|
83
81
|
|
|
84
82
|
## Anatomy
|
|
85
83
|
|
|
86
|
-
Each segment renders with `chorus-chip chorus-chip--filter` — see [Filter chip](../chip/filter.md). Selected swaps from unselected (`transparent` fill + `
|
|
84
|
+
Each segment renders with `chorus-chip chorus-chip--filter` — see [Filter chip](../chip/filter.md). Selected swaps from unselected (`transparent` fill + `border.default` border + `onSurface` label) to selected (`inverseSurface` fill + `inverseOnSurface` label, border `transparent` with 1px width held).
|
|
87
85
|
|
|
88
86
|
Chip behaviour inherited verbatim — except the focus ring, re-anchored as an inset overlay on a `::after` layer (segmented row is a horizontal scroller; the chip's default outward ring would clip).
|
|
89
87
|
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"selectionStates": {
|
|
69
69
|
"$ref": "../chip/filter.spec.json#/selectionStates",
|
|
70
|
-
"note": "Each segment swaps between Filter chip's unselected (surfaceContainerHigh +
|
|
70
|
+
"note": "Each segment swaps between Filter chip's unselected (surfaceContainerHigh + border.default border) and selected (inverseSurface + inverseOnSurface) recipes."
|
|
71
71
|
},
|
|
72
72
|
"states": {
|
|
73
73
|
"$ref": "../chip/filter.spec.json#/states",
|
|
@@ -82,19 +82,15 @@
|
|
|
82
82
|
"opacity": "sys.state.focus"
|
|
83
83
|
},
|
|
84
84
|
"ring": {
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"outerLayerPosition": "depth 0..2px from the segment edge (the outer stroke)",
|
|
88
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
89
|
-
"insetColor": "sys.color.focusInset",
|
|
90
|
-
"insetLayerPosition": "depth 2..3px from the segment edge (the counter-ring just inside the outer stroke)",
|
|
85
|
+
"width": "sys.borderWidth.hairline",
|
|
86
|
+
"color": "sys.color.border.focused",
|
|
91
87
|
"implementation": "inset box-shadow on the segment's `::after` overlay. Constrained strictly inside the segment's footprint and never exceeds it."
|
|
92
88
|
},
|
|
93
89
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
94
90
|
},
|
|
95
91
|
"forbidden": [
|
|
96
92
|
"segmented control given raw <button>/<a> or bare-text children instead of <Tab value=…> elements — the chip chrome, the selected pill state, and the aria-selected/data-value binding all live on <Tab>; raw children render unstyled with no selected state",
|
|
97
|
-
"active item painted with sys.color.primary fill — segmented active uses sys.color.surface fill on the selected pill (the rest of the row is surfaceContainer)",
|
|
93
|
+
"active item painted with sys.color.background.primary fill — segmented active uses sys.color.surface.default fill on the selected pill (the rest of the row is surfaceContainer)",
|
|
98
94
|
"segmented row reflowing on selection — anatomy is no-layout"
|
|
99
95
|
]
|
|
100
96
|
}
|
|
@@ -24,9 +24,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
|
24
24
|
</Tabs>
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
### With icon
|
|
27
|
+
## Leading icon
|
|
30
28
|
|
|
31
29
|
A leading glyph before the label.
|
|
32
30
|
|
|
@@ -43,7 +41,7 @@ import { PlusIcon, CheckedIcon } from '@teamblind-chorus/ui/icons';
|
|
|
43
41
|
</Tabs>
|
|
44
42
|
```
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
## Auto-fit
|
|
47
45
|
|
|
48
46
|
Wider terminal layout of Adaptive width — tabs share row width equally; indicator widens to match.
|
|
49
47
|
|
|
@@ -59,7 +57,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
|
59
57
|
</Tabs>
|
|
60
58
|
```
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
## Overflow
|
|
63
61
|
|
|
64
62
|
Narrower terminal layout — tabs hold content width and the row scrolls. The trailing 48px (`ref.space.600`) paints as a transparent `mask-image` edge fade; clears when scrolled to the last tab.
|
|
65
63
|
|
|
@@ -79,7 +77,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
|
79
77
|
</Tabs>
|
|
80
78
|
```
|
|
81
79
|
|
|
82
|
-
|
|
80
|
+
## Focused tab
|
|
83
81
|
|
|
84
82
|
Static specimen — pins the keyboard-focus ring to the selected tab. See top-level [Focus indicator](#focus-indicator).
|
|
85
83
|
|
|
@@ -104,9 +102,9 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
|
104
102
|
|
|
105
103
|
| Prop / state | Container | Label color | Indicator |
|
|
106
104
|
|------------------------|--------------------|---------------------------------------|-----------------------------------|
|
|
107
|
-
| **Tab — unselected** | transparent | `sys.color.
|
|
108
|
-
| **Tab — selected** | transparent | `sys.color.
|
|
109
|
-
| **Container row** | transparent, 16px inline padding, 1px `sys.color.
|
|
105
|
+
| **Tab — unselected** | transparent | `sys.color.border.boldest` (muted foreground) | none |
|
|
106
|
+
| **Tab — selected** | transparent | `sys.color.text.default` (strong foreground) | 2px `sys.borderWidth.thin` × `sys.color.text.default` along the bottom edge |
|
|
107
|
+
| **Container row** | transparent, 16px inline padding, 1px `sys.color.border.default` bottom divider running the full row width. Selected indicator paints over this divider. Edge fade (rightmost 48px / `ref.space.600`) paints via `mask-image` only while overflow is present. | — | — |
|
|
110
108
|
|
|
111
109
|
## Sizes
|
|
112
110
|
|
|
@@ -120,7 +118,7 @@ A single fixed rung — the 40px footprint stays constant across breakpoints.
|
|
|
120
118
|
| Inter-tab gap | 0 | — † |
|
|
121
119
|
| Slot gap (icon ↔ label) | 4px | `sys.layout.inline.sm` |
|
|
122
120
|
| Indicator height | 2px | `sys.borderWidth.thin` |
|
|
123
|
-
| Container bottom divider | 1px | `sys.borderWidth.hairline` × `sys.color.
|
|
121
|
+
| Container bottom divider | 1px | `sys.borderWidth.hairline` × `sys.color.border.default` |
|
|
124
122
|
| Label | 14 / Semibold | `sys.typo.label.md` |
|
|
125
123
|
| Icon | 16px | `sys.icon.md` |
|
|
126
124
|
| Edge fade width | 48px | `ref.space.600` — trailing `mask-image`, on overflow only |
|
|
@@ -136,7 +134,7 @@ A single fixed rung — the 40px footprint stays constant across breakpoints.
|
|
|
136
134
|
| `default` | — | Container + label at rest. |
|
|
137
135
|
| `hovered` | `sys.state.hover` (8%) | Pointer-driven via `:hover`. |
|
|
138
136
|
| `pressed` | `sys.state.pressed` (16%) | Pointer-driven via `:active`. |
|
|
139
|
-
| `selected` | — | Label flips to `sys.color.
|
|
137
|
+
| `selected` | — | Label flips to `sys.color.text.default`; 2px indicator slides to the new tab. |
|
|
140
138
|
| `disabled` | overlay suppressed | Label at `sys.state.disabled` (40%) opacity, focus ring suppressed, `cursor: not-allowed`. |
|
|
141
139
|
|
|
142
140
|
## Focus indicator
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Tabs",
|
|
4
4
|
"family": "tabs",
|
|
5
5
|
"subcomponent": "underline",
|
|
6
|
-
"description": "Horizontal tab row with a single 2px (`sys.borderWidth.thin`) `sys.color.
|
|
6
|
+
"description": "Horizontal tab row with a single 2px (`sys.borderWidth.thin`) `sys.color.border.selected` indicator that slides between the active tab's bottom edge on selection. Default content-section switcher.",
|
|
7
7
|
"element": "div",
|
|
8
8
|
"props": {
|
|
9
9
|
"variant": {
|
|
@@ -72,19 +72,19 @@
|
|
|
72
72
|
"slotGap": "sys.layout.inline.sm",
|
|
73
73
|
"indicatorHeight": "sys.borderWidth.thin",
|
|
74
74
|
"dividerWidth": "sys.borderWidth.hairline",
|
|
75
|
-
"dividerColor": "sys.color.
|
|
75
|
+
"dividerColor": "sys.color.border.default",
|
|
76
76
|
"labelTypo": "sys.typo.label.md",
|
|
77
77
|
"iconSize": "sys.icon.md",
|
|
78
78
|
"fadeWidth": "ref.space.600"
|
|
79
79
|
},
|
|
80
80
|
"selectionStates": {
|
|
81
81
|
"unselected": {
|
|
82
|
-
"label": "sys.color.
|
|
82
|
+
"label": "sys.color.text.subtle",
|
|
83
83
|
"indicator": null
|
|
84
84
|
},
|
|
85
85
|
"selected": {
|
|
86
|
-
"label": "sys.color.
|
|
87
|
-
"indicator": "sys.color.
|
|
86
|
+
"label": "sys.color.text.default",
|
|
87
|
+
"indicator": "sys.color.border.selected"
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
90
|
"states": {
|
|
@@ -113,20 +113,22 @@
|
|
|
113
113
|
"layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
|
|
114
114
|
"innerCounterRing": {
|
|
115
115
|
"width": "sys.borderWidth.hairline",
|
|
116
|
-
"color": "sys.color.
|
|
116
|
+
"color": "sys.color.border.focused"
|
|
117
117
|
},
|
|
118
118
|
"outerRing": {
|
|
119
119
|
"width": "sys.borderWidth.thin",
|
|
120
|
-
"color": "sys.color.
|
|
120
|
+
"color": "sys.color.border.focused"
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
123
|
"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 tab is in; never via plain mouse click."
|
|
124
124
|
},
|
|
125
125
|
"disabled": {
|
|
126
126
|
"overlay": null,
|
|
127
|
-
"
|
|
127
|
+
"label": "sys.color.text.disabled",
|
|
128
|
+
"indicator": null,
|
|
128
129
|
"suppressFocusRing": true,
|
|
129
|
-
"cursor": "not-allowed"
|
|
130
|
+
"cursor": "not-allowed",
|
|
131
|
+
"note": "Explicit disabled (no opacity): label drops to text.disabled, no indicator. Sits on a neutral surface so the disabled text token reads directly."
|
|
130
132
|
}
|
|
131
133
|
},
|
|
132
134
|
"focusIndicator": {
|
|
@@ -138,19 +140,15 @@
|
|
|
138
140
|
"opacity": "sys.state.focus"
|
|
139
141
|
},
|
|
140
142
|
"ring": {
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"outerLayerPosition": "depth 0..2px from the tab edge (the outer stroke)",
|
|
144
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
145
|
-
"insetColor": "sys.color.focusInset",
|
|
146
|
-
"insetLayerPosition": "depth 2..3px from the tab edge (the counter-ring just inside the outer stroke)",
|
|
143
|
+
"width": "sys.borderWidth.hairline",
|
|
144
|
+
"color": "sys.color.border.focused",
|
|
147
145
|
"implementation": "inset box-shadow on the tab's `::after` overlay (sits above the state-overlay `::before`, the label/icon, and the underline indicator). Constrained strictly inside the tab's footprint and never exceeds it."
|
|
148
146
|
},
|
|
149
147
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
150
148
|
},
|
|
151
149
|
"forbidden": [
|
|
152
150
|
"underline tabs given raw <button>/<a> or bare-text children instead of <Tab value=…> elements — the sliding indicator (.chorus-tabs__indicator), the useAdaptiveFit measurement that sets data-fit, the aria-selected/data-value binding, and the active label styling all live on <Tab>; raw children render as unstyled run-together text with no indicator and a broken scroll/stretch layout",
|
|
153
|
-
"active indicator painted with sys.color.brand — underline uses sys.color.
|
|
151
|
+
"active indicator painted with sys.color.text.brand — underline uses sys.color.border.selected for the indicator",
|
|
154
152
|
"tabs wrapped in extra horizontal padding — tabs is full-bleed by family declaration",
|
|
155
153
|
"tab label sizing below sys.typo.label.md (14px) — smaller text breaks Korean / CJK hierarchy",
|
|
156
154
|
"manual underline drawn via `border-bottom:` — indicator is the `::after` overlay, not a real border"
|
|
@@ -20,9 +20,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
|
|
|
20
20
|
<Thumbnail size={48} alt="Channel" src="/placeholder.png" />
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
### With update dot
|
|
23
|
+
## Update dot
|
|
26
24
|
|
|
27
25
|
A `brand`-tone dot at the top-right flags new activity. Decorative; the row carries the count in a sibling text slot.
|
|
28
26
|
|
|
@@ -34,7 +32,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
|
|
|
34
32
|
<Thumbnail size={48} alt="Channel" src="/placeholder.png" updateDot />
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
## Logo badge
|
|
38
36
|
|
|
39
37
|
A 16 × 16 sub-brand mark at the bottom-right on its own surface halo.
|
|
40
38
|
|
|
@@ -51,7 +49,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
|
|
|
51
49
|
/>
|
|
52
50
|
```
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
## Both badges
|
|
55
53
|
|
|
56
54
|
Top-right and bottom-right corners are independent and never collide.
|
|
57
55
|
|
|
@@ -69,7 +67,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
|
|
|
69
67
|
/>
|
|
70
68
|
```
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
## Surface outline
|
|
73
71
|
|
|
74
72
|
`outlined` paints a 2-token (`sys.borderWidth.thin`) `sys.color.surface` halo as an outset `box-shadow` around the container. The ring blends into the host's `surface*` tier and separates the circle's edge from anything visually noisy underneath. Painted as a shadow, not a `border:` — the rung's diameter never reflows.
|
|
75
73
|
|
|
@@ -87,7 +85,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
|
|
|
87
85
|
|
|
88
86
|
The two corner badges (`updateDot`, `logoBadge`) carry their own 1-token surface halos and compose cleanly over the outlined ring — order is image → outlined ring → badge halos → badge fills, all painted as `box-shadow` so footprint never changes.
|
|
89
87
|
|
|
90
|
-
|
|
88
|
+
## Size ladder
|
|
91
89
|
|
|
92
90
|
The full ladder side-by-side; update-dot steps down at the 32-rung boundary.
|
|
93
91
|
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"outlined": {
|
|
44
44
|
"type": "boolean",
|
|
45
45
|
"default": false,
|
|
46
|
-
"description": "When `true`, paints a 2-token-wide `sys.color.surface` outline around the Thumbnail container as an outset halo. The outline reads as an isolation ring that separates the Thumbnail's circular edge from anything visually noisy underneath it. Painted as `box-shadow: 0 0 0 sys.borderWidth.thin sys.color.surface` so it never reflows the slot's intrinsic diameter — same no-layout-stroke idiom the rest of the system uses. Pair with hosts whose chrome reads as a `surface*` tier (so the halo blends in) and whose backdrop differs from that tier (so the halo actually separates).",
|
|
46
|
+
"description": "When `true`, paints a 2-token-wide `sys.color.surface.default` outline around the Thumbnail container as an outset halo. The outline reads as an isolation ring that separates the Thumbnail's circular edge from anything visually noisy underneath it. Painted as `box-shadow: 0 0 0 sys.borderWidth.thin sys.color.surface.default` so it never reflows the slot's intrinsic diameter — same no-layout-stroke idiom the rest of the system uses. Pair with hosts whose chrome reads as a `surface*` tier (so the halo blends in) and whose backdrop differs from that tier (so the halo actually separates).",
|
|
47
47
|
"whenToReachForIt": [
|
|
48
48
|
"The Thumbnail half-overlaps or sits over an image — [ProfileHeader](../profile-header/profile-header.md) avatar (56-rung on cover band), [Profile carousel](../carousel/profile.md) avatar (64-rung on card cover), any avatar pulled onto a Hero / Cover photo.",
|
|
49
49
|
"The backdrop is a brand-tonal strip, a `*Container` fill, or a gradient band ([Banner](../banner/banner.md) inside a colour-tinted host, a Section painted with a `successContainer` / `errorContainer` / `brandContainer` fill).",
|
|
@@ -90,24 +90,24 @@
|
|
|
90
90
|
"containerRadius": "sys.radius.full",
|
|
91
91
|
"containerOutline": {
|
|
92
92
|
"appliesWhen": "props.outlined === true",
|
|
93
|
-
"color": "sys.color.surface",
|
|
93
|
+
"color": "sys.color.surface.default",
|
|
94
94
|
"width": "sys.borderWidth.thin",
|
|
95
95
|
"rendering": "box-shadow (outset, 0 0 0 width color) so the halo never reflows the slot's intrinsic diameter — same no-layout-stroke contract every other Chorus surface follows. Pairs the halo with the host's `surface*` tier so the ring blends into the chrome around it while separating the Thumbnail from a contrasting backdrop (cover image, brand tonal strip, gradient band)."
|
|
96
96
|
},
|
|
97
|
-
"imageFallbackFill": "sys.color.
|
|
97
|
+
"imageFallbackFill": "sys.color.surface.sunken",
|
|
98
98
|
"imageFallbackImage": "/placeholder.png",
|
|
99
99
|
"imageFallbackImageRendering": "background-image, cover, center — paints under the runtime <img>; visible only when the inline image is missing or fails to load, so the slot still resolves to an image rather than an empty surface tone.",
|
|
100
|
-
"imageFallbackGlyphColor": "sys.color.
|
|
101
|
-
"updateDotFill": "sys.color.brand",
|
|
100
|
+
"imageFallbackGlyphColor": "sys.color.text.subtle",
|
|
101
|
+
"updateDotFill": "sys.color.text.brand",
|
|
102
102
|
"updateDotRadius": "sys.radius.full",
|
|
103
103
|
"updateDotHalo": {
|
|
104
|
-
"color": "sys.color.surface",
|
|
104
|
+
"color": "sys.color.surface.default",
|
|
105
105
|
"width": "sys.borderWidth.hairline",
|
|
106
106
|
"rendering": "box-shadow"
|
|
107
107
|
},
|
|
108
108
|
"logoBadgeRadius": "sys.radius.full",
|
|
109
109
|
"logoBadgeHalo": {
|
|
110
|
-
"color": "sys.color.surface",
|
|
110
|
+
"color": "sys.color.surface.default",
|
|
111
111
|
"width": "sys.borderWidth.hairline",
|
|
112
112
|
"rendering": "box-shadow"
|
|
113
113
|
}
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"behavior": {
|
|
156
156
|
"slotOmissionCollapses": "Both badges (updateDot, logoBadge) drop out of the layout entirely when absent — no reserved corner whitespace.",
|
|
157
157
|
"badgesOverlay": "Both badges are absolutely positioned over the image; the container's overall footprint is the image's diameter regardless of whether the badges are present. The 1px surface halo is rendered as a box-shadow.",
|
|
158
|
-
"outlinedHalo": "When `outlined={true}`, the container paints a 2-token (`sys.borderWidth.thin`) `sys.color.surface` halo via `box-shadow` — outset of the diameter, never reflowing the slot. The corner badges' own 1-token halos are layered above it. Composes cleanly with `updateDot` and `logoBadge`.",
|
|
158
|
+
"outlinedHalo": "When `outlined={true}`, the container paints a 2-token (`sys.borderWidth.thin`) `sys.color.surface.default` halo via `box-shadow` — outset of the diameter, never reflowing the slot. The corner badges' own 1-token halos are layered above it. Composes cleanly with `updateDot` and `logoBadge`.",
|
|
159
159
|
"imageClip": "Container's radius.full plus overflow: hidden clips the image to a perfect circle even when the source is rectangular.",
|
|
160
160
|
"noTextFallback": "Slot only renders <img> content — there is no text fallback. When `src` is omitted or the server fails to deliver the image, the slot's background paints the bundled `/placeholder.png` over a `surfaceContainerHigh` base. The placeholder is the image-area's runtime safety net for load failures; design-time scaffolds should pass the same URL through `src` explicitly so the contract is visible in the composition rather than hidden in the CSS layer.",
|
|
161
161
|
"updateDotBreakpoint": "Dot drops from 8×8 (at and above rung 32, including 56) to 4×4 (rungs 24 / 20 / 16) so it reads as highlight, not occluder, on the smaller diameters.",
|
|
@@ -22,9 +22,7 @@ import { Toast } from '@teamblind-chorus/ui';
|
|
|
22
22
|
<Toast>Token copied to clipboard</Toast>
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
### With action
|
|
25
|
+
## Action
|
|
28
26
|
|
|
29
27
|
A small Text Button (`appearance="inverse"`) on the trailing edge for follow-through (Undo, Retry, View). With the trailing slot present, the toast stays on screen longer (~6s). The Button node is passed directly so the call site spells out the sub-component.
|
|
30
28
|
|
|
@@ -42,7 +40,7 @@ import { Toast, Button } from '@teamblind-chorus/ui';
|
|
|
42
40
|
</Toast>
|
|
43
41
|
```
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
## Dismiss
|
|
46
44
|
|
|
47
45
|
A medium Icon Button (`appearance="inverse"`) on the trailing edge for explicit dismissal — used when the toast carries information the user may want to read at their own pace. Composed at the call site so the `appearance="inverse"` binding stays visible.
|
|
48
46
|
|
|
@@ -66,7 +64,7 @@ import { XIcon } from '@teamblind-chorus/ui/icons';
|
|
|
66
64
|
</Toast>
|
|
67
65
|
```
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
## Max width
|
|
70
68
|
|
|
71
69
|
Strip grows until it hits the 400 cap (or viewport-minus-safe-area on narrow screens). Past that, the body wraps onto a second line rather than letting the strip stretch into a banner.
|
|
72
70
|
|
|
@@ -78,7 +76,7 @@ import { Toast } from '@teamblind-chorus/ui';
|
|
|
78
76
|
<Toast>Saved your draft to every workspace you joined this month</Toast>
|
|
79
77
|
```
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
## Truncation
|
|
82
80
|
|
|
83
81
|
Body wraps up to two lines and truncates with an ellipsis past that — body, trailing button, and any leading glyph stay vertically centred. Pair with a trailing dismiss when the message is status the user may want to read at their own pace.
|
|
84
82
|
|
|
@@ -108,7 +106,7 @@ A single appearance — inverse. The inverse pair (`inverseSurface` / `inverseOn
|
|
|
108
106
|
|
|
109
107
|
| Appearance | Container fill | Foreground | When to use |
|
|
110
108
|
|------------|-----------------------------|----------------------------------|-------------|
|
|
111
|
-
| `default` | `sys.color.
|
|
109
|
+
| `default` | `sys.color.background.inverse` | `sys.color.text.inverse` | Every toast. Status messages that must read against any surface tier in the stack. |
|
|
112
110
|
|
|
113
111
|
## Slots
|
|
114
112
|
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
},
|
|
67
67
|
"appearances": {
|
|
68
68
|
"default": {
|
|
69
|
-
"background": "sys.color.
|
|
70
|
-
"foreground": "sys.color.
|
|
69
|
+
"background": "sys.color.background.inverse",
|
|
70
|
+
"foreground": "sys.color.text.inverse",
|
|
71
71
|
"note": "The inverse pair is the only appearance — toasts always read as a contrasting strip against the page. Action buttons inside an inverse surface fall back to the regular primary family per the inverse-cluster contract."
|
|
72
72
|
}
|
|
73
73
|
},
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"role": "Container carries role='status' and aria-live='polite' so screen readers announce the confirmation without interrupting the user's current focus."
|
|
77
77
|
},
|
|
78
78
|
"forbidden": [
|
|
79
|
-
"Toast painted with sys.color.surface — toast uses sys.color.
|
|
79
|
+
"Toast painted with sys.color.surface.default — toast uses sys.color.background.inverse fill + inverseOnSurface text",
|
|
80
80
|
"Toast as a blocking commit prompt — destructive prompts use Dialog or BottomSheet; toast is for non-blocking feedback",
|
|
81
81
|
"Toast persistent (no auto-dismiss)",
|
|
82
82
|
"More than one toast stacked — toast queue is single-at-a-time",
|
|
@@ -18,9 +18,7 @@ import { Tooltip } from '@teamblind-chorus/ui';
|
|
|
18
18
|
<Tooltip placement="top">Tooltip text</Tooltip>
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
### Inverse
|
|
21
|
+
## Inverse
|
|
24
22
|
|
|
25
23
|
The dark-cluster bubble. Reach for it when the host screen is already saturated with `primary` tone — the inverse cluster (`inverseSurface` / `inverseOnSurface`) reads as a distinct floating note above the page.
|
|
26
24
|
|
|
@@ -32,7 +30,7 @@ import { Tooltip } from '@teamblind-chorus/ui';
|
|
|
32
30
|
<Tooltip placement="top" appearance="inverse">Tooltip text</Tooltip>
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
## Action
|
|
36
34
|
|
|
37
35
|
A small Text Button for follow-through ("Learn more", "Got it"). Bind the button's `appearance` to match the tooltip — `onPrimary` for the default (brand-blue) tooltip; `inverse` for the inverse tooltip.
|
|
38
36
|
|
|
@@ -50,7 +48,7 @@ import { Tooltip, Button } from '@teamblind-chorus/ui';
|
|
|
50
48
|
</Tooltip>
|
|
51
49
|
```
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
## Multi-line action
|
|
54
52
|
|
|
55
53
|
When the body grows past the 300 cap, the body wraps onto a second line and the action slot drops below the body. The body-to-action gap goes from 12 (inline) to 6 (block) so the stacked action reads as part of the same group.
|
|
56
54
|
|
|
@@ -68,7 +66,7 @@ import { Tooltip, Button } from '@teamblind-chorus/ui';
|
|
|
68
66
|
</Tooltip>
|
|
69
67
|
```
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
## Placements
|
|
72
70
|
|
|
73
71
|
Six placements, named `<edge>` or `<edge>-<align>`. The `<edge>` axis (top / bottom) places the bubble above or below the trigger and chooses the caret edge; the `<align>` axis (start / center / end) shifts the caret along the parallel axis.
|
|
74
72
|
|
|
@@ -93,8 +91,8 @@ Two appearances — `default` (the canonical brand-blue tooltip) and `inverse` (
|
|
|
93
91
|
|
|
94
92
|
| Appearance | Container fill | Foreground | Pair the action button with | When to use |
|
|
95
93
|
|------------|-----------------------------|----------------------------------|---------------------------------------------------------------|-------------|
|
|
96
|
-
| `default` | `sys.color.primary` | `sys.color.
|
|
97
|
-
| `inverse` | `sys.color.
|
|
94
|
+
| `default` | `sys.color.background.primary` | `sys.color.text.onFill` | `<Button variant="text" appearance="onPrimary">` | The canonical tooltip — brand-blue bubble, always-white label. |
|
|
95
|
+
| `inverse` | `sys.color.background.inverse` | `sys.color.text.inverse` | `<Button variant="text" appearance="inverse">` | When the host screen is saturated with `primary` tone — the dark inverse bubble reads as distinct floating chrome. |
|
|
98
96
|
|
|
99
97
|
`default` is the right reach in most cases; switch to `inverse` only when the surrounding surface gives the brand-blue bubble no breathing room.
|
|
100
98
|
|
|
@@ -80,14 +80,14 @@
|
|
|
80
80
|
},
|
|
81
81
|
"appearances": {
|
|
82
82
|
"default": {
|
|
83
|
-
"background": "sys.color.primary",
|
|
84
|
-
"foreground": "sys.color.
|
|
83
|
+
"background": "sys.color.background.primary",
|
|
84
|
+
"foreground": "sys.color.text.onFill",
|
|
85
85
|
"elevation": "sys.elevation.overlay",
|
|
86
86
|
"note": "Brand-blue bubble with an always-white label. Both `primary` and `onPrimary` are theme-stable, so the bubble reads the same in light and dark mode. Action buttons inside this appearance MUST use `Button variant=\"text\" appearance=\"onPrimary\"` so the label stays white in either theme — `appearance=\"inverse\"` flips with the theme and would render the label dark in dark mode."
|
|
87
87
|
},
|
|
88
88
|
"inverse": {
|
|
89
|
-
"background": "sys.color.
|
|
90
|
-
"foreground": "sys.color.
|
|
89
|
+
"background": "sys.color.background.inverse",
|
|
90
|
+
"foreground": "sys.color.text.inverse",
|
|
91
91
|
"elevation": "sys.elevation.overlay",
|
|
92
92
|
"note": "Inverse-cluster bubble for screens already saturated with primary tone, where the `default` brand-blue tooltip would compete with the surrounding chrome. Action buttons inside this appearance use `Button variant=\"text\" appearance=\"inverse\"` so the label flips with the host fill."
|
|
93
93
|
}
|