@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,237 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "List",
|
|
4
|
+
"family": "list",
|
|
5
|
+
"subcomponent": "entry",
|
|
6
|
+
"description": "Directory-entry List variant — edge-to-edge rows that pair an *optional* leading [Thumbnail](../thumbnail/thumbnail.md) (selectable rung 32 / 40 / 48 / 56 via `size=\"small|medium|large|xlarge\"`) with an identity group (label + optional inline `count` Badge + optional stacked `secondary` line — e.g. follower count — that tile flush with line-height-only spacing) and an optional single-line `description` separated from the identity group by `ref.space.25` (2). Optional trailing affordance per row. Same click semantics as the text variant — no selection model. Replaces the legacy `row/entity` atom — every entity-row case (follow suggestion, member directory, subscription / channel / topic / playlist directory) now ships as a full-bleed list at this variant. The optional leading slot also lets the same sub host **label-only** nav rows (settings menu, category index) — drop `thumbnail` from the row descriptor to collapse the leading column and tile the label flush at the 16 inline rail.",
|
|
7
|
+
"element": "ul",
|
|
8
|
+
"props": {
|
|
9
|
+
"embedded": {
|
|
10
|
+
"type": "boolean",
|
|
11
|
+
"default": false,
|
|
12
|
+
"description": "Composition mode flag. When `true` (or when the List 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 `list.family.json`."
|
|
13
|
+
},
|
|
14
|
+
"size": {
|
|
15
|
+
"type": "literal",
|
|
16
|
+
"values": [
|
|
17
|
+
"small",
|
|
18
|
+
"medium",
|
|
19
|
+
"large",
|
|
20
|
+
"xlarge"
|
|
21
|
+
],
|
|
22
|
+
"default": "medium",
|
|
23
|
+
"description": "Thumbnail rung shared by every row in the list — `small` = 32, `medium` = 40, `large` = 48, `xlarge` = 56. The row's inline padding (16), block padding (8), and the family-wide row min-height (48) stay constant across sizes; only the leading Thumbnail footprint changes. Pick `small` for compact directories where the avatar reads as a mark, `medium` for the canonical channel / source directory, `large` for member-directory rows where the avatar carries identity weight, `xlarge` for the canonical follow-suggestion rung used by [SuggestionList](../suggestion-list/suggestion-list.md)."
|
|
24
|
+
},
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "node",
|
|
27
|
+
"required": true,
|
|
28
|
+
"description": "Array of row descriptors. Each item passes `thumbnail` props forwarded verbatim to the Thumbnail component."
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"slots": {
|
|
32
|
+
"container": {
|
|
33
|
+
"required": true,
|
|
34
|
+
"description": "Outer scroll surface. Vertical stack with a transparent fill (inherits parent container tone); rows separated by a 1px outlineVariant divider, not a gap."
|
|
35
|
+
},
|
|
36
|
+
"row": {
|
|
37
|
+
"required": true,
|
|
38
|
+
"description": "Single list item. Whole row is the interactive target."
|
|
39
|
+
},
|
|
40
|
+
"leading": {
|
|
41
|
+
"required": false,
|
|
42
|
+
"omittedBehavior": "collapsed",
|
|
43
|
+
"fallbackOnMissingSrc": "sys.color.surfaceContainerHigh",
|
|
44
|
+
"description": "Optional Thumbnail at the leading edge at the list's `size` rung (32 / 40 / 48 / 56), vertically centred against the text column. Forwarded verbatim from item.thumbnail. When `thumbnail` is omitted on a row descriptor the leading column collapses, the `leading → text` (12) gap drops to 0, and the label sits flush at the 16 inline rail — reach for it on label-only nav rows (settings menu, category index) and mix-and-match identity rows. `fallbackOnMissingSrc` is the dim-tone fill the Thumbnail paints when `src` is present but empty / fails to load — at scaffold time, agents fill `src` with `/placeholder.png` rather than relying on the fallback. To intentionally render a label-only row, omit `thumbnail` entirely rather than passing an empty `src`.",
|
|
45
|
+
"accepts": [
|
|
46
|
+
"thumbnail"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"label": {
|
|
50
|
+
"required": true,
|
|
51
|
+
"description": "Primary row text. `sys.typo.label.md` (14 / Semibold) / `sys.color.onSurface`. Single line; truncates with ellipsis. Pairs flush with the inline `count` slot — no gap between them, only `sys.layout.inline.sm` (4) horizontal separation on the primary row.",
|
|
52
|
+
"accepts": [
|
|
53
|
+
"text"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"count": {
|
|
57
|
+
"required": false,
|
|
58
|
+
"description": "Optional inline node painted to the right of the label on the same line (e.g. follower count, member count). Canonical fill is a numeric [Badge](../badge/badge.md). Separated from the label by `sys.layout.inline.sm` (4). Label shrinks first so a long name truncates against the count.",
|
|
59
|
+
"accepts": [
|
|
60
|
+
"badge",
|
|
61
|
+
"text"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"secondary": {
|
|
65
|
+
"required": false,
|
|
66
|
+
"description": "Optional stacked meta line painted below the label inside the identity group (e.g. `'12.4K Followers'`, `'Brooklyn, NY · Home baker'`). `sys.typo.label.sm` (12 / Semibold) / `sys.color.onSurface`. Tiles flush with the label — line-height-only spacing, no margin — so the two lines read as one tight identity block.",
|
|
67
|
+
"accepts": [
|
|
68
|
+
"text"
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"description": {
|
|
72
|
+
"required": false,
|
|
73
|
+
"description": "Optional secondary line under the identity group. `sys.typo.label.sm` (12 / Semibold) / `sys.color.onSurfaceVariant`. Single line; truncates with ellipsis. Separated from the identity group by `ref.space.25` (2) so the description reads as a tight supporting layer below the name rather than as a co-equal line.",
|
|
74
|
+
"accepts": [
|
|
75
|
+
"text"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"trailingIcon": {
|
|
79
|
+
"required": false,
|
|
80
|
+
"description": "Node at the trailing edge. Consumer-supplied. Accepts a decorative icon, a toggle button (favorite / mute / pin), a numeric badge, or a small text button (Follow / Invite). Taps on the trailing slot stop propagating before they reach the row's `onClick` — the slot is a separate hit target so the consumer can wire its own action without committing the row.",
|
|
81
|
+
"accepts": [
|
|
82
|
+
"icon",
|
|
83
|
+
"badge",
|
|
84
|
+
"button"
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"rowProps": {
|
|
89
|
+
"value": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"required": true
|
|
92
|
+
},
|
|
93
|
+
"label": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"required": true
|
|
96
|
+
},
|
|
97
|
+
"count": {
|
|
98
|
+
"type": "node",
|
|
99
|
+
"optional": true,
|
|
100
|
+
"description": "Inline count node painted flush with the label. Canonical fill: `<Badge count={n} />`."
|
|
101
|
+
},
|
|
102
|
+
"secondary": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"optional": true,
|
|
105
|
+
"description": "Stacked meta line painted below the label inside the identity group (e.g. follower count text, location). Caption-tone but `onSurface` color — reads as part of the identity block, not as a recessive description."
|
|
106
|
+
},
|
|
107
|
+
"description": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"optional": true,
|
|
110
|
+
"description": "Single-line caption-tone description under the identity group. Truncates with ellipsis; never wraps to a second line."
|
|
111
|
+
},
|
|
112
|
+
"thumbnail": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"optional": true,
|
|
115
|
+
"description": "Optional. Forwarded to Thumbnail verbatim (`src`, `alt`, optional `shape`, optional `updateDot`, optional `logoBadge`). Omit to render a label-only row — the leading column collapses, the `leading → text` (12) gap drops to 0, and the label sits flush at the 16 inline rail."
|
|
116
|
+
},
|
|
117
|
+
"trailingIcon": {
|
|
118
|
+
"type": "node",
|
|
119
|
+
"optional": true,
|
|
120
|
+
"description": "Trailing-edge node. Wrapped in a slot that stops click propagation so the slot is its own hit target separate from the row's `onClick`."
|
|
121
|
+
},
|
|
122
|
+
"disabled": {
|
|
123
|
+
"type": "boolean",
|
|
124
|
+
"default": false
|
|
125
|
+
},
|
|
126
|
+
"divider": {
|
|
127
|
+
"type": "boolean",
|
|
128
|
+
"default": true,
|
|
129
|
+
"description": "Per-row bottom-divider opt-out. Pass `divider: false` to suppress the hairline `outlineVariant` rule beneath the row; the row's footprint and inline padding stay unchanged. Reach for it when a visual group ends mid-stack and the divider would visually fence off the next group from its label."
|
|
130
|
+
},
|
|
131
|
+
"strong": {
|
|
132
|
+
"type": "boolean",
|
|
133
|
+
"default": false,
|
|
134
|
+
"description": "Per-row label-emphasis opt-in (family-wide contract — see [list.md § Cross-sub contract](./list.md)). Already Semibold at rest in this variant; `strong` is the no-op marker reserved for parity with other subs and to support future weight shifts without breaking the prop surface."
|
|
135
|
+
},
|
|
136
|
+
"onClick": {
|
|
137
|
+
"type": "function",
|
|
138
|
+
"optional": true
|
|
139
|
+
},
|
|
140
|
+
"forcedState": {
|
|
141
|
+
"type": "literal",
|
|
142
|
+
"values": [
|
|
143
|
+
"hovered",
|
|
144
|
+
"pressed",
|
|
145
|
+
"focused"
|
|
146
|
+
],
|
|
147
|
+
"optional": true,
|
|
148
|
+
"description": "Docs-only — pins the row to a single visual state via `data-force-state`. Not for production use."
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"sizing": {
|
|
152
|
+
"rowPaddingBlock": "ref.space.100 (8) at small / medium / large; ref.space.150 (12) at xlarge — the 56 rung's identity stack reads denser so block padding bumps a step.",
|
|
153
|
+
"rowPaddingInline": "ref.space.200",
|
|
154
|
+
"rowMinHeight": "ref.space.600",
|
|
155
|
+
"leadingTextGap": "sys.layout.inline.lg (12) when the leading Thumbnail is present (the image-leading rung of the family's role-based spacing); 0 when `thumbnail` is omitted (the column collapses and the label sits flush at the 16 inline rail).",
|
|
156
|
+
"trailingActionGap": "sys.layout.inline.md",
|
|
157
|
+
"trailingActionGapNote": "Fixed 8px between the text group and a trailing action — the family-wide trailing gap.",
|
|
158
|
+
"dividerWidth": "sys.borderWidth.hairline",
|
|
159
|
+
"dividerColor": "sys.color.outlineVariant",
|
|
160
|
+
"dividerPerRowOptOut": "Pass `divider: false` on a row to suppress its bottom divider.",
|
|
161
|
+
"labelTypo": "sys.typo.label.md",
|
|
162
|
+
"labelColor": "sys.color.onSurface",
|
|
163
|
+
"labelToCountGap": "sys.layout.inline.sm",
|
|
164
|
+
"secondaryTypo": "sys.typo.label.sm",
|
|
165
|
+
"secondaryColor": "sys.color.onSurface",
|
|
166
|
+
"secondaryToLabelGap": "0",
|
|
167
|
+
"identityToDescriptionGap": "ref.space.25",
|
|
168
|
+
"descriptionTypo": "sys.typo.label.sm",
|
|
169
|
+
"descriptionColor": "sys.color.onSurfaceVariant",
|
|
170
|
+
"descriptionMaxLines": 1,
|
|
171
|
+
"leadingThumbnailSize": {
|
|
172
|
+
"small": 32,
|
|
173
|
+
"medium": 40,
|
|
174
|
+
"large": 48,
|
|
175
|
+
"xlarge": 56
|
|
176
|
+
},
|
|
177
|
+
"dividerInset": "left 16 + right 16 from the row's edges (overrides the family-wide `right: 0` rule). At `size=\"xlarge\"`, the divider's left edge anchors to the text column instead (16 + 56 + 12 = 84 from the row's leading edge) so the rule reads as separating the text columns of two suggestion rows. Label-only rows (no thumbnail) always fall back to the default 16 inset regardless of `size` — there is no avatar column to anchor against.",
|
|
178
|
+
"trailingIconSize": "16 × 16",
|
|
179
|
+
"trailingIconColor": "sys.color.onSurfaceVariant"
|
|
180
|
+
},
|
|
181
|
+
"states": {
|
|
182
|
+
"default": {
|
|
183
|
+
"overlay": null
|
|
184
|
+
},
|
|
185
|
+
"hovered": {
|
|
186
|
+
"overlay": {
|
|
187
|
+
"color": "label",
|
|
188
|
+
"opacity": "sys.state.hover"
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"pressed": {
|
|
192
|
+
"overlay": {
|
|
193
|
+
"color": "label",
|
|
194
|
+
"opacity": "sys.state.pressed"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"disabled": {
|
|
198
|
+
"containerOpacity": "sys.state.disabled",
|
|
199
|
+
"containerOpacityScope": "Dims the row content only — the inter-row divider and the focus overlay keep full opacity, so a disabled row never fades the hairline rule between it and the next row.",
|
|
200
|
+
"pointerEvents": "none"
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"focusIndicator": {
|
|
204
|
+
"description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the row is in.",
|
|
205
|
+
"composition": "inward",
|
|
206
|
+
"compositionReason": "Rows tile the column flush with only a hairline `outlineVariant` divider between them; an outward ring would overlap the divider and the neighbouring row.",
|
|
207
|
+
"overlay": {
|
|
208
|
+
"color": "label",
|
|
209
|
+
"opacity": "sys.state.focus"
|
|
210
|
+
},
|
|
211
|
+
"ring": {
|
|
212
|
+
"outerWidth": "sys.borderWidth.thin",
|
|
213
|
+
"outerColor": "sys.color.focus",
|
|
214
|
+
"outerLayerPosition": "depth 0..2px from the row edge (the outer stroke)",
|
|
215
|
+
"insetWidth": "sys.borderWidth.hairline",
|
|
216
|
+
"insetColor": "sys.color.focusInset",
|
|
217
|
+
"insetLayerPosition": "depth 2..3px from the row edge (the counter-ring just inside the outer stroke)",
|
|
218
|
+
"implementation": "inset box-shadow on the row's `::before` overlay (the `::after` carries the inter-row divider). Constrained strictly inside the row's footprint and never exceeds it."
|
|
219
|
+
},
|
|
220
|
+
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
221
|
+
},
|
|
222
|
+
"behavior": {
|
|
223
|
+
"keyboardNavigation": "Arrow up/down moves focus between rows. Home and End jump to first / last.",
|
|
224
|
+
"rowClickTarget": "Whole row is clickable when an `onClick` is bound to the item. The thumbnail is never a separate hit target.",
|
|
225
|
+
"identityGroupFlush": "Label and inline count tile flush on the primary line with `sys.layout.inline.sm` (4) horizontal separation and no vertical gap — they read as one tight identity block.",
|
|
226
|
+
"descriptionSingleLine": "Description always renders on a single line and truncates with ellipsis. The row never grows to fit longer description copy — overflow truncates against the trailing slot if present.",
|
|
227
|
+
"trailingSlotIndependent": "Clicks inside `trailingIcon` stop propagating before they reach the row's `onClick`. Wire a favorite / follow / overflow action there without it committing the row's primary action."
|
|
228
|
+
},
|
|
229
|
+
"forbidden": [
|
|
230
|
+
"thumbnail rendered as a raw <img> outside the chorus-thumbnail slot wrapper",
|
|
231
|
+
"thumbnail size different from the list's `size` rung (32 / 40 / 48 / 56) — the thumbnail is optional, but when present it MUST match the rung",
|
|
232
|
+
"passing `thumbnail: { src: '' }` to fake a label-only row — omit the `thumbnail` key entirely instead so the leading column actually collapses",
|
|
233
|
+
"description rendered as a multi-line block — the spec is single-line, truncated",
|
|
234
|
+
"extra gap between label and count — they tile flush on the primary line",
|
|
235
|
+
"wrapping the list in another `padding-inline` / `px-*` div — the list owns its 16px inline rail at the row level"
|
|
236
|
+
]
|
|
237
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "list",
|
|
4
|
+
"name": "List",
|
|
5
|
+
"description": "Vertical list of rows. Four sub-components share one anatomy and diverge on the leading slot, the selection contract, and the expand contract: `standard` (default — display / navigation rows; a row opts into a leading 40px [Thumbnail](../thumbnail/thumbnail.md) image (the image type) by passing `thumbnail`, or a leading 24px icon (the icon type) by passing `icon`, with optional `supportingText` second line + optional inline `count` badge + optional trailing affordance per row), `radio` (single-select picker with leading 16px radio indicator), `entry` (directory-entry rows with selectable 32 / 40 / 48 / 56 leading Thumbnail via `size=\"small|medium|large|xlarge\"`, identity group of label + optional inline `count` Badge + optional stacked `secondary` line, and an optional single-line `description` below — the canonical home for every entity-row case: follow suggestion, member directory, subscription / channel / topic / playlist directory), `accordion` (expandable rows with auto-rendered trailing chevron that rotates on expand, hosting a content body and optional nested `<List embedded>` group below the trigger). Drill-in rows are not a separate sub-component: any `standard` / `radio` row opts into a trailing drill-in chevron with `nav: true` (on a `radio` row it marks a major category that opens a second screen).",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"settings rows",
|
|
8
|
+
"menu rows",
|
|
9
|
+
"picker rows",
|
|
10
|
+
"single-select option group",
|
|
11
|
+
"drill-in navigation",
|
|
12
|
+
"avatar-anchored rows",
|
|
13
|
+
"directory rows",
|
|
14
|
+
"subscription / channel / topic directory entries",
|
|
15
|
+
"follow-suggestion stacks (full-bleed)",
|
|
16
|
+
"expandable rows",
|
|
17
|
+
"FAQ / policy sections",
|
|
18
|
+
"expandable filter groups",
|
|
19
|
+
"hierarchical menus (parent → child rows)"
|
|
20
|
+
],
|
|
21
|
+
"visualReuse": "open",
|
|
22
|
+
"layoutInset": "full-bleed",
|
|
23
|
+
"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.",
|
|
24
|
+
"compositionModes": {
|
|
25
|
+
"standalone": {
|
|
26
|
+
"default": true,
|
|
27
|
+
"chrome": {
|
|
28
|
+
"background": "sys.color.surface",
|
|
29
|
+
"padding": "row-level inline + block via list-row anatomy"
|
|
30
|
+
},
|
|
31
|
+
"context": "Direct child of the page shell or any host that pays the page-rail gutter once."
|
|
32
|
+
},
|
|
33
|
+
"embedded": {
|
|
34
|
+
"trigger": "prop `embedded={true}` on <List /> OR direct child of `.chorus-carousel` / `.chorus-feed` (DOM-ancestry safety net).",
|
|
35
|
+
"chrome": {
|
|
36
|
+
"background": "transparent",
|
|
37
|
+
"padding": "0"
|
|
38
|
+
},
|
|
39
|
+
"context": "Composed inside another rail-responsible host. Rows take over from the host's content-box edge; leading thumbnails / radios align with the host header's left rail."
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"usage": {
|
|
43
|
+
"note": "Rows are an `items` descriptor *array* on `<List>` — there is NO `<ListItem>` export. A styled `<button>` row or a hand-rolled flex row is a reinvention of `list / entry`. `accordion` is the one sub that takes `<Accordion>` children.",
|
|
44
|
+
"subs": {
|
|
45
|
+
"standard": { "variant": "standard", "example": "<List variant=\"standard\" items={[{ label, leadingIcon, trailing, onClick }]} />" },
|
|
46
|
+
"entry": { "variant": "entry", "example": "<List variant=\"entry\" items={[{ thumbnail, label, secondary, description, trailing, onClick }]} />" },
|
|
47
|
+
"radio": { "variant": "radio", "example": "<List variant=\"radio\" value={v} onChange={setV} items={[{ label, value }]} />" },
|
|
48
|
+
"accordion": { "import": "Accordion", "example": "<Accordion><Accordion.Item title=\"…\">…</Accordion.Item></Accordion>" }
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"spec": "list.md",
|
|
52
|
+
"subcomponents": [
|
|
53
|
+
{
|
|
54
|
+
"slug": "standard",
|
|
55
|
+
"spec": "standard.spec.json",
|
|
56
|
+
"md": "standard.md",
|
|
57
|
+
"default": true
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"slug": "radio",
|
|
61
|
+
"spec": "radio.spec.json",
|
|
62
|
+
"md": "radio.md"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"slug": "entry",
|
|
66
|
+
"spec": "entry.spec.json",
|
|
67
|
+
"md": "entry.md"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"slug": "accordion",
|
|
71
|
+
"spec": "accordion.spec.json",
|
|
72
|
+
"md": "accordion.md"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# List
|
|
2
|
+
|
|
3
|
+
A vertically-stacked sequence of rows for menus, settings panels, picker sheets, inline option groups, directory entries, and expandable hierarchies. Four sub-components share one anatomy and diverge on the leading slot, the selection contract, and the expand contract.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the rows are same-kind chrome — settings entries, menu options, single-select picker rows, drill-in navigation, directory rows, expandable section headers. **Skip when** the rows are authored content with author + body + footer (use [Feed](../feed/feed.md)) or a horizontal collection of curated cards (use [Carousel](../carousel/carousel.md)). Pick the sub by the trailing affordance and the leading slot: display / navigation rows → [Standard](./standard.md) (no `thumbnail` for text-only; pass `thumbnail` for the 40px avatar-leading image type, label + optional `supportingText`); single-select radio → [Radio](./radio.md); 32 / 40 / 48 avatar leading with identity-group (label + inline `count`) + optional single-line description → [Entry](./entry.md); drill-in chevron → any [Standard](./standard.md) / [Radio](./radio.md) row with `nav: true`; expand chevron that rotates on click → [Accordion](./accordion.md).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `full-bleed` — sits as a direct child of the page shell. Each row pays its own `8px block / 16px inline` padding via `layout.container.xs` / `layout.container.md`; do **not** wrap the List in another `padding-inline` / `px-*` / `style={{ padding: … }}` div, or the row leading edge (radio glyph, thumbnail, label start) lands at a different inset than surrounding chrome. 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
|
+
## Cross-sub contract
|
|
10
|
+
|
|
11
|
+
- **Container.** Vertical stack, transparent fill (inherits the parent surface). Rows separated by a 1px `outlineVariant` divider inset 16px (`layout.container.md`) from **both** the leading and trailing edges so the rule reads as separating *content*, not the container. The Entry sub overrides the leading inset at `size="xlarge"` only — see [entry.md](./entry.md). No outer radius — corner shape belongs to the wrapping container.
|
|
12
|
+
- **Row geometry.** 8px block / 16px inline padding (`layout.container.xs` / `layout.container.md`); min-height 48px. Row spacing is **role-based**, not a single flex gap: the **text group → trailing action** gap is a fixed `layout.inline.md` (8px) in every sub, while the **leading → text group** gap depends on the leading *type* — `layout.inline.md` (8px) for an icon leading (Radio's indicator), `layout.inline.lg` (12px) for an image leading (a Standard row's `thumbnail` image type / Entry Thumbnail). A label-only Entry row (no `thumbnail`) drops the leading gap to 0. Row grows when `supportingText` is present.
|
|
13
|
+
- **Label column.** Label: 16px / Regular / `onSurface` (sub-list rows compressed inside an accordion render at 14px / Regular — see [accordion.md](./accordion.md) § Nested list; Entry rows promote the label to 14px / Semibold so the inline `count` reads as part of the identity group). SupportingText: 14px / Regular / `onSurfaceVariant`, sits directly under the label with no extra gap — the two lines stack on the label-column's intrinsic line-box rhythm. The Entry sub replaces the second line with a single-line `description` (12px / Regular / `onSurfaceVariant`, separated from the identity group by `ref.space.25` (2) — see [entry.md](./entry.md)). All secondary lines truncate with ellipsis.
|
|
14
|
+
- **Strong-label opt-in.** Pass `strong={true}` on a row (`<Accordion.Item strong>` on the accordion sub) to promote the label's weight from Regular (`body.*-weight`, 400) to Semibold (`label.*-weight`, 600) at the same size and line-height — `body.md → label.lg` at the 16 rung, `body.sm → label.md` at the 14 rung. The row's geometry (height, dividers, slot positions) is unchanged; only the label glyphs gain stroke weight. Reach for it when one row needs to read as the primary entry within a denser scan — the active company in a directory, the canonical answer in an FAQ, the user's own row in a member list. Use sparingly — a stack where every row is strong reads as the default again, defeating the marker.
|
|
15
|
+
- **States.** `selected` exists only on Radio. The whole row is the interactive surface.
|
|
16
|
+
- **Focus indicator.** Composition: Inward (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)) — inset shadows entirely inside the row's box. Trigger: `:focus-visible`.
|
|
17
|
+
- **Accessibility.** Every non-Radio sub (Standard / Entry / Accordion) exposes `role="list"`; Radio exposes `role="radiogroup"` with each row `role="radio"` + `aria-checked`. A `nav: true` row's trailing chevron is decorative (`aria-hidden`) — the whole row stays the single interactive target. Keyboard navigation (Arrow ↑/↓, Home/End) is handled by the container; Radio also commits on Space / Enter.
|
|
18
|
+
|
|
19
|
+
## Sub-components
|
|
20
|
+
|
|
21
|
+
- **[Standard](./standard.md)** — Display / navigation rows; the default sub for menu lists that route or fire without a selection model. Text-only by default (no leading slot, whole row is the click target); a row opts into a 40px leading [Thumbnail](../thumbnail/thumbnail.md) — the image type — by passing `thumbnail` (the former Image sub, now a per-row case), or a 24px (`sys.icon.lg`) leading glyph — the icon type — by passing `icon` (8px from the text group, mutually exclusive with `thumbnail`). A row opts into an inline `count` Badge to the right of the label (the unread / status-count case); for the avatar-anchored identity group with an inline count, reach for [Entry](./entry.md) instead. Set `nav: true` on a row for the drill-in chevron (the former Nav sub, now a per-row case).
|
|
22
|
+
- **[Radio](./radio.md)** — Single-select picker with a leading 16px radio indicator; clicking a row commits its value via `onChange(value)`.
|
|
23
|
+
- **[Entry](./entry.md)** — Directory-entry rows with a selectable 32 / 40 / 48 / 56 leading [Thumbnail](../thumbnail/thumbnail.md) (`size="small|medium|large|xlarge"`). Identity group of label + optional inline `count` Badge + optional stacked `secondary` line, plus an optional single-line `description` separated by `ref.space.25` (2). Same click semantics as Standard. The single home for every entity-row case (follow suggestion, member directory, subscription / channel / topic / playlist directory).
|
|
24
|
+
- **[Accordion](./accordion.md)** — Expandable rows. Trailing edge auto-renders a `ChevronDownIcon` that rotates `0° → 180°` on expand; the open trigger hosts a content body (prose or another `<List embedded>`) indented one extra `layout.container.md` so the body reads as nested inside the trigger's label column. When the body holds a `<List embedded>`, a hairline `outlineVariant` divider paints at the top of the body so parent ↔ child hierarchy reads.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Radio
|
|
2
|
+
|
|
3
|
+
Single-select picker List sub-component. Each row carries a leading 24px (`sys.icon.lg`) radio indicator; clicking commits that row's value via `onChange(value)`. Exactly one row is selected at a time. Row geometry, typography, divider, state overlays, and inward focus ring all delegate to the [family-wide rules](./list.md); this sub documents the Radio-specific leading indicator and selection contract.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the user picks exactly one value from a short, fully-visible set — sort order, range filter, equity tier. **Skip when** multiple values may be selected (use [`Chip variant="filter"`](../chip/filter.md) or [`Button variant="check"`](../button/check.md)), the set is long enough to demand a sheet-driven picker ([Select](../form-field/select.md)), or the row *only* navigates without selecting (use a [Standard](./standard.md) row with `nav: true`). When a value both selects *and* opens a deeper screen — a major category — keep Radio and add `nav: true` (see [Major category with a second screen](#major-category-with-a-second-screen)).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `full-bleed` — sits as a direct child of the page shell. Each row pays its own `16px inline / 8px block` padding via `layout.container.*`; do **not** wrap the list in another `padding-inline` / `px-*` / `style={{ padding: … }}` div, or the radio indicator lands at a different inset than the section headings around it. 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
|
+
A radio-selectable list — single-select picker over the shared anatomy.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
list/radio
|
|
15
|
+
---
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import { List } from '@teamblind-chorus/ui';
|
|
18
|
+
|
|
19
|
+
const [value, setValue] = useState('week');
|
|
20
|
+
|
|
21
|
+
<List
|
|
22
|
+
variant="radio"
|
|
23
|
+
value={value}
|
|
24
|
+
onChange={setValue}
|
|
25
|
+
items={[
|
|
26
|
+
{ value: 'day', label: 'Day' },
|
|
27
|
+
{ value: 'week', label: 'Week' },
|
|
28
|
+
{ value: 'month', label: 'Month' },
|
|
29
|
+
{ value: 'quarter', label: 'Quarter' },
|
|
30
|
+
{ value: 'year', label: 'Year' },
|
|
31
|
+
]}
|
|
32
|
+
/>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Use cases
|
|
36
|
+
|
|
37
|
+
### With supporting text
|
|
38
|
+
|
|
39
|
+
Pairs each label with a secondary line — for when the label alone doesn't carry enough context (*sort orders explained in copy*, *equity types with one-line definitions*).
|
|
40
|
+
|
|
41
|
+
```preview
|
|
42
|
+
list/radio-with-supporting
|
|
43
|
+
---
|
|
44
|
+
import { useState } from 'react';
|
|
45
|
+
import { List } from '@teamblind-chorus/ui';
|
|
46
|
+
|
|
47
|
+
const [value, setValue] = useState('trending');
|
|
48
|
+
|
|
49
|
+
<List
|
|
50
|
+
variant="radio"
|
|
51
|
+
value={value}
|
|
52
|
+
onChange={setValue}
|
|
53
|
+
aria-label="Sort posts by"
|
|
54
|
+
items={[
|
|
55
|
+
{ value: 'newest', label: 'Newest first', supportingText: 'Most recent posts at the top' },
|
|
56
|
+
{ value: 'trending', label: 'Trending', supportingText: 'Active threads from the last 24h' },
|
|
57
|
+
{ value: 'most-liked', label: 'Most liked', supportingText: 'Highest like count this week' },
|
|
58
|
+
{ value: 'oldest', label: 'Oldest first', supportingText: 'Earliest posts at the top' },
|
|
59
|
+
]}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Disabled item
|
|
64
|
+
|
|
65
|
+
A row pinned to `disabled: true` — pointer-events suppressed, indicator dims with the row at `sys.state.disabled` opacity. For options contextually unavailable but still belonging in the set (*paywalled tier*, *region-locked option*).
|
|
66
|
+
|
|
67
|
+
```preview
|
|
68
|
+
list/radio-disabled-item
|
|
69
|
+
---
|
|
70
|
+
import { useState } from 'react';
|
|
71
|
+
import { List } from '@teamblind-chorus/ui';
|
|
72
|
+
|
|
73
|
+
const [value, setValue] = useState('week');
|
|
74
|
+
|
|
75
|
+
<List
|
|
76
|
+
variant="radio"
|
|
77
|
+
value={value}
|
|
78
|
+
onChange={setValue}
|
|
79
|
+
items={[
|
|
80
|
+
{ value: 'day', label: 'Day' },
|
|
81
|
+
{ value: 'week', label: 'Week' },
|
|
82
|
+
{ value: 'month', label: 'Month' },
|
|
83
|
+
{ value: 'quarter', label: 'Quarter', disabled: true },
|
|
84
|
+
{ value: 'year', label: 'Year' },
|
|
85
|
+
]}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Major category with a second screen
|
|
90
|
+
|
|
91
|
+
A `nav: true` row adds a trailing right-pointing chevron alongside the radio indicator — for a major category that both commits a value *and* opens a deeper screen of sub-options. Selecting the row fires `onChange`; the row's `onClick` routes to the second screen. The chevron is decorative; the whole row is the single click target.
|
|
92
|
+
|
|
93
|
+
```preview
|
|
94
|
+
list/radio-nav
|
|
95
|
+
---
|
|
96
|
+
import { useState } from 'react';
|
|
97
|
+
import { List } from '@teamblind-chorus/ui';
|
|
98
|
+
|
|
99
|
+
const [value, setValue] = useState('apparel');
|
|
100
|
+
|
|
101
|
+
<List
|
|
102
|
+
variant="radio"
|
|
103
|
+
value={value}
|
|
104
|
+
onChange={setValue}
|
|
105
|
+
aria-label="Choose a category"
|
|
106
|
+
items={[
|
|
107
|
+
{ value: 'all', label: 'All categories' },
|
|
108
|
+
{ value: 'apparel', label: 'Apparel', supportingText: 'Tops, outerwear, footwear', nav: true, onClick: () => {} },
|
|
109
|
+
{ value: 'home', label: 'Home & living', supportingText: 'Furniture, decor, kitchen', nav: true, onClick: () => {} },
|
|
110
|
+
{ value: 'beauty', label: 'Beauty' },
|
|
111
|
+
]}
|
|
112
|
+
/>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Slots
|
|
116
|
+
|
|
117
|
+
- **container** — outer vertical stack (delegates to family).
|
|
118
|
+
- **row** — single list item; whole row is the click target.
|
|
119
|
+
- **leading** — required. 24×24 (`sys.icon.lg`) radio indicator: `RadioIcon` (outline) at rest, `RadioFillIcon` (primary) when selected. Decorative.
|
|
120
|
+
- **label** — primary row text. 16px / Regular / `onSurface`.
|
|
121
|
+
- **supportingText** *(optional)* — secondary line under label.
|
|
122
|
+
- **navChevron** *(optional, per-row)* — auto-rendered 16px right-pointing chevron, painted when the row sets `nav: true` — a major category that opens a second screen. `onSurfaceVariant`, decorative; never a separate hit target.
|
|
123
|
+
|
|
124
|
+
## States
|
|
125
|
+
|
|
126
|
+
`selected` is unique to this sub — the leading indicator switches to `RadioFillIcon` in `primary`; row foreground stays at `onSurface` (no fill change on the row itself).
|
|
127
|
+
|
|
128
|
+
| State | Overlay opacity | Additional |
|
|
129
|
+
|------------|----------------------|------------|
|
|
130
|
+
| `default` | — | Outline indicator. |
|
|
131
|
+
| `hovered` | `sys.state.hover` | `:hover`. |
|
|
132
|
+
| `pressed` | `sys.state.pressed` | `:active`. |
|
|
133
|
+
| `selected` | — | Indicator → `RadioFillIcon` in `primary`. |
|
|
134
|
+
| `disabled` | overlay suppressed | Row at `sys.state.disabled` opacity; indicator dims with the row. |
|
|
135
|
+
|
|
136
|
+
## Focus indicator
|
|
137
|
+
|
|
138
|
+
Inward 3-layer ring inside the row's bounds — see [Focus indicator](./list.md#cross-sub-contract). The row is the keyboard target, not the indicator.
|
|
139
|
+
|
|
140
|
+
## Behavior
|
|
141
|
+
|
|
142
|
+
- **Keyboard navigation.** Arrow ↑ / ↓ moves focus; **Space** and **Enter** commit; Home / End jump to first / last.
|
|
143
|
+
- **Selection model.** Single-select; selecting a row deselects the previous. Controlled via `value` + `onChange`.
|
|
144
|
+
- **Row click target.** Whole row is clickable; the indicator is never a separate hit target.
|