@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
|
@@ -10,7 +10,7 @@ A small inline status pill — a tonal mark sized for the trailing edge of a row
|
|
|
10
10
|
|
|
11
11
|
## Default
|
|
12
12
|
|
|
13
|
-
The `neutral` appearance — `sys.color.
|
|
13
|
+
The `neutral` appearance — `sys.color.background.neutral` fill (the translucent inverse-tone scrim — ~8% black in light, ~8% white in dark) with `onSurfaceVariant` foreground. The quiet informational state.
|
|
14
14
|
|
|
15
15
|
```preview
|
|
16
16
|
status-tag/default
|
|
@@ -20,9 +20,7 @@ import { StatusTag } from '@teamblind-chorus/ui';
|
|
|
20
20
|
<StatusTag>Pending</StatusTag>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
### Error
|
|
23
|
+
## Error
|
|
26
24
|
|
|
27
25
|
The `error` appearance — `errorContainer` fill with an `onErrorContainer` foreground. The rejection / blocked / failed state.
|
|
28
26
|
|
|
@@ -34,7 +32,7 @@ import { StatusTag } from '@teamblind-chorus/ui';
|
|
|
34
32
|
<StatusTag appearance="error">Rejected</StatusTag>
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
## List row
|
|
38
36
|
|
|
39
37
|
The canonical pairing — a `list/thumbnail` row whose label carries a trailing StatusTag with a `sys.layout.container.2xs` (4px) inline gap. The gap belongs to the label column; StatusTag carries no outer margin.
|
|
40
38
|
|
|
@@ -70,7 +68,7 @@ import { List, StatusTag, Thumbnail } from '@teamblind-chorus/ui';
|
|
|
70
68
|
/>
|
|
71
69
|
```
|
|
72
70
|
|
|
73
|
-
|
|
71
|
+
## Inline
|
|
74
72
|
|
|
75
73
|
A StatusTag tucked inline inside a paragraph or a feed-post header. Same 4px gap rule: the host column owns the whitespace, StatusTag stays a bare pill.
|
|
76
74
|
|
|
@@ -79,7 +77,7 @@ status-tag/inline
|
|
|
79
77
|
---
|
|
80
78
|
import { StatusTag } from '@teamblind-chorus/ui';
|
|
81
79
|
|
|
82
|
-
<p style={{ font: '14px var(--sys-typo-fontFamily)', color: 'var(--sys-color-
|
|
80
|
+
<p style={{ font: '14px var(--sys-typo-fontFamily)', color: 'var(--sys-color-text-default)' }}>
|
|
83
81
|
Shared document <StatusTag>Pending</StatusTag> is awaiting review.
|
|
84
82
|
</p>
|
|
85
83
|
```
|
|
@@ -101,8 +99,8 @@ import { StatusTag } from '@teamblind-chorus/ui';
|
|
|
101
99
|
|
|
102
100
|
| Appearance | Container fill | Foreground | When to reach |
|
|
103
101
|
|------------|-----------------------------------------------------------------------------|----------------------------------|-------------------------------------------------------------------------------|
|
|
104
|
-
| `neutral` | `sys.color.
|
|
105
|
-
| `error` | `sys.color.
|
|
102
|
+
| `neutral` | `sys.color.background.neutral` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | `sys.color.text.subtle` | Quiet informational default — visible on every surface tier. In-progress / awaiting states — "pending", "draft", "queued", "in review". |
|
|
103
|
+
| `error` | `sys.color.background.danger` | `sys.color.text.danger` | Rejection / blocked / failed state. Use sparingly. |
|
|
106
104
|
|
|
107
105
|
## States
|
|
108
106
|
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
},
|
|
41
41
|
"appearances": {
|
|
42
42
|
"neutral": {
|
|
43
|
-
"background": "sys.color.
|
|
44
|
-
"foreground": "sys.color.
|
|
45
|
-
"note": "The quiet informational default. Background paints `sys.color.
|
|
43
|
+
"background": "sys.color.background.neutral",
|
|
44
|
+
"foreground": "sys.color.text.subtle",
|
|
45
|
+
"note": "The quiet informational default. Background paints `sys.color.background.neutral` — the translucent inverse-tone scrim (~8% black in light, ~8% white in dark) shared with Chip / Tag default, Progress track, and Skeleton — so the pill stays visible against every surface tier in either theme. Pair with statuses that describe an in-progress / awaiting state — 'pending', 'draft', 'queued', 'in review'."
|
|
46
46
|
},
|
|
47
47
|
"error": {
|
|
48
|
-
"background": "sys.color.
|
|
49
|
-
"foreground": "sys.color.
|
|
48
|
+
"background": "sys.color.background.danger",
|
|
49
|
+
"foreground": "sys.color.text.danger",
|
|
50
50
|
"note": "The rejection / blocked / failed state. Pair with statuses that describe a terminal negative outcome — 'rejected', 'failed', 'blocked'. Keep usage scarce — every error pill on a screen competes with the others for attention."
|
|
51
51
|
}
|
|
52
52
|
},
|
|
@@ -30,11 +30,9 @@ import { SuggestionList } from '@teamblind-chorus/ui';
|
|
|
30
30
|
/>
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## Header action
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Extends the header with a trailing `accent` Text Button when the screen has a broader index page to route to.
|
|
35
|
+
Extends the header with a trailing `accent` Text Button when the screen has a broader index page to route to. Surfaces the entity-agnostic anatomy — the same row shape carries a suggested person instead of a channel.
|
|
38
36
|
|
|
39
37
|
```preview
|
|
40
38
|
suggestion-list/with-header-action
|
|
@@ -46,8 +44,6 @@ import { SuggestionList } from '@teamblind-chorus/ui';
|
|
|
46
44
|
headerAction={{ label: 'See all', href: '/channels' }}
|
|
47
45
|
items={[
|
|
48
46
|
{ value: 'jordan', name: 'Jordan Lee', followers: '342 Followers', description: 'PM at a logistics startup. Mostly here for the threads on roadmap reviews.', thumbnail: { alt: 'Jordan Lee' } },
|
|
49
|
-
{ value: 'taylor', name: 'Taylor Brooks', followers: '1.1K Followers', description: 'Frontend engineer. Writes about the bits between the framework and the user.', thumbnail: { alt: 'Taylor Brooks' } },
|
|
50
|
-
{ value: 'morgan', name: 'Morgan Park', followers: '512 Followers', description: 'Designer-turned-PM. Notes on the handoff layer.', thumbnail: { alt: 'Morgan Park' } },
|
|
51
47
|
]}
|
|
52
48
|
/>
|
|
53
49
|
```
|
|
@@ -72,7 +68,7 @@ import { SuggestionList } from '@teamblind-chorus/ui';
|
|
|
72
68
|
| headerAction | Header's trailing `xsmall` [Text Button](../button/text.md), `accent` appearance |
|
|
73
69
|
| pager | Horizontal scroll, `scroll-snap-type: x mandatory`, scrollbar hidden; `sys.layout.inline.xl` (16/24px) gap. Re-pays the 16 left rail via `padding-left: sys.layout.container.md` (+ matching `scroll-padding-left` snap-port); the full-bleed host means the pager spans the surface so the next-page peek reaches the trailing edge intrinsically (no negative margin). When embedded in a padded host (Feed), the pager re-adds `margin-inline: -sys.layout.container.md` to pierce the host's rail. |
|
|
74
70
|
| page | `flex: 0 0 calc(100% - sys.layout.inline.xl - sys.layout.inline.md)` so the next page leading edge shows by 8px; `scroll-snap-align: start`; `sys.layout.stack.sm` (12px) between rows |
|
|
75
|
-
| row | [list/entry](../list/entry.md)-shaped row at `xlarge` rung — 56 avatar, `inline.lg` gap, label.md primary / label.sm `secondary` + `description`. Keeps the list/entry native `container.md` inline padding (tap target) and adds `margin-inline: -container.md` so the visible content (avatar / toggle) sits flush at the page boundaries — the avatar reads at 16 from the surface, aligned with the header label. SuggestionList adds: 12px bottom padding + `::after` divider 1px / `
|
|
71
|
+
| row | [list/entry](../list/entry.md)-shaped row at `xlarge` rung — 56 avatar, `inline.lg` gap, label.md primary / label.sm `secondary` + `description`. Keeps the list/entry native `container.md` inline padding (tap target) and adds `margin-inline: -container.md` so the visible content (avatar / toggle) sits flush at the page boundaries — the avatar reads at 16 from the surface, aligned with the header label. SuggestionList adds: 12px bottom padding + `::after` divider 1px / `border.default` anchored at the text column (standalone: `container.md` 16 + `ref.space.700` 56 + `inline.lg` 12 = 84px from row left; embedded: 68px since the row's inline padding + margin are zeroed). |
|
|
76
72
|
| trailingAction | [Toggle Button](../button/toggle.md), `variant="toggle"` — composed into the row's `trailingIcon` slot. |
|
|
77
73
|
|
|
78
74
|
## States
|
|
@@ -79,17 +79,17 @@
|
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
81
|
"sizing": {
|
|
82
|
-
"containerFill": "sys.color.surface",
|
|
82
|
+
"containerFill": "sys.color.surface.default",
|
|
83
83
|
"containerPaddingBlock": "sys.layout.container.lg",
|
|
84
84
|
"containerPaddingInline": "0",
|
|
85
85
|
"containerPaddingInlineNote": "Full-bleed: the host pays no inline padding. The Header pays the 16 inline rail (sys.layout.container.md) and the pager re-pays it via padding-left.",
|
|
86
86
|
"headerToPagerGap": "sys.layout.stack.md",
|
|
87
87
|
"headerToPagerGapNote": "Header's own block-end (stack.md = 16) is the header↔pager gap; the host gap collapses to 0 (matches DirectoryList).",
|
|
88
88
|
"labelTypo": "sys.typo.heading.md",
|
|
89
|
-
"labelColor": "sys.color.
|
|
90
|
-
"headerActionRendersAs": "Button variant='text' size='xsmall' appearance='accent' — label paints in sys.color.primary via the Text Button accent token.",
|
|
89
|
+
"labelColor": "sys.color.text.default",
|
|
90
|
+
"headerActionRendersAs": "Button variant='text' size='xsmall' appearance='accent' — label paints in sys.color.background.primary via the Text Button accent token.",
|
|
91
91
|
"headerActionTypo": "sys.typo.label.sm",
|
|
92
|
-
"headerActionColor": "sys.color.
|
|
92
|
+
"headerActionColor": "sys.color.text.link",
|
|
93
93
|
"pageGap": "sys.layout.inline.xl",
|
|
94
94
|
"pagePeek": "sys.layout.inline.md",
|
|
95
95
|
"pageRowGap": "sys.layout.stack.sm",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"rowBottomPadding": "sys.layout.stack.sm",
|
|
99
99
|
"rowBottomPaddingNote": "The bottom padding sits between the row's text content and the hairline divider so the divider reads as a separator rather than a baseline rule.",
|
|
100
100
|
"dividerWidth": "sys.borderWidth.hairline",
|
|
101
|
-
"dividerColor": "sys.color.
|
|
101
|
+
"dividerColor": "sys.color.border.default",
|
|
102
102
|
"dividerInset": "calc(sys.layout.container.md + ref.space.700 + sys.layout.inline.lg) = 84px from the row's leading edge in standalone mode — anchors to the start of the row's text column so the divider aligns with the column, not the avatar. Embedded mode re-anchors to 68px (drops the container.md term) since the row's inline padding + negative margin are zeroed."
|
|
103
103
|
},
|
|
104
104
|
"rowProps": {
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"required": true,
|
|
124
124
|
"agentRequired": true,
|
|
125
125
|
"omittedBehavior": "error",
|
|
126
|
-
"fallbackOnMissingSrc": "sys.color.
|
|
126
|
+
"fallbackOnMissingSrc": "sys.color.surface.sunken",
|
|
127
127
|
"description": "Forwarded to Thumbnail verbatim — src, alt, updateDot, logoBadge. Agents MUST pass `src`; fill `/placeholder.png` when no real subject is implied. `fallbackOnMissingSrc` is the runtime safety net for load failures, not a scaffold-time omission license."
|
|
128
128
|
},
|
|
129
129
|
"active": {
|
|
@@ -147,12 +147,8 @@
|
|
|
147
147
|
"opacity": "sys.state.focus"
|
|
148
148
|
},
|
|
149
149
|
"ring": {
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"outerLayerPosition": "depth 0..2px from the row edge (the outer stroke)",
|
|
153
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
154
|
-
"insetColor": "sys.color.focusInset",
|
|
155
|
-
"insetLayerPosition": "depth 2..3px from the row edge (the counter-ring just inside the outer stroke)",
|
|
150
|
+
"width": "sys.borderWidth.hairline",
|
|
151
|
+
"color": "sys.color.border.focused",
|
|
156
152
|
"implementation": "inset box-shadow constrained strictly inside the row's footprint."
|
|
157
153
|
},
|
|
158
154
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 🇰🇷 한국어: [`i18n/ko/schema/components/switch/switch.md`](../../../i18n/ko/schema/components/switch/switch.md)
|
|
4
4
|
|
|
5
|
-
A binary active/inactive control — a pill-shaped track with a circular thumb that translates between ends. **Inactive** reads as a `
|
|
5
|
+
A binary active/inactive control — a pill-shaped track with a circular thumb that translates between ends. **Inactive** reads as a `icon.subtlest` track with an `border.default` hairline and a fixed-white thumb; **active** paints the track in `primary` so the contract reads chromatically without an inline label.
|
|
6
6
|
|
|
7
7
|
**Reach for this when** a setting commits the moment it changes — notifications, privacy toggles, *show in feed*, instant-commit list trailing. **Skip when** the commit needs confirmation (use [Button](../button/button.md) + [Dialog](../dialog/dialog.md)), when the user picks one of several options ([List/radio](../list/radio.md), [Tabs](../tabs/tabs.md)), or when destructive — Switch carries no undo.
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ A binary active/inactive control — a pill-shaped track with a circular thumb t
|
|
|
10
10
|
|
|
11
11
|
## Inactive
|
|
12
12
|
|
|
13
|
-
The resting state — a `
|
|
13
|
+
The resting state — a `icon.subtlest` track (inverse-tone ~8% tint: black in light, white in dark) with an `border.default` hairline and a fixed-white thumb at the leading end. The tint stays distinct on any host surface tier. The specimen below is pinned to this type (rendered `checked={false}`); see [Behavior](#behavior) for the live toggle contract.
|
|
14
14
|
|
|
15
15
|
```preview
|
|
16
16
|
switch/inactive
|
|
@@ -32,9 +32,7 @@ import { Switch } from '@teamblind-chorus/ui';
|
|
|
32
32
|
<Switch checked aria-label="Notifications" />
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
### With label
|
|
35
|
+
## Label
|
|
38
36
|
|
|
39
37
|
The canonical pairing — a visible label to the left, `sys.layout.inline.md` (12px) gap. The label carries the accessible name via `htmlFor` + `id` or `aria-labelledby`; Switch drops `aria-label`.
|
|
40
38
|
|
|
@@ -52,16 +50,16 @@ import { Switch } from '@teamblind-chorus/ui';
|
|
|
52
50
|
padding: 'var(--sys-layout-container-xs) var(--sys-layout-container-md)',
|
|
53
51
|
}}
|
|
54
52
|
>
|
|
55
|
-
<span id="notif-label" className="sys-typo-body-sm" style={{ color: 'var(--sys-color-
|
|
53
|
+
<span id="notif-label" className="sys-typo-body-sm" style={{ color: 'var(--sys-color-text-default)' }}>
|
|
56
54
|
Push notifications
|
|
57
55
|
</span>
|
|
58
56
|
<Switch checked aria-labelledby="notif-label" />
|
|
59
57
|
</div>
|
|
60
58
|
```
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
## Focus ring
|
|
63
61
|
|
|
64
|
-
Outward
|
|
62
|
+
Outward single ring on the track's outer edge, shown on the inactive specimen. The card is pinned to its focused state via `forcedState="focused"`; in production the ring triggers on `:focus-visible` (keyboard / programmatic focus, never a plain mouse click).
|
|
65
63
|
|
|
66
64
|
```preview
|
|
67
65
|
switch/focused
|
|
@@ -80,10 +78,10 @@ import { Switch } from '@teamblind-chorus/ui';
|
|
|
80
78
|
|
|
81
79
|
| Slot | Token bindings |
|
|
82
80
|
|------------------|----------------|
|
|
83
|
-
| track (inactive) | `sys.color.
|
|
84
|
-
| track (active) | `sys.color.primary` fill, no stroke, fully rounded |
|
|
81
|
+
| track (inactive) | `sys.color.background.neutral` fill, hairline `border.default` stroke, fully rounded |
|
|
82
|
+
| track (active) | `sys.color.background.primary` fill, no stroke, fully rounded |
|
|
85
83
|
| thumb (inactive) | `ref.palette.white.1000` (fixed white) fill, 28 × 28, fully rounded, 2px inset from leading edge |
|
|
86
|
-
| thumb (active) | `sys.color.
|
|
84
|
+
| thumb (active) | `sys.color.text.onFill` fill, 28 × 28, translated 20px to the trailing end |
|
|
87
85
|
| transition | 120ms `ease-out` on track-fill, thumb-fill, and thumb-translate |
|
|
88
86
|
|
|
89
87
|
## Appearance
|
|
@@ -92,8 +90,8 @@ A single appearance — no emphasis axis. The visible variation is the `data-sta
|
|
|
92
90
|
|
|
93
91
|
| State | Track fill | Track stroke | Thumb fill |
|
|
94
92
|
|----------|---------------------|---------------------------|----------------------------------------|
|
|
95
|
-
| inactive | `sys.color.
|
|
96
|
-
| active | `sys.color.primary` | none | `sys.color.
|
|
93
|
+
| inactive | `sys.color.background.neutral` | `border.default` hairline | `ref.palette.white.1000` (fixed white) |
|
|
94
|
+
| active | `sys.color.background.primary` | none | `sys.color.text.onFill` |
|
|
97
95
|
|
|
98
96
|
## States
|
|
99
97
|
|
|
@@ -106,7 +104,7 @@ A single appearance — no emphasis axis. The visible variation is the `data-sta
|
|
|
106
104
|
|
|
107
105
|
## Focus indicator
|
|
108
106
|
|
|
109
|
-
Outward
|
|
107
|
+
Outward single ring on the track's outer edge via an `::after` overlay. Trigger: `:focus-visible`. Switch sits inline next to siblings with whitespace around it, so outward reads cleanly — see [Focus ring composition](../../DESIGN.md#focus-ring-composition).
|
|
110
108
|
|
|
111
109
|
## Behavior
|
|
112
110
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "../../spec.schema.json",
|
|
3
3
|
"name": "Switch",
|
|
4
4
|
"family": "switch",
|
|
5
|
-
"description": "Pill-shaped track (52 × 32) with a circular thumb (28 × 28) that translates between the two ends. Inactive paints a `
|
|
5
|
+
"description": "Pill-shaped track (52 × 32) with a circular thumb (28 × 28) that translates between the two ends. Inactive paints a `icon.subtlest` track (a muted neutral grey that stays distinct on any host surface) with an `border.default` hairline stroke and a fixed-white thumb (`ref.palette.white.1000`, identical in light and dark); active paints the track in `primary` and the thumb in `onPrimary` so the active state reads chromatically. Whole control is the click target — the thumb is decorative. Instant commit: `onCheckedChange` fires the moment the thumb moves.",
|
|
6
6
|
"element": "button",
|
|
7
7
|
"props": {
|
|
8
8
|
"checked": {
|
|
@@ -62,21 +62,21 @@
|
|
|
62
62
|
"thumbTravel": "ref.space.250",
|
|
63
63
|
"thumbRadius": "sys.radius.full",
|
|
64
64
|
"outlineWidth": "sys.borderWidth.hairline",
|
|
65
|
-
"outlineColor": "sys.color.
|
|
65
|
+
"outlineColor": "sys.color.border.default",
|
|
66
66
|
"transitionDuration": "120ms",
|
|
67
67
|
"transitionTiming": "ease-out"
|
|
68
68
|
},
|
|
69
69
|
"appearances": {
|
|
70
70
|
"inactive": {
|
|
71
|
-
"trackBackground": "sys.color.
|
|
72
|
-
"trackOutline": "sys.color.
|
|
71
|
+
"trackBackground": "sys.color.icon.subtlest",
|
|
72
|
+
"trackOutline": "sys.color.border.default",
|
|
73
73
|
"thumbBackground": "ref.palette.white.1000",
|
|
74
|
-
"note": "Resting state. The track fills with `
|
|
74
|
+
"note": "Resting state. The track fills with `icon.subtlest` — a muted neutral grey that stays distinct on every host surface tier, reinforced by the hairline outline. The thumb is fixed white (`ref.palette.white.1000`) so it reads identically in light and dark rather than dimming to a surface tone."
|
|
75
75
|
},
|
|
76
76
|
"active": {
|
|
77
|
-
"trackBackground": "sys.color.primary",
|
|
77
|
+
"trackBackground": "sys.color.background.primary",
|
|
78
78
|
"trackOutline": "transparent",
|
|
79
|
-
"thumbBackground": "sys.color.
|
|
79
|
+
"thumbBackground": "sys.color.text.onFill",
|
|
80
80
|
"note": "Chromatic active state. The outline disappears so the filled track reads as one solid block. Thumb steps to `onPrimary` so the contrast against the filled track stays legible."
|
|
81
81
|
}
|
|
82
82
|
},
|
|
@@ -88,21 +88,29 @@
|
|
|
88
88
|
"pressed": {
|
|
89
89
|
"overlay": { "color": "label", "opacity": "sys.state.pressed" }
|
|
90
90
|
},
|
|
91
|
+
"focused": {
|
|
92
|
+
"focusRing": {
|
|
93
|
+
"composition": "outward",
|
|
94
|
+
"layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
|
|
95
|
+
"innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.border.focused" },
|
|
96
|
+
"outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.border.focused" }
|
|
97
|
+
},
|
|
98
|
+
"note": "Keyboard-focus (:focus-visible) visual — a single outward ring on the track's outer edge, with no state-overlay tint (the ring alone carries focus here). Mirrors the `focusIndicator` block for spec-only renderers. Composes over the lifecycle state the control is in."
|
|
99
|
+
},
|
|
91
100
|
"disabled": {
|
|
92
|
-
"
|
|
101
|
+
"trackBackground": "sys.color.icon.disabled",
|
|
102
|
+
"trackOutline": "sys.color.border.bold",
|
|
93
103
|
"pointerEvents": "none",
|
|
94
|
-
"note": "
|
|
104
|
+
"note": "Explicit disabled (no opacity): track to icon.disabled (both checked and unchecked), hairline outline to border.bold; the thumb stays white at the position set by checked."
|
|
95
105
|
}
|
|
96
106
|
},
|
|
97
107
|
"focusIndicator": {
|
|
98
|
-
"description": "Keyboard-focus visual painted as a
|
|
108
|
+
"description": "Keyboard-focus visual painted as a single outward ring on the track's outer edge.",
|
|
99
109
|
"composition": "outward",
|
|
100
110
|
"compositionReason": "Switch sits inline next to siblings with whitespace around it (form rows, settings list trailing slots); an outward ring reads cleanly without colliding with neighbouring affordances.",
|
|
101
111
|
"ring": {
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
105
|
-
"insetColor": "sys.color.focusInset",
|
|
112
|
+
"width": "sys.borderWidth.hairline",
|
|
113
|
+
"color": "sys.color.border.focused",
|
|
106
114
|
"implementation": "outset box-shadow on the track's `::after` overlay."
|
|
107
115
|
},
|
|
108
116
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
@@ -114,7 +122,7 @@
|
|
|
114
122
|
},
|
|
115
123
|
"forbidden": [
|
|
116
124
|
"switch used for an action that needs confirmation (a destructive commit, an action with an undo) — wrap that in a Button + Dialog instead",
|
|
117
|
-
"track outline removed in the inactive state — the `
|
|
125
|
+
"track outline removed in the inactive state — the `icon.subtlest` fill and the hairline outline work together to hold the affordance on any host surface; dropping the outline weakens the edge on tiers where the tint reads faint",
|
|
118
126
|
"inactive thumb dimmed to a surface tone — it stays fixed white (`ref.palette.white.1000`) so it reads the same in light and dark",
|
|
119
127
|
"thumb rendered as a separate focusable / clickable element — only the track carries focus and click",
|
|
120
128
|
"trailing label or icon painted inside the track — the active/inactive contract is the whole control; any label sits outside the Switch (in a List row label, a FormField label, etc.)",
|
|
@@ -33,11 +33,9 @@ import { HomeIcon, HomeFillIcon, BuildingIcon, BuildingFillIcon, SearchIcon, Bri
|
|
|
33
33
|
/>
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## Primary item
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
A bar including a primary-coloured Create affordance at the trailing end. The icon ([`PlusSquareFillIcon`](../../icons/svg/PlusSquareFill.svg)) is painted in `sys.color.brand` via `appearance="primary"` and still occupies one equal-width slot.
|
|
38
|
+
A bar including a primary-coloured Create affordance at the trailing end. The icon ([`PlusSquareFillIcon`](../../icons/svg/PlusSquareFill.svg)) is painted in `sys.color.text.brand` via `appearance="primary"` and still occupies one equal-width slot.
|
|
41
39
|
|
|
42
40
|
```preview
|
|
43
41
|
tab-bar/with-primary
|
|
@@ -59,7 +57,7 @@ import { HomeIcon, HomeFillIcon, BuildingIcon, BuildingFillIcon, SearchIcon, Sea
|
|
|
59
57
|
/>
|
|
60
58
|
```
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
## Three destinations
|
|
63
61
|
|
|
64
62
|
A bar with a smaller destination set — `space-evenly` distribution scales to any item count.
|
|
65
63
|
|
|
@@ -80,7 +78,7 @@ import { HomeIcon, HomeFillIcon, SearchIcon, SearchFillIcon, ProfileIcon, Profil
|
|
|
80
78
|
/>
|
|
81
79
|
```
|
|
82
80
|
|
|
83
|
-
|
|
81
|
+
## Truncation
|
|
84
82
|
|
|
85
83
|
Labels exceeding their slot truncate with a single-line ellipsis — a safety net for long i18n strings.
|
|
86
84
|
|
|
@@ -103,7 +101,7 @@ import { HomeIcon, HomeFillIcon, BuildingIcon, BuildingFillIcon, SearchIcon, Sea
|
|
|
103
101
|
/>
|
|
104
102
|
```
|
|
105
103
|
|
|
106
|
-
|
|
104
|
+
## Focus specimen
|
|
107
105
|
|
|
108
106
|
Static specimen — pins the keyboard-focus ring to a single destination. See top-level [Focus indicator](#focus-indicator).
|
|
109
107
|
|
|
@@ -137,11 +135,11 @@ import { HomeIcon, HomeFillIcon, BuildingIcon, BuildingFillIcon, SearchIcon, Sea
|
|
|
137
135
|
|
|
138
136
|
| Slot | Token bindings |
|
|
139
137
|
|-----------|------------------------------------------------------------------------------------------------------|
|
|
140
|
-
| container | `surface` fill; top hairline `
|
|
138
|
+
| container | `surface` fill; top hairline `border.default` divider (inset shadow); `display: flex` + `justify-content: space-evenly` |
|
|
141
139
|
| item | Flex column, icon over label; `flex: 1 1 0` with `max-width: 80px`; tap target is the full slot. State layer is a `sys.radius.md` rounded rectangle filling the slot |
|
|
142
|
-
| icon | `sys.color.
|
|
143
|
-
| label | `sys.typo.
|
|
144
|
-
| primary | When `appearance="primary"`, only the icon paints in `sys.color.brand`; the label stays in the bar's default `sys.color.
|
|
140
|
+
| icon | `sys.color.text.subtle` → `sys.color.text.default` (active) |
|
|
141
|
+
| label | `sys.typo.label.xs` (10 / Semibold); `onSurfaceVariant` → `onSurface` (active) |
|
|
142
|
+
| primary | When `appearance="primary"`, only the icon paints in `sys.color.text.brand`; the label stays in the bar's default `sys.color.text.subtle` so every label across the row reads as one rung. Pair with a filled-tile glyph (e.g. [`PlusSquareFillIcon`](../../icons/svg/PlusSquareFill.svg)) |
|
|
145
143
|
|
|
146
144
|
## Sizes
|
|
147
145
|
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"sizing": {
|
|
57
|
-
"containerFill": "sys.color.surface",
|
|
57
|
+
"containerFill": "sys.color.surface.default",
|
|
58
58
|
"containerMinHeight": "56px",
|
|
59
59
|
"containerPaddingBlock": "0",
|
|
60
60
|
"containerPaddingBlockEnd": "env(safe-area-inset-bottom, 0px)",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"containerJustifyContent": "space-evenly",
|
|
64
64
|
"containerTopDivider": {
|
|
65
65
|
"width": "sys.borderWidth.hairline",
|
|
66
|
-
"color": "sys.color.
|
|
66
|
+
"color": "sys.color.border.default",
|
|
67
67
|
"implementation": "inset box-shadow on the container (does not contribute to layout)"
|
|
68
68
|
},
|
|
69
69
|
"itemFlex": "1 1 0",
|
|
@@ -72,17 +72,17 @@
|
|
|
72
72
|
"itemPaddingInline": "0",
|
|
73
73
|
"itemIconLabelGap": "sys.layout.stack.3xs",
|
|
74
74
|
"iconSize": "sys.icon.lg",
|
|
75
|
-
"labelTypo": "sys.typo.
|
|
75
|
+
"labelTypo": "sys.typo.label.xs",
|
|
76
76
|
"labelOverflow": "ellipsis (overflow: hidden; text-overflow: ellipsis; white-space: nowrap)",
|
|
77
77
|
"stateLayerInset": "0",
|
|
78
78
|
"stateLayerRadius": "sys.radius.md"
|
|
79
79
|
},
|
|
80
80
|
"appearance": {
|
|
81
|
-
"itemInactiveColor": "sys.color.
|
|
82
|
-
"itemActiveColor": "sys.color.
|
|
83
|
-
"itemPrimaryIconColor": "sys.color.brand",
|
|
84
|
-
"itemPrimaryLabelColor": "sys.color.
|
|
85
|
-
"note": "Primary (`appearance='primary'`) items paint only the icon in `sys.color.brand`; the label stays in the bar's default `onSurfaceVariant` so every label across the row reads as one rung."
|
|
81
|
+
"itemInactiveColor": "sys.color.text.subtle",
|
|
82
|
+
"itemActiveColor": "sys.color.text.default",
|
|
83
|
+
"itemPrimaryIconColor": "sys.color.text.brand",
|
|
84
|
+
"itemPrimaryLabelColor": "sys.color.text.subtle",
|
|
85
|
+
"note": "Primary (`appearance='primary'`) items paint only the icon in `sys.color.text.brand`; the label stays in the bar's default `onSurfaceVariant` so every label across the row reads as one rung."
|
|
86
86
|
},
|
|
87
87
|
"itemProps": {
|
|
88
88
|
"value": {
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"primary"
|
|
118
118
|
],
|
|
119
119
|
"optional": true,
|
|
120
|
-
"description": "Paint the icon in `sys.color.brand` — the 'Create' / 'Compose' commit affordance. Pair with a filled-tile glyph (e.g. PlusSquareFillIcon) so the icon's own shape provides the tile and the brand fill paints it as the commit colour. The label stays in the bar's default `onSurfaceVariant` so the row's labels read as one rung — only the icon carries the brand emphasis. Primary items invoke a screen-covering overlay, NOT a sibling destination — they render in the fill glyph by default, never receive `aria-current='page'`, and never animate outline → fill (no resting/active states inside the bar)."
|
|
120
|
+
"description": "Paint the icon in `sys.color.text.brand` — the 'Create' / 'Compose' commit affordance. Pair with a filled-tile glyph (e.g. PlusSquareFillIcon) so the icon's own shape provides the tile and the brand fill paints it as the commit colour. The label stays in the bar's default `onSurfaceVariant` so the row's labels read as one rung — only the icon carries the brand emphasis. Primary items invoke a screen-covering overlay, NOT a sibling destination — they render in the fill glyph by default, never receive `aria-current='page'`, and never animate outline → fill (no resting/active states inside the bar)."
|
|
121
121
|
},
|
|
122
122
|
"forcedState": {
|
|
123
123
|
"type": "literal",
|
|
@@ -136,39 +136,53 @@
|
|
|
136
136
|
},
|
|
137
137
|
"hovered": {
|
|
138
138
|
"description": "Pointer over the item. State layer fills the slot, painted with `onSurface` at `sys.state.hover` (8%).",
|
|
139
|
-
"stateLayerFill": "color-mix(sys.color.
|
|
139
|
+
"stateLayerFill": "color-mix(sys.color.text.default, sys.state.hover)"
|
|
140
140
|
},
|
|
141
141
|
"pressed": {
|
|
142
142
|
"description": "Active press on the item. State layer fills the slot, painted with `onSurface` at `sys.state.pressed` (12%).",
|
|
143
|
-
"stateLayerFill": "color-mix(sys.color.
|
|
143
|
+
"stateLayerFill": "color-mix(sys.color.text.default, sys.state.pressed)"
|
|
144
|
+
},
|
|
145
|
+
"focused": {
|
|
146
|
+
"stateLayerFill": "color-mix(sys.color.text.default, sys.state.focus)",
|
|
147
|
+
"focusRing": {
|
|
148
|
+
"composition": "inward",
|
|
149
|
+
"layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
|
|
150
|
+
"innerCounterRing": {
|
|
151
|
+
"width": "sys.borderWidth.hairline",
|
|
152
|
+
"color": "sys.color.border.focused"
|
|
153
|
+
},
|
|
154
|
+
"outerRing": {
|
|
155
|
+
"width": "sys.borderWidth.thin",
|
|
156
|
+
"color": "sys.color.border.focused"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"note": "Keyboard-focus (:focus-visible) visual. State layer fills the slot with `onSurface` at `sys.state.focus` (12%); the single ring is forced inward (adjacent items are flush under flex:1 1 0). Mirrors the `focusIndicator` block for spec-only renderers. Single-focus: at most one item holds the ring."
|
|
144
160
|
},
|
|
145
161
|
"active": {
|
|
146
162
|
"description": "Currently selected destination. Filled glyph (`activeIcon`) + label, both at `onSurface`. Carries `aria-current='page'`. No persistent state layer — only hover / pressed / focus paint."
|
|
147
163
|
},
|
|
148
164
|
"disabled": {
|
|
149
|
-
"
|
|
150
|
-
"
|
|
165
|
+
"icon": "sys.color.icon.disabled",
|
|
166
|
+
"label": "sys.color.text.disabled",
|
|
167
|
+
"pointerEvents": "none",
|
|
168
|
+
"note": "Explicit disabled (no opacity): glyph to icon.disabled, label to text.disabled. The bar surface is untouched."
|
|
151
169
|
}
|
|
152
170
|
},
|
|
153
171
|
"focusIndicator": {
|
|
154
172
|
"description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the item is in. The state layer beneath is filled with `onSurface` at `sys.state.focus` (12%). Single-focus: at most one item holds the ring at a time, arriving via `:focus-visible` (keyboard `Tab` / programmatic focus).",
|
|
155
173
|
"composition": "inward",
|
|
156
174
|
"compositionReason": "Adjacent items are flush under `flex: 1 1 0`; an outward ring would overlap the neighbouring slot. The ring is constrained strictly inside the slot's bounding box and never exceeds it.",
|
|
157
|
-
"stateLayerFill": "color-mix(sys.color.
|
|
175
|
+
"stateLayerFill": "color-mix(sys.color.text.default, sys.state.focus)",
|
|
158
176
|
"ring": {
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"outerLayerPosition": "depth 0..2px from the slot edge (the outer stroke)",
|
|
162
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
163
|
-
"insetColor": "sys.color.focusInset",
|
|
164
|
-
"insetLayerPosition": "depth 2..3px from the slot edge (the counter-ring just inside the outer stroke)",
|
|
177
|
+
"width": "sys.borderWidth.hairline",
|
|
178
|
+
"color": "sys.color.border.focused",
|
|
165
179
|
"composition": "inward — inset box-shadow on the ::after at `inset: 0`. Adjacent items are flush under `flex: 1 1 0`, so an outward ring would overlap a neighbour; the ring is forced inside the slot bounds without exceeding them."
|
|
166
180
|
},
|
|
167
181
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
168
182
|
},
|
|
169
183
|
"behavior": {
|
|
170
184
|
"selectionNavigates": "Tapping a destination item routes the app to that destination's page; `value` updates to mark the new active item. The selected item swaps its glyph from the outline form to the filled companion (`activeIcon`) to signal 'you are here' — the icon fill, paired with the `onSurface` colour swap on icon + label, is the bar's primary current-location indicator.",
|
|
171
|
-
"primaryItemIsAnOverlayAction": "Items with `appearance='primary'` (the conventional Create / Compose affordance) invoke a screen-covering overlay rather than navigating to a sibling destination, so they have no resting/active distinction inside the bar. They render in the fill-type glyph by default (painted in `sys.color.brand`) and never receive `aria-current='page'` or the outline→fill transition. Activation dismisses or replaces the current view via the host framework's overlay/modal route, not via tab-bar selection.",
|
|
185
|
+
"primaryItemIsAnOverlayAction": "Items with `appearance='primary'` (the conventional Create / Compose affordance) invoke a screen-covering overlay rather than navigating to a sibling destination, so they have no resting/active distinction inside the bar. They render in the fill-type glyph by default (painted in `sys.color.text.brand`) and never receive `aria-current='page'` or the outline→fill transition. Activation dismisses or replaces the current view via the host framework's overlay/modal route, not via tab-bar selection.",
|
|
172
186
|
"distribution": "Capped equal-grow plus optical alignment. Every item uses `flex: 1 1 0` with `max-width: 80px`, so items grow at the same rate up to an 80-wide cap. Once every item has hit the cap, the row's leftover inline space is handed to the container's `justify-content: space-evenly`, which paints start padding, inter-item gap, and end padding as the same visible whitespace. `min-width: 0` on the item lets the label's ellipsis truncation actually take effect inside the slot.",
|
|
173
187
|
"labelTruncation": "Labels that exceed their slot truncate with a single-line ellipsis (`overflow: hidden; text-overflow: ellipsis; white-space: nowrap`). The row never wraps or scrolls.",
|
|
174
188
|
"fixedRow": "Author destinations to a five- or six-item ceiling so the per-slot width stays wide enough for the destination's short name to read without ellipsis at the system's narrowest mobile breakpoint.",
|
|
@@ -176,9 +190,9 @@
|
|
|
176
190
|
"noNativeHomeBar": "The component renders just the bar itself; it does not draw or reserve space for an OS-level home indicator. The host shell handles safe-area inset and pinning."
|
|
177
191
|
},
|
|
178
192
|
"forbidden": [
|
|
179
|
-
"Create item not styled with sys.color.brand fill — the Create entry is the single brand-marked instance per screen",
|
|
193
|
+
"Create item not styled with sys.color.text.brand fill — the Create entry is the single brand-marked instance per screen",
|
|
180
194
|
"more than one Create / brand-marked item — brand instance cap on this row is exactly 1",
|
|
181
195
|
"tab-bar height different from the spec'd geometry — the row geometry is fixed across all routes",
|
|
182
|
-
"active state for non-Create items painted with brand color — non-Create items use sys.color.
|
|
196
|
+
"active state for non-Create items painted with brand color — non-Create items use sys.color.text.default for both rest and active (active darkens via state overlay, not by color swap)"
|
|
183
197
|
]
|
|
184
198
|
}
|
|
@@ -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
|
|