@teamblind-chorus/ui 1.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/LICENSE +21 -0
- package/README.md +112 -0
- package/agents/AGENTS.md +143 -0
- package/agents/DESIGN.md +1311 -0
- package/agents/LOVABLE.md +472 -0
- package/agents/anti-patterns.md +533 -0
- package/agents/catalog.md +232 -0
- package/agents/components/avatar-rail/avatar-rail.family.json +46 -0
- package/agents/components/avatar-rail/avatar-rail.md +103 -0
- package/agents/components/avatar-rail/avatar-rail.spec.json +160 -0
- package/agents/components/badge/badge.family.json +45 -0
- package/agents/components/badge/badge.md +10 -0
- package/agents/components/badge/role.md +100 -0
- package/agents/components/badge/role.spec.json +75 -0
- package/agents/components/badge/update.md +132 -0
- package/agents/components/badge/update.spec.json +114 -0
- package/agents/components/banner/banner.family.json +28 -0
- package/agents/components/banner/banner.md +136 -0
- package/agents/components/banner/banner.spec.json +136 -0
- package/agents/components/bottom-sheet/bottom-sheet.family.json +29 -0
- package/agents/components/bottom-sheet/bottom-sheet.md +176 -0
- package/agents/components/bottom-sheet/bottom-sheet.spec.json +168 -0
- package/agents/components/bubble/bubble.family.json +29 -0
- package/agents/components/bubble/bubble.md +134 -0
- package/agents/components/bubble/bubble.spec.json +91 -0
- package/agents/components/button/button.family.json +76 -0
- package/agents/components/button/button.md +31 -0
- package/agents/components/button/check.md +138 -0
- package/agents/components/button/check.spec.json +161 -0
- package/agents/components/button/fab.md +161 -0
- package/agents/components/button/fab.spec.json +106 -0
- package/agents/components/button/icon.md +141 -0
- package/agents/components/button/icon.spec.json +164 -0
- package/agents/components/button/standard.md +219 -0
- package/agents/components/button/standard.spec.json +205 -0
- package/agents/components/button/text.md +186 -0
- package/agents/components/button/text.spec.json +215 -0
- package/agents/components/button/toggle.md +108 -0
- package/agents/components/button/toggle.spec.json +124 -0
- package/agents/components/button/toolbar.md +189 -0
- package/agents/components/button/toolbar.spec.json +109 -0
- package/agents/components/carousel/carousel.family.json +41 -0
- package/agents/components/carousel/carousel.md +40 -0
- package/agents/components/carousel/post.md +148 -0
- package/agents/components/carousel/post.spec.json +229 -0
- package/agents/components/carousel/profile.md +184 -0
- package/agents/components/carousel/profile.spec.json +219 -0
- package/agents/components/chip/chip.family.json +37 -0
- package/agents/components/chip/chip.md +10 -0
- package/agents/components/chip/filter.md +212 -0
- package/agents/components/chip/filter.spec.json +124 -0
- package/agents/components/chip/tag.md +137 -0
- package/agents/components/chip/tag.spec.json +104 -0
- package/agents/components/dialog/dialog.family.json +29 -0
- package/agents/components/dialog/dialog.md +113 -0
- package/agents/components/dialog/dialog.spec.json +156 -0
- package/agents/components/directory-list/directory-list.family.json +46 -0
- package/agents/components/directory-list/directory-list.md +87 -0
- package/agents/components/directory-list/directory-list.spec.json +104 -0
- package/agents/components/divider/divider.family.json +28 -0
- package/agents/components/divider/divider.md +78 -0
- package/agents/components/divider/divider.spec.json +51 -0
- package/agents/components/feed/ad.md +108 -0
- package/agents/components/feed/ad.spec.json +187 -0
- package/agents/components/feed/feed.family.json +48 -0
- package/agents/components/feed/feed.md +30 -0
- package/agents/components/feed/post.md +240 -0
- package/agents/components/feed/post.spec.json +361 -0
- package/agents/components/form-field/form-field.family.json +50 -0
- package/agents/components/form-field/form-field.md +11 -0
- package/agents/components/form-field/input.md +198 -0
- package/agents/components/form-field/input.spec.json +202 -0
- package/agents/components/form-field/search.md +81 -0
- package/agents/components/form-field/search.spec.json +135 -0
- package/agents/components/form-field/select.md +101 -0
- package/agents/components/form-field/select.spec.json +194 -0
- package/agents/components/form-field/textarea.md +89 -0
- package/agents/components/form-field/textarea.spec.json +176 -0
- package/agents/components/header/header.family.json +43 -0
- package/agents/components/header/header.md +18 -0
- package/agents/components/header/main.md +101 -0
- package/agents/components/header/main.spec.json +117 -0
- package/agents/components/header/sub.md +129 -0
- package/agents/components/header/sub.spec.json +81 -0
- package/agents/components/list/accordion.md +183 -0
- package/agents/components/list/accordion.spec.json +201 -0
- package/agents/components/list/entry.md +280 -0
- package/agents/components/list/entry.spec.json +237 -0
- package/agents/components/list/list.family.json +75 -0
- package/agents/components/list/list.md +24 -0
- package/agents/components/list/radio.md +144 -0
- package/agents/components/list/radio.spec.json +186 -0
- package/agents/components/list/standard.md +262 -0
- package/agents/components/list/standard.spec.json +221 -0
- package/agents/components/metadata/compact.md +69 -0
- package/agents/components/metadata/compact.spec.json +69 -0
- package/agents/components/metadata/metadata.family.json +42 -0
- package/agents/components/metadata/metadata.md +26 -0
- package/agents/components/metadata/standard.md +104 -0
- package/agents/components/metadata/standard.spec.json +152 -0
- package/agents/components/nav-card/nav-card.family.json +29 -0
- package/agents/components/nav-card/nav-card.md +179 -0
- package/agents/components/nav-card/nav-card.spec.json +161 -0
- package/agents/components/nav-list/nav-list.family.json +46 -0
- package/agents/components/nav-list/nav-list.md +91 -0
- package/agents/components/nav-list/nav-list.spec.json +107 -0
- package/agents/components/navigation-bar/main.md +201 -0
- package/agents/components/navigation-bar/main.spec.json +109 -0
- package/agents/components/navigation-bar/navigation-bar.family.json +44 -0
- package/agents/components/navigation-bar/navigation-bar.md +21 -0
- package/agents/components/navigation-bar/search.md +96 -0
- package/agents/components/navigation-bar/search.spec.json +142 -0
- package/agents/components/navigation-bar/sub.md +174 -0
- package/agents/components/navigation-bar/sub.spec.json +123 -0
- package/agents/components/page-shell/page-shell.family.json +22 -0
- package/agents/components/page-shell/page-shell.md +51 -0
- package/agents/components/profile-header/profile-header.family.json +29 -0
- package/agents/components/profile-header/profile-header.md +149 -0
- package/agents/components/profile-header/profile-header.spec.json +200 -0
- package/agents/components/progress/progress.family.json +27 -0
- package/agents/components/progress/progress.md +38 -0
- package/agents/components/progress/progress.spec.json +67 -0
- package/agents/components/side-sheet/side-sheet.family.json +30 -0
- package/agents/components/side-sheet/side-sheet.md +154 -0
- package/agents/components/side-sheet/side-sheet.spec.json +109 -0
- package/agents/components/skeleton/skeleton.family.json +28 -0
- package/agents/components/skeleton/skeleton.md +123 -0
- package/agents/components/skeleton/skeleton.spec.json +73 -0
- package/agents/components/status-tag/status-tag.family.json +26 -0
- package/agents/components/status-tag/status-tag.md +114 -0
- package/agents/components/status-tag/status-tag.spec.json +69 -0
- package/agents/components/suggestion-list/suggestion-list.family.json +46 -0
- package/agents/components/suggestion-list/suggestion-list.md +91 -0
- package/agents/components/suggestion-list/suggestion-list.spec.json +178 -0
- package/agents/components/switch/switch.family.json +27 -0
- package/agents/components/switch/switch.md +114 -0
- package/agents/components/switch/switch.spec.json +123 -0
- package/agents/components/tab-bar/tab-bar.family.json +27 -0
- package/agents/components/tab-bar/tab-bar.md +178 -0
- package/agents/components/tab-bar/tab-bar.spec.json +184 -0
- package/agents/components/tabs/rounded.md +150 -0
- package/agents/components/tabs/rounded.spec.json +140 -0
- package/agents/components/tabs/segmented.md +114 -0
- package/agents/components/tabs/segmented.spec.json +100 -0
- package/agents/components/tabs/tabs.family.json +59 -0
- package/agents/components/tabs/tabs.md +18 -0
- package/agents/components/tabs/underline.md +147 -0
- package/agents/components/tabs/underline.spec.json +139 -0
- package/agents/components/thumbnail/thumbnail.family.json +28 -0
- package/agents/components/thumbnail/thumbnail.md +152 -0
- package/agents/components/thumbnail/thumbnail.spec.json +172 -0
- package/agents/components/toast/toast.family.json +28 -0
- package/agents/components/toast/toast.md +133 -0
- package/agents/components/toast/toast.spec.json +89 -0
- package/agents/components/tooltip/tooltip.family.json +29 -0
- package/agents/components/tooltip/tooltip.md +139 -0
- package/agents/components/tooltip/tooltip.spec.json +110 -0
- package/agents/compose.md +240 -0
- package/agents/icons.json +831 -0
- package/agents/images.md +66 -0
- package/agents/manifest.json +87 -0
- package/agents/patterns/README.md +59 -0
- package/agents/patterns/actions.md +50 -0
- package/agents/patterns/browsing.md +52 -0
- package/agents/patterns/communications.md +56 -0
- package/agents/patterns/layout.md +72 -0
- package/agents/patterns/modals.md +50 -0
- package/agents/patterns/visual.md +55 -0
- package/agents/reconstruct.md +55 -0
- package/agents/scoped-adoption.md +111 -0
- package/agents/tokens.usage.json +1657 -0
- package/agents/usage.json +422 -0
- package/dist/icons/index.cjs +1332 -0
- package/dist/icons/index.cjs.map +1 -0
- package/dist/icons/index.d.cts +228 -0
- package/dist/icons/index.d.ts +228 -0
- package/dist/icons/index.js +1114 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/index.cjs +5905 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +896 -0
- package/dist/index.d.ts +896 -0
- package/dist/index.js +5847 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +5765 -0
- package/eslint/README.md +79 -0
- package/eslint/index.js +78 -0
- package/eslint/rules.js +472 -0
- package/eslint/test.mjs +135 -0
- package/package.json +96 -0
- package/placeholder.png +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Rounded
|
|
2
|
+
|
|
3
|
+
Rounded-rectangle tab row — each tab a self-contained chip with a required leading icon and label. Shares chrome with [Segmented](./segmented.md) and [Filter chip](../chip/filter.md) verbatim; the single divergence is corner radius, which steps from `sys.radius.full` (capsule) to `sys.radius.md` (8) — reads as a soft rounded rectangle, not a pill.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the row is a sort/filter switcher inside content. **Skip when** the row anchors content sections — use [Underline](./underline.md) — or when it's an in-place mode change — use [Segmented](./segmented.md).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** full-bleed — edge-to-edge family. Sits as a direct child of the page shell and stretches edge-to-edge; the row pays its own `16px inline / 8px block` padding via `layout.container.*` — do **not** wrap it in another padding div, or the page rail double-pays. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
Bare headline form — labels carry meaning, no glyphs.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
tabs/rounded/default
|
|
15
|
+
---
|
|
16
|
+
import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<Tabs variant="rounded" value="latest" onChange={setValue} aria-label="Sort">
|
|
19
|
+
<Tab value="latest">Latest</Tab>
|
|
20
|
+
<Tab value="popular">Popular</Tab>
|
|
21
|
+
<Tab value="following">Following</Tab>
|
|
22
|
+
</Tabs>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Use cases
|
|
26
|
+
|
|
27
|
+
### With icon
|
|
28
|
+
|
|
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.
|
|
30
|
+
|
|
31
|
+
```preview
|
|
32
|
+
tabs/rounded/leading-icon
|
|
33
|
+
---
|
|
34
|
+
import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
35
|
+
import { PulseIcon, StarIcon, HeartIcon, BookmarkIcon } from '@teamblind-chorus/ui/icons';
|
|
36
|
+
|
|
37
|
+
<Tabs variant="rounded" value="latest" onChange={setValue} aria-label="Sort">
|
|
38
|
+
<Tab value="latest" leadingIcon={<PulseIcon />}>Latest</Tab>
|
|
39
|
+
<Tab value="popular" leadingIcon={<StarIcon />}>Popular</Tab>
|
|
40
|
+
<Tab value="favorites" leadingIcon={<HeartIcon />}>Favorites</Tab>
|
|
41
|
+
<Tab value="saved" leadingIcon={<BookmarkIcon />}>Saved</Tab>
|
|
42
|
+
</Tabs>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Icon only
|
|
46
|
+
|
|
47
|
+
Glyph-only tab — collapses to a clean 32×32 square (inline padding 12 → 8). Requires `aria-label`.
|
|
48
|
+
|
|
49
|
+
```preview
|
|
50
|
+
tabs/rounded/icon-only
|
|
51
|
+
---
|
|
52
|
+
import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
53
|
+
import { StarIcon, BookmarkIcon, HeartIcon } from '@teamblind-chorus/ui/icons';
|
|
54
|
+
|
|
55
|
+
<Tabs variant="rounded" value="featured" onChange={setValue} aria-label="View">
|
|
56
|
+
<Tab value="featured" leadingIcon={<StarIcon />} aria-label="Featured" />
|
|
57
|
+
<Tab value="saved" leadingIcon={<BookmarkIcon />} aria-label="Saved" />
|
|
58
|
+
<Tab value="loved" leadingIcon={<HeartIcon />} aria-label="Loved" />
|
|
59
|
+
</Tabs>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Overflow
|
|
63
|
+
|
|
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.
|
|
65
|
+
|
|
66
|
+
```preview
|
|
67
|
+
tabs/rounded/overflow
|
|
68
|
+
---
|
|
69
|
+
import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
70
|
+
import { PulseIcon, StarIcon, HeartIcon, BookmarkIcon, TagIcon, ProfileIcon, MentionIcon } from '@teamblind-chorus/ui/icons';
|
|
71
|
+
|
|
72
|
+
<Tabs variant="rounded" value="latest" aria-label="Feed">
|
|
73
|
+
<Tab value="latest" leadingIcon={<PulseIcon />}>Latest</Tab>
|
|
74
|
+
<Tab value="popular" leadingIcon={<StarIcon />}>Popular</Tab>
|
|
75
|
+
<Tab value="favorites" leadingIcon={<HeartIcon />}>Favorites</Tab>
|
|
76
|
+
<Tab value="saved" leadingIcon={<BookmarkIcon />}>Saved</Tab>
|
|
77
|
+
<Tab value="topics" leadingIcon={<TagIcon />}>Topics</Tab>
|
|
78
|
+
<Tab value="people" leadingIcon={<ProfileIcon />}>People</Tab>
|
|
79
|
+
<Tab value="mentions" leadingIcon={<MentionIcon />}>Mentions</Tab>
|
|
80
|
+
</Tabs>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Focus indicator
|
|
84
|
+
|
|
85
|
+
Static specimen — pins the focus ring to the selected tab. See top-level [Focus indicator](#focus-indicator).
|
|
86
|
+
|
|
87
|
+
```preview
|
|
88
|
+
tabs/rounded/focused
|
|
89
|
+
---
|
|
90
|
+
import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
91
|
+
|
|
92
|
+
<Tabs variant="rounded" value="latest" aria-label="Sort">
|
|
93
|
+
<Tab value="latest" state="focused">Latest</Tab>
|
|
94
|
+
<Tab value="popular">Popular</Tab>
|
|
95
|
+
<Tab value="following">Following</Tab>
|
|
96
|
+
</Tabs>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Slots
|
|
100
|
+
|
|
101
|
+
- **label** — accessible name. Single line. Optional only when icon-only — requires `aria-label`.
|
|
102
|
+
- **leadingIcon** (optional) — 16px (`sys.icon.md`) glyph. May carry its own brand/category color.
|
|
103
|
+
|
|
104
|
+
At least one of `label` / `leadingIcon` must be present.
|
|
105
|
+
|
|
106
|
+
## Anatomy
|
|
107
|
+
|
|
108
|
+
Selected/unselected pairs are inherited verbatim from [Filter chip's variants](../chip/filter.md#variants).
|
|
109
|
+
|
|
110
|
+
| Prop / state | Container | Label color | Border (always 1px `sys.borderWidth.hairline`) |
|
|
111
|
+
|------------------------|------------------------------------|-----------------------------------|-------------------------------------------------------------------------|
|
|
112
|
+
| **Tab — unselected** | `transparent` | `sys.color.onSurface` | `sys.color.outlineVariant` |
|
|
113
|
+
| **Tab — selected** | `sys.color.inverseSurface` | `sys.color.inverseOnSurface` | `transparent` — 1px width held so footprint never changes between states |
|
|
114
|
+
|
|
115
|
+
## Sizes
|
|
116
|
+
|
|
117
|
+
A single fixed rung. Every dimension other than the corner radius matches [Segmented](./segmented.md#sizes) and [Filter chip](../chip/filter.md#sizes).
|
|
118
|
+
|
|
119
|
+
| Property | Value | Token |
|
|
120
|
+
|-----------------------------------|----------------------|-------------------------------------|
|
|
121
|
+
| Container background | transparent | — |
|
|
122
|
+
| Container padding (block × inline)| 16 × 16 | `sys.layout.container.md` × `sys.layout.container.md` |
|
|
123
|
+
| Min-height | 32px | `ref.space.400` ‡ |
|
|
124
|
+
| Padding (block × inline) | 4 × 12 | `sys.layout.container.2xs` × `sys.layout.container.sm` |
|
|
125
|
+
| Label inset (within label slot) | 4px (horizontal) | `sys.layout.container.2xs` |
|
|
126
|
+
| Slot gap (icon ↔ label) | 0 | — † |
|
|
127
|
+
| Inter-tab gap | 4px | `sys.layout.inline.sm` |
|
|
128
|
+
| Radius | **8px** | `sys.radius.md` ⁂ |
|
|
129
|
+
| Label | 12 / Semibold | `sys.typo.label.sm` |
|
|
130
|
+
| Icon | 16px (fixed) | `sys.icon.md` |
|
|
131
|
+
|
|
132
|
+
‡ `min-height` binds raw `ref.space.*` — `sys.*` does not currently expose a 32px step.
|
|
133
|
+
|
|
134
|
+
† Slot gap is a literal `0` — visible label-to-glyph rhythm comes from the label-slot inset (`padding: 0 4px`), not a sibling `gap`.
|
|
135
|
+
|
|
136
|
+
⁂ Radius is the only divergence from Segmented (`sys.radius.full` → `sys.radius.md`).
|
|
137
|
+
|
|
138
|
+
## States
|
|
139
|
+
|
|
140
|
+
| State | Overlay opacity | Additional |
|
|
141
|
+
|------------|----------------------------|-----------------------------------------------------------------------------|
|
|
142
|
+
| `default` | — | Container + label at rest. |
|
|
143
|
+
| `hovered` | `sys.state.hover` (8%) | Pointer-driven via `:hover`. |
|
|
144
|
+
| `pressed` | `sys.state.pressed` (16%) | Pointer-driven via `:active`. |
|
|
145
|
+
| `selected` | — | Swap to inverse-surface pair; hairline stroke goes `transparent` (1px held). |
|
|
146
|
+
| `disabled` | overlay suppressed | Container at `sys.state.disabled` (40%) opacity, focus ring suppressed, `cursor: not-allowed`. |
|
|
147
|
+
|
|
148
|
+
## Focus indicator
|
|
149
|
+
|
|
150
|
+
Standard ring (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)). Trigger: `:focus-visible`.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Tabs",
|
|
4
|
+
"family": "tabs",
|
|
5
|
+
"subcomponent": "rounded",
|
|
6
|
+
"description": "Rounded-rectangle tab row — each tab is a self-contained chip carrying a required leading icon and a label, sharing the Segmented geometry verbatim but swapping the capsule radius for sys.radius.md (8) so the tabs read as soft rounded rectangles instead of pills. Reach for this on feed / list headers where the row of choices should sit as standalone chips with a more contained, card-adjacent silhouette.",
|
|
7
|
+
"delegatesTo": "chip/filter",
|
|
8
|
+
"element": "div",
|
|
9
|
+
"props": {
|
|
10
|
+
"variant": {
|
|
11
|
+
"type": "literal",
|
|
12
|
+
"value": "rounded"
|
|
13
|
+
},
|
|
14
|
+
"value": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "The active tab's value."
|
|
17
|
+
},
|
|
18
|
+
"onChange": {
|
|
19
|
+
"type": "function",
|
|
20
|
+
"description": "Fired with the next value when a tab is selected."
|
|
21
|
+
},
|
|
22
|
+
"aria-label": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Accessible name for the tablist."
|
|
25
|
+
},
|
|
26
|
+
"embedded": {
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
"default": false,
|
|
29
|
+
"description": "Composition mode flag. When `true` (or when Tabs is a direct child of `.chorus-carousel` / `.chorus-feed`), enters **embedded mode**: zeroes its own `background` + `padding` so chrome defers to the host. Pass explicitly inside `<Carousel>` / `<Feed>` for the contract to be visible in JSX; the DOM-ancestry safety net in styles.css also activates the mode when omitted. See `compositionModes` in `tabs.family.json`."
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"tabProps": {
|
|
33
|
+
"value": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"required": true
|
|
36
|
+
},
|
|
37
|
+
"leadingIcon": {
|
|
38
|
+
"type": "node",
|
|
39
|
+
"optional": true,
|
|
40
|
+
"description": "Optional 16px (sys.icon.md) context glyph rendered before the label. May carry its own brand / category color independent of the label."
|
|
41
|
+
},
|
|
42
|
+
"disabled": {
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"default": false
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"slots": {
|
|
48
|
+
"label": {
|
|
49
|
+
"required": false,
|
|
50
|
+
"description": "Tab's accessible name. Single line. Omit only when the tab is icon-only (pair with an `aria-label` for assistive tech). At least one of label / leadingIcon must be present.",
|
|
51
|
+
"accepts": [
|
|
52
|
+
"text"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"leadingIcon": {
|
|
56
|
+
"required": false,
|
|
57
|
+
"description": "16px (sys.icon.md) context glyph before the label. At least one of label / leadingIcon must be present.",
|
|
58
|
+
"accepts": [
|
|
59
|
+
"icon"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"sizing": {
|
|
64
|
+
"minHeight": "ref.space.400",
|
|
65
|
+
"containerPaddingBlock": "sys.layout.container.md",
|
|
66
|
+
"containerPaddingInline": "sys.layout.container.md",
|
|
67
|
+
"paddingBlock": "sys.layout.container.2xs",
|
|
68
|
+
"paddingInline": "sys.layout.container.sm",
|
|
69
|
+
"labelInset": "sys.layout.container.2xs",
|
|
70
|
+
"slotGap": "0",
|
|
71
|
+
"interTabGap": "sys.layout.inline.sm",
|
|
72
|
+
"radius": "sys.radius.md",
|
|
73
|
+
"labelTypo": "sys.typo.label.sm",
|
|
74
|
+
"iconSize": "sys.icon.md",
|
|
75
|
+
"fadeWidth": "ref.space.600"
|
|
76
|
+
},
|
|
77
|
+
"selectionStates": {
|
|
78
|
+
"unselected": {
|
|
79
|
+
"background": "transparent",
|
|
80
|
+
"label": "sys.color.onSurface",
|
|
81
|
+
"border": {
|
|
82
|
+
"width": "sys.borderWidth.hairline",
|
|
83
|
+
"color": "sys.color.outlineVariant"
|
|
84
|
+
},
|
|
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
|
+
},
|
|
87
|
+
"selected": {
|
|
88
|
+
"background": "sys.color.inverseSurface",
|
|
89
|
+
"label": "sys.color.inverseOnSurface",
|
|
90
|
+
"border": null
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"states": {
|
|
94
|
+
"default": {
|
|
95
|
+
"overlay": null
|
|
96
|
+
},
|
|
97
|
+
"hovered": {
|
|
98
|
+
"overlay": {
|
|
99
|
+
"color": "label",
|
|
100
|
+
"opacity": "sys.state.hover"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"pressed": {
|
|
104
|
+
"overlay": {
|
|
105
|
+
"color": "label",
|
|
106
|
+
"opacity": "sys.state.pressed"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"disabled": {
|
|
110
|
+
"overlay": null,
|
|
111
|
+
"containerOpacity": "sys.state.disabled",
|
|
112
|
+
"suppressFocusRing": true,
|
|
113
|
+
"cursor": "not-allowed"
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"focusIndicator": {
|
|
117
|
+
"description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the tab is in. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
|
|
118
|
+
"composition": "inward",
|
|
119
|
+
"compositionReason": "The Tabs row is a horizontal scroller; an outward ring would clip at the row's top/bottom edges.",
|
|
120
|
+
"overlay": {
|
|
121
|
+
"color": "label",
|
|
122
|
+
"opacity": "sys.state.focus"
|
|
123
|
+
},
|
|
124
|
+
"ring": {
|
|
125
|
+
"outerWidth": "sys.borderWidth.thin",
|
|
126
|
+
"outerColor": "sys.color.focus",
|
|
127
|
+
"outerLayerPosition": "depth 0..2px from the tab edge (the outer stroke)",
|
|
128
|
+
"insetWidth": "sys.borderWidth.hairline",
|
|
129
|
+
"insetColor": "sys.color.focusInset",
|
|
130
|
+
"insetLayerPosition": "depth 2..3px from the tab edge (the counter-ring just inside the outer stroke)",
|
|
131
|
+
"implementation": "inset box-shadow on the tab's `::after` overlay. Constrained strictly inside the tab's footprint and never exceeds it."
|
|
132
|
+
},
|
|
133
|
+
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
134
|
+
},
|
|
135
|
+
"forbidden": [
|
|
136
|
+
"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",
|
|
137
|
+
"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",
|
|
138
|
+
"active state painted with sys.color.brand — rounded tabs use sys.color.inverseSurface fill + sys.color.inverseOnSurface label in selected state"
|
|
139
|
+
]
|
|
140
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Segmented
|
|
2
|
+
|
|
3
|
+
The inline view-mode toggle — a row of mutually-exclusive selectors for in-place mode changes (List ↔ Grid, Day ↔ Week ↔ Month).
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the segments swap the *same* content's view or range in place — List ↔ Grid, Day ↔ Week ↔ Month. **Skip when** segments switch between *different* panels ([Underline](./underline.md)), multiple values can co-select ([Filter chip](../chip/filter.md)), or the rung is a single-select picker over a long list ([Radio list](../list/radio.md)).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `full-bleed` — **edge-to-edge** family. Sits as a direct child of the page shell (or any surface that pays the gutter) and stretches edge-to-edge. The row pays its own `16px inline / 8px block` padding via `layout.container.*`; do **not** wrap it in another `padding-inline` / `px-*` / `style={{ padding: … }}` div, or the page rail double-pays. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
Headline form — 2-segment List / Grid view toggle.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
tabs/segmented/default
|
|
15
|
+
---
|
|
16
|
+
import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<Tabs variant="segmented" value="list" aria-label="View mode">
|
|
19
|
+
<Tab value="list">List</Tab>
|
|
20
|
+
<Tab value="grid">Grid</Tab>
|
|
21
|
+
</Tabs>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Use cases
|
|
25
|
+
|
|
26
|
+
### With icon
|
|
27
|
+
|
|
28
|
+
Leading glyph in each segment — useful when the verb alone could read as anything.
|
|
29
|
+
|
|
30
|
+
```preview
|
|
31
|
+
tabs/segmented/leading-icon
|
|
32
|
+
---
|
|
33
|
+
import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
34
|
+
import { CheckedIcon, PlusIcon } from '@teamblind-chorus/ui/icons';
|
|
35
|
+
|
|
36
|
+
<Tabs variant="segmented" value="grid" aria-label="View mode">
|
|
37
|
+
<Tab value="list" leadingIcon={<CheckedIcon />}>List</Tab>
|
|
38
|
+
<Tab value="grid" leadingIcon={<PlusIcon />}>Grid</Tab>
|
|
39
|
+
</Tabs>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Overflow
|
|
43
|
+
|
|
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.
|
|
45
|
+
|
|
46
|
+
```preview
|
|
47
|
+
tabs/segmented/overflow
|
|
48
|
+
---
|
|
49
|
+
import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
50
|
+
|
|
51
|
+
<Tabs variant="segmented" value="day" aria-label="Range">
|
|
52
|
+
<Tab value="day">Day</Tab>
|
|
53
|
+
<Tab value="week">Week</Tab>
|
|
54
|
+
<Tab value="month">Month</Tab>
|
|
55
|
+
<Tab value="quarter">Quarter</Tab>
|
|
56
|
+
<Tab value="year">Year</Tab>
|
|
57
|
+
<Tab value="decade">Decade</Tab>
|
|
58
|
+
<Tab value="century">Century</Tab>
|
|
59
|
+
</Tabs>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Focus indicator
|
|
63
|
+
|
|
64
|
+
Static specimen — pins the focus ring to a selected segment. See top-level [Focus indicator](#focus-indicator).
|
|
65
|
+
|
|
66
|
+
```preview
|
|
67
|
+
tabs/segmented/focused
|
|
68
|
+
---
|
|
69
|
+
import { Tabs, Tab } from '@teamblind-chorus/ui';
|
|
70
|
+
|
|
71
|
+
<Tabs variant="segmented" value="list" aria-label="View">
|
|
72
|
+
<Tab value="list" state="focused">List</Tab>
|
|
73
|
+
<Tab value="grid">Grid</Tab>
|
|
74
|
+
</Tabs>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Slots
|
|
78
|
+
|
|
79
|
+
- **label** — segment's accessible name. Required, single line.
|
|
80
|
+
- **leadingIcon** (optional) — context glyph before the label.
|
|
81
|
+
|
|
82
|
+
## Anatomy
|
|
83
|
+
|
|
84
|
+
Each segment renders with `chorus-chip chorus-chip--filter` — see [Filter chip](../chip/filter.md). Selected swaps from unselected (`transparent` fill + `outlineVariant` border + `onSurface` label) to selected (`inverseSurface` fill + `inverseOnSurface` label, border `transparent` with 1px width held).
|
|
85
|
+
|
|
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).
|
|
87
|
+
|
|
88
|
+
## Sizes
|
|
89
|
+
|
|
90
|
+
Row container only — segment-internal sizing (min-height, padding, radius, typo, icon) is delegated to [Filter chip → Sizes](../chip/filter.md#sizes).
|
|
91
|
+
|
|
92
|
+
| Property | Value | Token |
|
|
93
|
+
|-----------------------------------------|----------------------|-------------------------------------|
|
|
94
|
+
| Container background | transparent | — |
|
|
95
|
+
| Container padding (block) | 16px | `sys.layout.container.md` |
|
|
96
|
+
| Container padding (inline) | 16px | `sys.layout.container.md` |
|
|
97
|
+
| Inter-segment gap | 4px | `sys.layout.inline.sm` |
|
|
98
|
+
| Segment min-height / radius / typo / etc.| (inherited) | see [Filter chip](../chip/filter.md#sizes) |
|
|
99
|
+
|
|
100
|
+
## States
|
|
101
|
+
|
|
102
|
+
Delegates to [Filter chip](../chip/filter.md) — segment chrome composes identically.
|
|
103
|
+
|
|
104
|
+
| State | Overlay opacity | Additional |
|
|
105
|
+
|------------|----------------------------|-----------------------------------------------------------------------------|
|
|
106
|
+
| `default` | — | Container + label at rest. |
|
|
107
|
+
| `hovered` | `sys.state.hover` (8%) | Pointer-driven via `:hover`. |
|
|
108
|
+
| `pressed` | `sys.state.pressed` (16%) | Pointer-driven via `:active`. |
|
|
109
|
+
| `selected` | — | Swap to inverse-surface pair; hairline border goes `transparent` (1px held). |
|
|
110
|
+
| `disabled` | overlay suppressed | Container at `sys.state.disabled` (40%) opacity, focus ring suppressed, `cursor: not-allowed`. |
|
|
111
|
+
|
|
112
|
+
## Focus indicator
|
|
113
|
+
|
|
114
|
+
**Composition: Inward** (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)) — row is a horizontal scroller, so an outward ring would clip at top/bottom. Paints as inset shadows inside the segment's bounding box on a `::after` so it never shifts a segment or row. Trigger: `:focus-visible`.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Tabs",
|
|
4
|
+
"family": "tabs",
|
|
5
|
+
"subcomponent": "segmented",
|
|
6
|
+
"description": "Inline mutually-exclusive selector. Visual chrome delegated to Filter chip — each segment renders with `chorus-chip chorus-chip--filter` classes and inherits the chip's container/label/border/state recipes verbatim. This sub adds only the row container (transparent, horizontal padding, fixed inter-segment gap) and the single-select semantics enforced via Tabs context.",
|
|
7
|
+
"delegatesTo": "chip/filter",
|
|
8
|
+
"element": "div",
|
|
9
|
+
"props": {
|
|
10
|
+
"variant": {
|
|
11
|
+
"type": "literal",
|
|
12
|
+
"value": "segmented"
|
|
13
|
+
},
|
|
14
|
+
"value": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "The active segment's value."
|
|
17
|
+
},
|
|
18
|
+
"onChange": {
|
|
19
|
+
"type": "function",
|
|
20
|
+
"description": "Fired with the next value when a segment is selected."
|
|
21
|
+
},
|
|
22
|
+
"aria-label": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Accessible name for the tablist."
|
|
25
|
+
},
|
|
26
|
+
"embedded": {
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
"default": false,
|
|
29
|
+
"description": "Composition mode flag. When `true` (or when Tabs is a direct child of `.chorus-carousel` / `.chorus-feed`), enters **embedded mode**: zeroes its own `background` + `padding` so chrome defers to the host. Pass explicitly inside `<Carousel>` / `<Feed>` for the contract to be visible in JSX; the DOM-ancestry safety net in styles.css also activates the mode when omitted. See `compositionModes` in `tabs.family.json`."
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"tabProps": {
|
|
33
|
+
"value": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"required": true
|
|
36
|
+
},
|
|
37
|
+
"leadingIcon": {
|
|
38
|
+
"type": "node",
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"disabled": {
|
|
42
|
+
"type": "boolean",
|
|
43
|
+
"default": false
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"slots": {
|
|
47
|
+
"label": {
|
|
48
|
+
"required": true,
|
|
49
|
+
"description": "Same contract as Filter chip's label slot.",
|
|
50
|
+
"accepts": [
|
|
51
|
+
"text"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"leadingIcon": {
|
|
55
|
+
"required": false,
|
|
56
|
+
"description": "Same contract as Filter chip's leadingIcon slot.",
|
|
57
|
+
"accepts": [
|
|
58
|
+
"icon"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"sizing": {
|
|
63
|
+
"containerPaddingBlock": "sys.layout.container.md",
|
|
64
|
+
"containerPaddingInline": "sys.layout.container.md",
|
|
65
|
+
"interSegmentGap": "sys.layout.inline.sm",
|
|
66
|
+
"fadeWidth": "ref.space.600"
|
|
67
|
+
},
|
|
68
|
+
"selectionStates": {
|
|
69
|
+
"$ref": "../chip/filter.spec.json#/selectionStates",
|
|
70
|
+
"note": "Each segment swaps between Filter chip's unselected (surfaceContainerHigh + outlineVariant border) and selected (inverseSurface + inverseOnSurface) recipes."
|
|
71
|
+
},
|
|
72
|
+
"states": {
|
|
73
|
+
"$ref": "../chip/filter.spec.json#/states",
|
|
74
|
+
"note": "Hover / pressed / focus overlays and the disabled treatment are inherited from Filter chip; same opacities, same focus ring composition."
|
|
75
|
+
},
|
|
76
|
+
"focusIndicator": {
|
|
77
|
+
"description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the segment is in. Re-anchored inside the segment from Filter chip's default outward composition because the Segmented row is a horizontal scroller.",
|
|
78
|
+
"composition": "inward",
|
|
79
|
+
"compositionReason": "The Tabs row is a horizontal scroller; an outward ring would clip at the row's top/bottom edges.",
|
|
80
|
+
"overlay": {
|
|
81
|
+
"color": "label",
|
|
82
|
+
"opacity": "sys.state.focus"
|
|
83
|
+
},
|
|
84
|
+
"ring": {
|
|
85
|
+
"outerWidth": "sys.borderWidth.thin",
|
|
86
|
+
"outerColor": "sys.color.focus",
|
|
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)",
|
|
91
|
+
"implementation": "inset box-shadow on the segment's `::after` overlay. Constrained strictly inside the segment's footprint and never exceeds it."
|
|
92
|
+
},
|
|
93
|
+
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
94
|
+
},
|
|
95
|
+
"forbidden": [
|
|
96
|
+
"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)",
|
|
98
|
+
"segmented row reflowing on selection — anatomy is no-layout"
|
|
99
|
+
]
|
|
100
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "tabs",
|
|
4
|
+
"name": "Tabs",
|
|
5
|
+
"description": "Tab row family. `underline` is the default content-section switcher (sliding bottom-edge indicator); `rounded` is a soft-rectangle chip row with required leading icons; `segmented` delegates chrome to Filter chip for in-place mode toggles.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"section switcher",
|
|
8
|
+
"content tabs",
|
|
9
|
+
"chip tab row",
|
|
10
|
+
"in-place mode toggle",
|
|
11
|
+
"list-grid toggle"
|
|
12
|
+
],
|
|
13
|
+
"visualReuse": "open",
|
|
14
|
+
"layoutInset": "full-bleed",
|
|
15
|
+
"wrapperGuidance": "Owns its inline padding internally. Place as a direct child of the page-shell <main> (or any host that pays the gutter once). Do NOT wrap in a padding-inline div, className=\"px-*\", or style={{ padding }} \u2014 the page rail is paid once at the shell, never on the full-bleed child. Inside a bounded surface (Dialog / BottomSheet / SideSheet), apply the negative-margin opt-out \u2014 see AGENTS.md \u00a7 Composition rules.",
|
|
16
|
+
"compositionModes": {
|
|
17
|
+
"standalone": {
|
|
18
|
+
"default": true,
|
|
19
|
+
"chrome": {
|
|
20
|
+
"background": "sys.color.surface",
|
|
21
|
+
"padding": "per-variant container.* inline"
|
|
22
|
+
},
|
|
23
|
+
"context": "Direct child of the page shell or any host that pays the page-rail gutter once."
|
|
24
|
+
},
|
|
25
|
+
"embedded": {
|
|
26
|
+
"trigger": "prop `embedded={true}` on <Tabs /> OR direct child of `.chorus-carousel` / `.chorus-feed` (DOM-ancestry safety net).",
|
|
27
|
+
"chrome": {
|
|
28
|
+
"background": "transparent",
|
|
29
|
+
"padding": "0"
|
|
30
|
+
},
|
|
31
|
+
"context": "Composed inside another rail-responsible host (e.g. a labelled Section that hosts a stage-tab row). Tabs takes over from the host's content-box edge; the underline indicator and segmented chips still paint correctly because `padding-inline: 0` doesn't change tab geometry, only the rail surround."
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"usage": {
|
|
35
|
+
"note": "`<Tabs>` selects chrome by the `variant` prop; every tab MUST be a `<Tab value=…>` child (compound) — there is no per-variant tab export and bare text / raw <button> children do not work.",
|
|
36
|
+
"subs": {
|
|
37
|
+
"underline": { "variant": "underline", "example": "<Tabs variant=\"underline\" value={v} onChange={setV}><Tab value=\"a\">A</Tab><Tab value=\"b\">B</Tab></Tabs>" }
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"spec": "tabs.md",
|
|
41
|
+
"subcomponents": [
|
|
42
|
+
{
|
|
43
|
+
"slug": "underline",
|
|
44
|
+
"spec": "underline.spec.json",
|
|
45
|
+
"md": "underline.md",
|
|
46
|
+
"default": true
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"slug": "rounded",
|
|
50
|
+
"spec": "rounded.spec.json",
|
|
51
|
+
"md": "rounded.md"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"slug": "segmented",
|
|
55
|
+
"spec": "segmented.spec.json",
|
|
56
|
+
"md": "segmented.md"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
A horizontal row of mutually-exclusive selections — exactly one tab is active, and selecting one swaps the surrounding panel. Sub-flavors: **Underline** is the canonical content-section switcher; **Segmented** is the inline view-mode toggle whose chrome delegates to [Filter chip](../chip/filter.md).
|
|
4
|
+
|
|
5
|
+
**Layout inset.** `full-bleed` — sits as a direct child of the page shell (or any surface that pays the gutter) and stretches edge-to-edge inside it. The tabs rail pays its own internal padding via `layout.container.*`; do **not** wrap it in another `padding-inline` / `px-*` / `style={{ padding: … }}` div, or the page rail double-pays. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
6
|
+
|
|
7
|
+
## Cross-sub contract
|
|
8
|
+
|
|
9
|
+
- **Selection model.** Single-select. `Tabs` owns the controlled `value`; selecting fires `onChange(value)`. For multi-select, use [Filter chips](../chip/filter.md).
|
|
10
|
+
- **Slots.** Each Tab has a required **label** and an optional **leadingIcon** sized to the active sub's icon rung. No trailing slot.
|
|
11
|
+
- **Selected as a state, not a variant.** Same element flips in/out as the parent's `value` changes.
|
|
12
|
+
- **Accessibility.** Container exposes `role="tablist"` with caller-supplied `aria-label`; each Tab has `role="tab"` + `aria-selected`. Keyboard nav (←/→, Home/End) handled by container.
|
|
13
|
+
|
|
14
|
+
## Sub-components
|
|
15
|
+
|
|
16
|
+
- **[Underline](./underline.md)** — Tab row with a single 2px (`sys.borderWidth.thin`) `onSurface` indicator that slides along the active tab's bottom edge. Default for switching content panels.
|
|
17
|
+
- **[Rounded](./rounded.md)** — Rounded-rectangle tab row, each tab a self-contained chip with a required leading icon and label. Shares Segmented's geometry, but corner radius steps from `full` to `sys.radius.md` (8).
|
|
18
|
+
- **[Segmented](./segmented.md)** — Row of inline mode toggles whose chrome delegates to [Filter chip](../chip/filter.md). For in-place view-mode changes (List ↔ Grid, Day ↔ Week ↔ Month).
|