@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,101 @@
|
|
|
1
|
+
# Select
|
|
2
|
+
|
|
3
|
+
Input-shaped picker — same box, label, helper, and error re-tone as [Input](./input.md), but read-only and ending in an `ArrowDownIcon` chevron. Clicking opens a Bottom Sheet with the option list; chosen value is echoed back through `value`.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the user picks one value from a known set that's too long for inline chips — country, currency, sort order, equity tier. **Skip when** the value is free text ([Input](./input.md)), the user searches an open set ([Search bar](./search.md)), or the list is short enough to surface inline as a [Radio list](../list/radio.md).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `inline` — ships no padding outside its own box chrome. Sits inside the host form column (settings page, Dialog body, BottomSheet form group) with the host paying surrounding stack rhythm and inline padding. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), the host already owns the inset — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
Neutral at-rest field — transparent fill, hairline `outlineVariant` stroke, placeholder in faint `outline` colour. The trailing 16px chevron signals that the field opens a sheet rather than a caret.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
form-field/select/default
|
|
15
|
+
---
|
|
16
|
+
import { FormField } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<FormField variant="select" placeholder="Select an option" onOpen={() => {}} />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use cases
|
|
22
|
+
|
|
23
|
+
### With leading icon
|
|
24
|
+
|
|
25
|
+
Optional `leadingIcon` (16px / `sys.icon.md`) pins inner-left — same affordance as Search bar's glyph. Available on `input` and `select`.
|
|
26
|
+
|
|
27
|
+
```preview
|
|
28
|
+
form-field/select/with-leading-icon
|
|
29
|
+
---
|
|
30
|
+
import { FormField } from '@teamblind-chorus/ui';
|
|
31
|
+
import { GlobeIcon } from '@teamblind-chorus/ui/icons';
|
|
32
|
+
|
|
33
|
+
<FormField
|
|
34
|
+
variant="select"
|
|
35
|
+
label="Country"
|
|
36
|
+
leadingIcon={<GlobeIcon />}
|
|
37
|
+
placeholder="Choose country"
|
|
38
|
+
/>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Group
|
|
42
|
+
|
|
43
|
+
Pair a Select with an Input on one row via `<FormFieldGroup direction="horizontal">`. The group owns one shared label above and helper below; children render as bare boxes joined at `sys.layout.inline.md` gap. Typical pattern: leading Select (country dial code, currency, unit) + trailing real Input.
|
|
44
|
+
|
|
45
|
+
```preview
|
|
46
|
+
form-field/select/group
|
|
47
|
+
---
|
|
48
|
+
import { FormField, FormFieldGroup } from '@teamblind-chorus/ui';
|
|
49
|
+
|
|
50
|
+
<FormFieldGroup direction="horizontal" label="Phone number" helper="We'll text a one-time code">
|
|
51
|
+
<FormField variant="select" value="+82" onOpen={() => {}} style={{ flex: '0 0 96px' }} />
|
|
52
|
+
<FormField variant="input" placeholder="010-0000-0000" />
|
|
53
|
+
</FormFieldGroup>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Focus indicator
|
|
57
|
+
|
|
58
|
+
Same as [Input → Focus indicator](./input.md#focus-indicator) — layered on top of the `active` border re-tone.
|
|
59
|
+
|
|
60
|
+
```preview
|
|
61
|
+
form-field/select/focused
|
|
62
|
+
---
|
|
63
|
+
import { FormField } from '@teamblind-chorus/ui';
|
|
64
|
+
|
|
65
|
+
<FormField
|
|
66
|
+
variant="select"
|
|
67
|
+
placeholder="Select an option"
|
|
68
|
+
state="focused"
|
|
69
|
+
onOpen={() => {}}
|
|
70
|
+
/>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Appearance
|
|
74
|
+
|
|
75
|
+
Same two-appearance axis as [Input → Appearance](./input.md#appearance) — `default` and `error`. The error re-tone covers box, chevron, label and helper rung; placeholder text steps to `onErrorContainer`.
|
|
76
|
+
|
|
77
|
+
## Slots
|
|
78
|
+
|
|
79
|
+
- **container** — same as [Input → Slots](./input.md#slots). Read-only.
|
|
80
|
+
- **leading** *(optional)* — 16px decorative glyph at the inner-left edge.
|
|
81
|
+
- **input** — read-only echo of the picked value (or placeholder when empty). Not editable.
|
|
82
|
+
- **dropdown** — trailing `ArrowDownIcon` chevron (16px / `sys.icon.md`). Always present; clicking it fires `onOpen` (so does parent box click).
|
|
83
|
+
- **label** *(optional)* / **helper** *(optional)* — same anatomy as Input. `count` is **not** offered (read-only field has nothing to count).
|
|
84
|
+
|
|
85
|
+
## Sizes
|
|
86
|
+
|
|
87
|
+
Identical footprint to [Input → Sizes](./input.md#sizes) — 40px tall, `sys.radius.md` corners, hairline rest stroke, thin active stroke as inset `box-shadow`. The trailing chevron occupies the slot Input reserves for clear.
|
|
88
|
+
|
|
89
|
+
## States
|
|
90
|
+
|
|
91
|
+
Same five states as [Input → States](./input.md#states). `active` is reached by clicking the field (the sheet then takes over) rather than by a caret.
|
|
92
|
+
|
|
93
|
+
## Focus indicator
|
|
94
|
+
|
|
95
|
+
Same composition as [Input → Focus indicator](./input.md#focus-indicator). Suppressed while `disabled`. Trigger: `:focus-visible`.
|
|
96
|
+
|
|
97
|
+
## Behavior
|
|
98
|
+
|
|
99
|
+
- **Read-only.** Does not accept keystrokes — `value` is set by the consumer when a sheet option is picked.
|
|
100
|
+
- **Open on click.** Clicking the box (or the chevron) fires `onOpen`. The consumer owns the `BottomSheet` state and the option list (typically a [Radio list](../list/radio.md)); on pick, closes the sheet and updates `value`. The docs preview shows the field at rest only.
|
|
101
|
+
- **No clear button.** Trailing slot is the chevron; clearing is done by the sheet's option list.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "FormField",
|
|
4
|
+
"family": "form-field",
|
|
5
|
+
"subcomponent": "select",
|
|
6
|
+
"exportAlias": "Select",
|
|
7
|
+
"description": "Input-shaped picker — the same transparent-fill box, hairline-at-rest / thin-at-active inset box-shadow stroke, label / helper rungs and `error` re-tone as Input, but the field is read-only and the trailing slot carries a `ArrowDownIcon` chevron (16px / `sys.icon.md`). Clicking anywhere in the box fires `onOpen`, and the consumer raises a `BottomSheet` (or other surface) with the option list. The chosen value is echoed back through `value`. Supports an optional leading icon, sized to `sys.icon.md`, that pins at the inner-left edge — same affordance as Search bar's glyph.",
|
|
8
|
+
"element": "div",
|
|
9
|
+
"props": {
|
|
10
|
+
"variant": {
|
|
11
|
+
"type": "literal",
|
|
12
|
+
"value": "select"
|
|
13
|
+
},
|
|
14
|
+
"appearance": {
|
|
15
|
+
"type": "enum",
|
|
16
|
+
"values": [
|
|
17
|
+
"default",
|
|
18
|
+
"error"
|
|
19
|
+
],
|
|
20
|
+
"default": "default"
|
|
21
|
+
},
|
|
22
|
+
"value": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"optional": true
|
|
25
|
+
},
|
|
26
|
+
"defaultValue": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"optional": true
|
|
29
|
+
},
|
|
30
|
+
"placeholder": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"label": {
|
|
35
|
+
"type": "node",
|
|
36
|
+
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"helper": {
|
|
39
|
+
"type": "node",
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"leadingIcon": {
|
|
43
|
+
"type": "node",
|
|
44
|
+
"optional": true,
|
|
45
|
+
"description": "Optional 16px (`sys.icon.md`) decorative glyph pinned at the inner-left edge of the field. Tracks the field's active text colour (`sys.color.onSurface` on the default appearance, `sys.color.onErrorContainer` on `error`) so the glyph reads as part of the typed content."
|
|
46
|
+
},
|
|
47
|
+
"onOpen": {
|
|
48
|
+
"type": "function",
|
|
49
|
+
"optional": true,
|
|
50
|
+
"description": "Fired when the field box or the trailing chevron is clicked. Consumers use this to raise a `BottomSheet` with the option list."
|
|
51
|
+
},
|
|
52
|
+
"disabled": {
|
|
53
|
+
"type": "boolean",
|
|
54
|
+
"default": false
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"slots": {
|
|
58
|
+
"container": {
|
|
59
|
+
"required": true,
|
|
60
|
+
"intrinsic": true,
|
|
61
|
+
"description": "The box — same as Input."
|
|
62
|
+
},
|
|
63
|
+
"leading": {
|
|
64
|
+
"required": false,
|
|
65
|
+
"description": "Optional 16px glyph at the inner-left edge. Decorative (`aria-hidden`).",
|
|
66
|
+
"accepts": [
|
|
67
|
+
"icon"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"input": {
|
|
71
|
+
"required": true,
|
|
72
|
+
"description": "Read-only echo of the selected value (or the placeholder when empty).",
|
|
73
|
+
"accepts": [
|
|
74
|
+
"text"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"dropdown": {
|
|
78
|
+
"required": true,
|
|
79
|
+
"intrinsic": true,
|
|
80
|
+
"description": "Trailing chevron button (`ArrowDownIcon`, 16px). Always present, fires `onOpen`."
|
|
81
|
+
},
|
|
82
|
+
"label": {
|
|
83
|
+
"required": false,
|
|
84
|
+
"accepts": [
|
|
85
|
+
"text"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"helper": {
|
|
89
|
+
"required": false,
|
|
90
|
+
"accepts": [
|
|
91
|
+
"text"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"sizing": {
|
|
96
|
+
"minHeight": "ref.space.500",
|
|
97
|
+
"paddingBlock": "sys.layout.container.xs",
|
|
98
|
+
"paddingInline": "sys.layout.container.sm",
|
|
99
|
+
"slotGap": "sys.layout.inline.md",
|
|
100
|
+
"radius": "sys.radius.md",
|
|
101
|
+
"borderWidth": "sys.borderWidth.hairline",
|
|
102
|
+
"activeStrokeWeight": "sys.borderWidth.thin",
|
|
103
|
+
"groupGap": "sys.layout.stack.xs",
|
|
104
|
+
"labelTypo": "sys.typo.label.md",
|
|
105
|
+
"helperTypo": "sys.typo.body.sm",
|
|
106
|
+
"countTypo": "sys.typo.body.sm",
|
|
107
|
+
"countCurrentTypo": "sys.typo.label.md",
|
|
108
|
+
"textTypo": "sys.typo.body.md",
|
|
109
|
+
"iconSize": "sys.icon.md"
|
|
110
|
+
},
|
|
111
|
+
"groupColors": {
|
|
112
|
+
"label": "sys.color.onSurface",
|
|
113
|
+
"helper": "sys.color.onSurfaceVariant",
|
|
114
|
+
"helperError": "sys.color.error",
|
|
115
|
+
"count": "sys.color.onSurfaceVariant",
|
|
116
|
+
"countCurrent": "sys.color.onSurface"
|
|
117
|
+
},
|
|
118
|
+
"appearances": {
|
|
119
|
+
"default": {
|
|
120
|
+
"background": "transparent",
|
|
121
|
+
"text": "sys.color.onSurface",
|
|
122
|
+
"placeholder": "sys.color.outline",
|
|
123
|
+
"borderRest": "sys.color.outlineVariant",
|
|
124
|
+
"borderHover": "sys.color.outline",
|
|
125
|
+
"borderActive": "sys.color.onSurface"
|
|
126
|
+
},
|
|
127
|
+
"error": {
|
|
128
|
+
"background": "sys.color.errorContainer",
|
|
129
|
+
"text": "sys.color.onErrorContainer",
|
|
130
|
+
"placeholder": "sys.color.onErrorContainer",
|
|
131
|
+
"borderRest": "sys.color.error",
|
|
132
|
+
"borderHover": "sys.color.error",
|
|
133
|
+
"borderActive": "sys.color.error"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"states": {
|
|
137
|
+
"default": {
|
|
138
|
+
"overlay": null,
|
|
139
|
+
"border": "borderRest"
|
|
140
|
+
},
|
|
141
|
+
"hovered": {
|
|
142
|
+
"overlay": null,
|
|
143
|
+
"border": "borderHover"
|
|
144
|
+
},
|
|
145
|
+
"pressed": {
|
|
146
|
+
"border": "borderHover",
|
|
147
|
+
"overlay": {
|
|
148
|
+
"color": "text",
|
|
149
|
+
"opacity": "sys.state.pressed"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"active": {
|
|
153
|
+
"overlay": null,
|
|
154
|
+
"border": "borderActive",
|
|
155
|
+
"strokeWeight": "activeStrokeWeight"
|
|
156
|
+
},
|
|
157
|
+
"disabled": {
|
|
158
|
+
"overlay": null,
|
|
159
|
+
"background": "sys.color.surfaceContainerLow",
|
|
160
|
+
"containerOpacity": "sys.state.disabled",
|
|
161
|
+
"suppressFocusRing": true,
|
|
162
|
+
"cursor": "not-allowed"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"focusIndicator": {
|
|
166
|
+
"description": "Same keyboard-focus indicator as Input — outward two-layer ring composed over the active stroke.",
|
|
167
|
+
"composition": "outward",
|
|
168
|
+
"compositionReason": "Action affordance with breathing room around it.",
|
|
169
|
+
"overlay": {
|
|
170
|
+
"color": "label",
|
|
171
|
+
"opacity": "sys.state.focus"
|
|
172
|
+
},
|
|
173
|
+
"ring": {
|
|
174
|
+
"outerWidth": "sys.borderWidth.thin",
|
|
175
|
+
"outerColor": "sys.color.focus",
|
|
176
|
+
"insetWidth": "sys.borderWidth.hairline",
|
|
177
|
+
"insetColor": "sys.color.focusInset"
|
|
178
|
+
},
|
|
179
|
+
"trigger": ":focus-visible"
|
|
180
|
+
},
|
|
181
|
+
"accessibility": {
|
|
182
|
+
"role": "the field box is the combobox trigger: role='combobox' with aria-haspopup='listbox' (the raised BottomSheet hosts the option list as a listbox) and aria-expanded reflecting whether the popup is open. aria-controls points at the popup list's id while open. Mirrors the dropdown-trigger contract already defined on button/text (aria-haspopup / aria-expanded / aria-controls).",
|
|
183
|
+
"focusable": "Because the element is a `div`, it MUST carry tabindex='0' so it is reachable in the tab order — without it the picker is keyboard-inoperable.",
|
|
184
|
+
"keyboard": "Enter, Space, and ArrowDown all fire onOpen (open the option surface) when the field is focused — a keyboard user must be able to open the picker without a pointer. Esc closes the raised surface (owned by the BottomSheet).",
|
|
185
|
+
"labelling": "aria-labelledby points at the label when one is composed; otherwise the consumer supplies an aria-label. aria-describedby points at the helper when rendered.",
|
|
186
|
+
"invalid": "On the `error` appearance the trigger carries aria-invalid='true', cleared when the appearance returns to default — same contract as Input.",
|
|
187
|
+
"leadingIconDecorative": "The leading glyph is aria-hidden (see the leading slot)."
|
|
188
|
+
},
|
|
189
|
+
"forbidden": [
|
|
190
|
+
"select rendered as a raw <select> — select variant wraps the native control with the chorus-field chrome",
|
|
191
|
+
"trailing chevron omitted — select always carries the trailing chevron in the rest state",
|
|
192
|
+
"select trigger that is not keyboard-focusable / openable — the div MUST carry tabindex='0' and open on Enter / Space / ArrowDown; a pointer-only picker is forbidden"
|
|
193
|
+
]
|
|
194
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Textarea
|
|
2
|
+
|
|
3
|
+
Multi-line cousin of [input](./input.md) — identical chrome contract (transparent fill, inset box-shadow stroke, optional `label` / `helper` / `maxLength` group rungs); the inner element is a `<textarea>` with configurable `rows` (default 4) and vertical-only resize.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the value naturally spans multiple lines: compose surfaces, bug reports, profile bios, comment composers. **Skip when** the value is single-line ([input](./input.md)), needs a leading magnifier glyph ([search](./search.md)), or opens a sheet-driven option list ([select](./select.md)).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `inline` — ships no padding outside its own box chrome. Sits inside the host form column (settings page, Dialog body, BottomSheet compose surface) with the host paying surrounding stack rhythm and inline padding. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), the host already owns the inset — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
Labeled multi-line field with helper text. Four rows tall by default; the user drags the bottom-right resize handle to grow taller.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
form-field/textarea
|
|
15
|
+
---
|
|
16
|
+
import { FormField } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<FormField
|
|
19
|
+
variant="textarea"
|
|
20
|
+
label="Description"
|
|
21
|
+
placeholder="Add a description for your channel"
|
|
22
|
+
helper="Up to 280 characters. Markdown is supported."
|
|
23
|
+
/>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Use cases
|
|
27
|
+
|
|
28
|
+
### Error appearance
|
|
29
|
+
|
|
30
|
+
`appearance="error"` re-tones container to `errorContainer` and stroke to `error`. The optional `helper` paints in `sys.color.error` as the error caption.
|
|
31
|
+
|
|
32
|
+
```preview
|
|
33
|
+
form-field/textarea-error
|
|
34
|
+
---
|
|
35
|
+
import { FormField } from '@teamblind-chorus/ui';
|
|
36
|
+
|
|
37
|
+
<FormField
|
|
38
|
+
variant="textarea"
|
|
39
|
+
label="Description"
|
|
40
|
+
defaultValue=""
|
|
41
|
+
appearance="error"
|
|
42
|
+
helper="Description is required."
|
|
43
|
+
/>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### With character count
|
|
47
|
+
|
|
48
|
+
`maxLength` caps value length and renders a `current/max` count below the box (right-aligned). Mutually exclusive with `helper` — count wins.
|
|
49
|
+
|
|
50
|
+
```preview
|
|
51
|
+
form-field/textarea-count
|
|
52
|
+
---
|
|
53
|
+
import { FormField } from '@teamblind-chorus/ui';
|
|
54
|
+
|
|
55
|
+
<FormField
|
|
56
|
+
variant="textarea"
|
|
57
|
+
label="Bio"
|
|
58
|
+
defaultValue="Designing for clarity, building for trust."
|
|
59
|
+
maxLength={140}
|
|
60
|
+
rows={3}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Slots
|
|
65
|
+
|
|
66
|
+
Delegates to the family group anatomy. Single divergence vs [input](./input.md) is the inner element:
|
|
67
|
+
|
|
68
|
+
- **textarea** — multi-line editable text. `body.md` typo, `resize: vertical`, `rows` minimum. No trailing clear (multi-line content is too costly to wipe in one click).
|
|
69
|
+
|
|
70
|
+
See [input.md § Slots](./input.md#slots) for shared label / container / helper / count slots.
|
|
71
|
+
|
|
72
|
+
## Anatomy
|
|
73
|
+
|
|
74
|
+
| Slot | Token bindings |
|
|
75
|
+
|-----------|----------------|
|
|
76
|
+
| group | Same as [input.md § Anatomy](./input.md#anatomy) |
|
|
77
|
+
| label | Same as input |
|
|
78
|
+
| container | Same chrome; `align-items: stretch` so the textarea fills the box height |
|
|
79
|
+
| textarea | `sys.typo.body.md`, `resize: vertical`, `rows` floor (default 4) |
|
|
80
|
+
| helper | Same as input |
|
|
81
|
+
| count | Same as input |
|
|
82
|
+
|
|
83
|
+
## Behavior
|
|
84
|
+
|
|
85
|
+
- **`rows` floor.** At least `rows` tall (default 4). Drag the bottom-right handle to grow.
|
|
86
|
+
- **Vertical resize only.** `resize: vertical` — horizontal resize breaks the parent column's rhythm and is forbidden.
|
|
87
|
+
- **No clear button.** Multi-line content is too costly to wipe in one click. Users clear by selecting and deleting.
|
|
88
|
+
- **`maxLength` precedence.** When both `helper` and `maxLength` are supplied, count wins and `helper` is ignored. Pick one per field.
|
|
89
|
+
- **Disabled state.** Same as input — `sys.color.surfaceContainerLow` background at `sys.state.disabled` opacity, `cursor: not-allowed`.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "FormField",
|
|
4
|
+
"family": "form-field",
|
|
5
|
+
"subcomponent": "textarea",
|
|
6
|
+
"exportAlias": "Textarea",
|
|
7
|
+
"description": "Multi-line text field. Identical chrome contract to [input](./input.md) — transparent-fill box whose visible stroke is an inset box-shadow (never a `border`, so the field's footprint is pixel-stable across every state), 16px text in `body.md`, optional `label` above and `helper` / `maxLength` below. Diverges only on the inner element: a `<textarea>` instead of a single-line `<input>`, with a configurable `rows` rung (default 4) and a vertical-only resize handle. Same `label` / `helper` / `maxLength` group rules as input — multi-line + character count is the canonical Chorus pairing for any composition field that bounds the message length.",
|
|
8
|
+
"element": "textarea",
|
|
9
|
+
"props": {
|
|
10
|
+
"variant": {
|
|
11
|
+
"type": "literal",
|
|
12
|
+
"value": "textarea"
|
|
13
|
+
},
|
|
14
|
+
"appearance": {
|
|
15
|
+
"type": "enum",
|
|
16
|
+
"values": ["default", "error"],
|
|
17
|
+
"default": "default"
|
|
18
|
+
},
|
|
19
|
+
"value": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"optional": true
|
|
22
|
+
},
|
|
23
|
+
"defaultValue": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"optional": true
|
|
26
|
+
},
|
|
27
|
+
"placeholder": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"optional": true
|
|
30
|
+
},
|
|
31
|
+
"label": {
|
|
32
|
+
"type": "node",
|
|
33
|
+
"optional": true,
|
|
34
|
+
"description": "Visible label rendered above the field box and associated with it (`<label htmlFor>`)."
|
|
35
|
+
},
|
|
36
|
+
"helper": {
|
|
37
|
+
"type": "node",
|
|
38
|
+
"optional": true,
|
|
39
|
+
"description": "Assistive text rendered below the field box, left-aligned. Same rules as [input.helper](./input.md): mutually exclusive with `maxLength`, optional on every appearance, re-tones to `sys.color.error` on the error appearance."
|
|
40
|
+
},
|
|
41
|
+
"maxLength": {
|
|
42
|
+
"type": "number",
|
|
43
|
+
"optional": true,
|
|
44
|
+
"description": "Caps the value length and renders a `current/max` character count below the box, right-aligned. Mutually exclusive with `helper`."
|
|
45
|
+
},
|
|
46
|
+
"rows": {
|
|
47
|
+
"type": "number",
|
|
48
|
+
"default": 4,
|
|
49
|
+
"description": "Minimum visible rows. The textarea is `resize: vertical` so the user can pull it taller; the value never shrinks below `rows`."
|
|
50
|
+
},
|
|
51
|
+
"disabled": {
|
|
52
|
+
"type": "boolean",
|
|
53
|
+
"default": false
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"slots": {
|
|
57
|
+
"group": {
|
|
58
|
+
"required": false,
|
|
59
|
+
"description": "Wrapper around the label + box + helper/count, present only when at least one of `label` / `helper` / `maxLength` is supplied. Delegates to the input/textarea-shared group spec.",
|
|
60
|
+
"intrinsic": true
|
|
61
|
+
},
|
|
62
|
+
"label": {
|
|
63
|
+
"required": false,
|
|
64
|
+
"description": "Visible label above the box. `sys.typo.label.md`, `sys.color.onSurface`. Associated with the textarea via `htmlFor`.",
|
|
65
|
+
"accepts": ["text"]
|
|
66
|
+
},
|
|
67
|
+
"container": {
|
|
68
|
+
"required": true,
|
|
69
|
+
"description": "The box — owns the transparent fill, the stroke (an inset box-shadow), radius, padding, focus ring. Same chrome as input; differs only in `align-items: stretch` so the inner textarea fills the box height.",
|
|
70
|
+
"intrinsic": true
|
|
71
|
+
},
|
|
72
|
+
"textarea": {
|
|
73
|
+
"required": true,
|
|
74
|
+
"description": "The editable multi-line text. `body.md` typo, `resize: vertical`, `rows` minimum. Carries `aria-describedby` pointing at the helper / count when present. No trailing clear button — the value can be long enough that wiping it via a single click is hostile; the user clears multi-line content by selecting text.",
|
|
75
|
+
"accepts": ["text"]
|
|
76
|
+
},
|
|
77
|
+
"helper": {
|
|
78
|
+
"required": false,
|
|
79
|
+
"description": "Assistive text below the box. Same rules as input.helper.",
|
|
80
|
+
"accepts": ["text"]
|
|
81
|
+
},
|
|
82
|
+
"count": {
|
|
83
|
+
"required": false,
|
|
84
|
+
"description": "`current/max` character count below the box, right-aligned. Same rules as input.count.",
|
|
85
|
+
"accepts": ["text"]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"sizing": {
|
|
89
|
+
"minHeight": "ref.space.500",
|
|
90
|
+
"paddingBlock": "sys.layout.container.xs",
|
|
91
|
+
"paddingInline": "sys.layout.container.sm",
|
|
92
|
+
"slotGap": "sys.layout.inline.md",
|
|
93
|
+
"radius": "sys.radius.md",
|
|
94
|
+
"borderWidth": "sys.borderWidth.hairline",
|
|
95
|
+
"activeStrokeWeight": "sys.borderWidth.thin",
|
|
96
|
+
"groupGap": "sys.layout.stack.xs",
|
|
97
|
+
"labelTypo": "sys.typo.label.md",
|
|
98
|
+
"helperTypo": "sys.typo.body.sm",
|
|
99
|
+
"countTypo": "sys.typo.body.sm",
|
|
100
|
+
"countCurrentTypo": "sys.typo.label.md",
|
|
101
|
+
"textTypo": "sys.typo.body.md",
|
|
102
|
+
"iconSize": "sys.icon.md",
|
|
103
|
+
"rowsDefault": 4,
|
|
104
|
+
"resize": "vertical"
|
|
105
|
+
},
|
|
106
|
+
"groupColors": {
|
|
107
|
+
"label": "sys.color.onSurface",
|
|
108
|
+
"helper": "sys.color.onSurfaceVariant",
|
|
109
|
+
"helperError": "sys.color.error",
|
|
110
|
+
"count": "sys.color.onSurfaceVariant",
|
|
111
|
+
"countCurrent": "sys.color.onSurface"
|
|
112
|
+
},
|
|
113
|
+
"appearances": {
|
|
114
|
+
"default": {
|
|
115
|
+
"background": "transparent",
|
|
116
|
+
"text": "sys.color.onSurface",
|
|
117
|
+
"placeholder": "sys.color.outline",
|
|
118
|
+
"borderRest": "sys.color.outlineVariant",
|
|
119
|
+
"borderHover": "sys.color.outline",
|
|
120
|
+
"borderActive": "sys.color.onSurface"
|
|
121
|
+
},
|
|
122
|
+
"error": {
|
|
123
|
+
"background": "sys.color.errorContainer",
|
|
124
|
+
"text": "sys.color.onErrorContainer",
|
|
125
|
+
"placeholder": "sys.color.onErrorContainer",
|
|
126
|
+
"borderRest": "sys.color.error",
|
|
127
|
+
"borderHover": "sys.color.error",
|
|
128
|
+
"borderActive": "sys.color.error"
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"states": {
|
|
132
|
+
"default": { "overlay": null, "border": "borderRest" },
|
|
133
|
+
"hovered": { "overlay": null, "border": "borderHover" },
|
|
134
|
+
"pressed": {
|
|
135
|
+
"border": "borderHover",
|
|
136
|
+
"overlay": { "color": "text", "opacity": "sys.state.pressed" }
|
|
137
|
+
},
|
|
138
|
+
"active": {
|
|
139
|
+
"overlay": null,
|
|
140
|
+
"border": "borderActive",
|
|
141
|
+
"strokeWeight": "activeStrokeWeight",
|
|
142
|
+
"caret": "visible",
|
|
143
|
+
"note": "Stroke steps from `hairline` (1px) to `activeStrokeWeight` (2px) as an inset box-shadow — same pixel-stable contract as input."
|
|
144
|
+
},
|
|
145
|
+
"disabled": {
|
|
146
|
+
"overlay": null,
|
|
147
|
+
"background": "sys.color.surfaceContainerLow",
|
|
148
|
+
"containerOpacity": "sys.state.disabled",
|
|
149
|
+
"suppressFocusRing": true,
|
|
150
|
+
"cursor": "not-allowed"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"focusIndicator": {
|
|
154
|
+
"description": "Same outward 3-layer ring as input.focusIndicator.",
|
|
155
|
+
"composition": "outward",
|
|
156
|
+
"compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
|
|
157
|
+
"overlay": {
|
|
158
|
+
"color": "label",
|
|
159
|
+
"opacity": "sys.state.focus"
|
|
160
|
+
},
|
|
161
|
+
"ring": {
|
|
162
|
+
"outerWidth": "sys.borderWidth.thin",
|
|
163
|
+
"outerColor": "sys.color.focus",
|
|
164
|
+
"insetWidth": "sys.borderWidth.hairline",
|
|
165
|
+
"insetColor": "sys.color.focusInset"
|
|
166
|
+
},
|
|
167
|
+
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
168
|
+
},
|
|
169
|
+
"forbidden": [
|
|
170
|
+
"raw <textarea> styled with Tailwind / inline color — the textarea is wrapped in the chorus-field chrome that owns the stroke",
|
|
171
|
+
"auto-grow without bounds — the textarea has a `rows` floor; tall content is reachable by the user via the resize handle, not by an uncapped programmatic grow",
|
|
172
|
+
"horizontal resize handle — only the vertical handle is allowed (`resize: vertical`); horizontal resize breaks the parent column's rhythm",
|
|
173
|
+
"trailing clear button — multi-line content is too costly to wipe in one click",
|
|
174
|
+
"stroke painted via `border:` — stroke is an inset box-shadow on the field"
|
|
175
|
+
]
|
|
176
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "header",
|
|
4
|
+
"name": "Header",
|
|
5
|
+
"description": "Labelled-header family. Two members split on the tone/weight axis: `main` (the louder member, `<Header>`) is an `onSurface` section heading at 16 / 20 that owns the See-all action, icon drill-in, and sort/filter dropdown affordances; `sub` (the quieter member, `<SubHeader>`) is a muted `onSurfaceVariant` group label at 14 that names the rows beneath it and carries at most a single Text Button action. Both are full-bleed (transparent background, own `container.md` 16 inline rail), share the heading anatomy (leading label + optional trailing affordance), and head a block of content below — `main` above a list / carousel / feed / card / sheet section, `sub` above a grouped sub-stack within an already-headed region.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"section heading",
|
|
8
|
+
"see-all link",
|
|
9
|
+
"in-sheet sub-section heading",
|
|
10
|
+
"bounded card heading",
|
|
11
|
+
"labelled block above a list / carousel / feed",
|
|
12
|
+
"quiet group label above a grouped stack of rows"
|
|
13
|
+
],
|
|
14
|
+
"visualReuse": "open",
|
|
15
|
+
"layoutInset": "full-bleed",
|
|
16
|
+
"wrapperGuidance": "Full-bleed: both members own their inline padding internally — `container.md` (16), the same rail the List rows / Feed items they head pay — plus asymmetric block padding (`main`: `stack.lg` 24 above / `stack.md` 16 below; `sub`: `stack.lg` 24 above / `stack.xs` 8 below). Drop the header directly above a labelled region as a sibling of the full-bleed content it heads; both land on the shared rail with no host help. Do NOT wrap it in an extra `padding-inline` div, `className=\"px-*\"`, or `style={{ padding }}`, and do NOT pay the rail again on the host — the gutter is paid once, on the header. Hosts that bundle a header above a List/Feed (DirectoryList, NavList, SideSheet column) stay full-bleed (no inline padding of their own) and absorb only the header's block padding into their stack rhythm. The transparent background means the header paints on whatever surface tier hosts it.",
|
|
17
|
+
"spec": "header.md",
|
|
18
|
+
"usage": {
|
|
19
|
+
"subs": {
|
|
20
|
+
"main": {
|
|
21
|
+
"import": "Header",
|
|
22
|
+
"example": "<Header label=\"Recommended channels\" headerAction={{ label: 'See all', href: '#all' }} />"
|
|
23
|
+
},
|
|
24
|
+
"sub": {
|
|
25
|
+
"import": "SubHeader",
|
|
26
|
+
"example": "<SubHeader label=\"Following\" />"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"subcomponents": [
|
|
31
|
+
{
|
|
32
|
+
"slug": "main",
|
|
33
|
+
"spec": "main.spec.json",
|
|
34
|
+
"md": "main.md",
|
|
35
|
+
"default": true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"slug": "sub",
|
|
39
|
+
"spec": "sub.spec.json",
|
|
40
|
+
"md": "sub.md"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Header
|
|
2
|
+
|
|
3
|
+
The labelled header that names a region and exposes its one highest-priority affordance. Two members share this contract and split on a **tone/weight axis**: **[Main](./main.md)** (`<Header>`) — the louder `onSurface` section heading at 16 / 20 that owns the See-all action, the icon drill-in, and the sort/filter dropdown — and **[Sub](./sub.md)** (`<SubHeader>`) — the quieter muted `onSurfaceVariant` group label at 14 that names the rows beneath it and carries at most a single Text Button action.
|
|
4
|
+
|
|
5
|
+
**Layout inset.** `full-bleed` — both members have a transparent background and own their `container.md` (16) inline rail, so the label lands on the same rail as the List rows / Feed items it heads with no host help. Drop the header directly above the labelled region as a sibling; do **not** wrap it in a `padding-inline` / `px-*` / `style={{ padding }}` div, and do not pay the rail again on the host. A bundling host (Carousel, DirectoryList, NavList, SideSheet column) stays full-bleed and absorbs only the header's block padding into its stack rhythm. See [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
6
|
+
|
|
7
|
+
## Cross-sub contract
|
|
8
|
+
|
|
9
|
+
- **Full-bleed, transparent.** Both members paint no fill and pay `sys.layout.container.md` (16) inline so the label aligns with the content rail beneath it. The header composes on whatever surface tier hosts it — never give it an opaque background.
|
|
10
|
+
- **Asymmetric block rhythm.** `sys.layout.stack.lg` (24) above breaks from the previous region; the below padding binds the label to its block — `sys.layout.stack.md` (16) for Main, `sys.layout.stack.xs` (8) for the quieter Sub.
|
|
11
|
+
- **Leading label + one trailing affordance.** A leading heading label and an optional trailing commit on opposite edges of a `space-between` row, separated by `sys.layout.inline.md` (8). The trailing affordance owns its own tap target; the wrapper is never interactive.
|
|
12
|
+
- **Text Button actions are accent.** Any trailing Text Button reading as a link (`See all`, `Edit`) is `size="xsmall"`, `appearance="accent"` per the link-affordance rule.
|
|
13
|
+
- **The split is tone/weight, not the presence of an action.** Both members may carry a trailing Text Button; only Main adds the icon drill-in and dropdown disclosure, because its `onSurface` heading earns the heavier affordance set.
|
|
14
|
+
|
|
15
|
+
## Sub-components
|
|
16
|
+
|
|
17
|
+
- **[Main](./main.md)** (`<Header>`) — Louder section heading. Label in `onSurface` at `typo.heading.md` (20 / Semibold, `size="large"`) or `typo.heading.sm` (16 / Semibold, `size="medium"`); trailing affordance is a See-all Text Button, an icon drill-in chevron, or a sort/filter dropdown (mutually exclusive). The canonical heading above a list / carousel / feed / card / sheet section.
|
|
18
|
+
- **[Sub](./sub.md)** (`<SubHeader>`) — Quieter group label. A single muted `onSurfaceVariant` line at `typo.label.md` (14 / Semibold), no size axis; carries at most one trailing Text Button action. Names a group of rows within an already-headed region ("Following", "More Topics to follow").
|