@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.
Files changed (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/agents/AGENTS.md +143 -0
  4. package/agents/DESIGN.md +1311 -0
  5. package/agents/LOVABLE.md +472 -0
  6. package/agents/anti-patterns.md +533 -0
  7. package/agents/catalog.md +232 -0
  8. package/agents/components/avatar-rail/avatar-rail.family.json +46 -0
  9. package/agents/components/avatar-rail/avatar-rail.md +103 -0
  10. package/agents/components/avatar-rail/avatar-rail.spec.json +160 -0
  11. package/agents/components/badge/badge.family.json +45 -0
  12. package/agents/components/badge/badge.md +10 -0
  13. package/agents/components/badge/role.md +100 -0
  14. package/agents/components/badge/role.spec.json +75 -0
  15. package/agents/components/badge/update.md +132 -0
  16. package/agents/components/badge/update.spec.json +114 -0
  17. package/agents/components/banner/banner.family.json +28 -0
  18. package/agents/components/banner/banner.md +136 -0
  19. package/agents/components/banner/banner.spec.json +136 -0
  20. package/agents/components/bottom-sheet/bottom-sheet.family.json +29 -0
  21. package/agents/components/bottom-sheet/bottom-sheet.md +176 -0
  22. package/agents/components/bottom-sheet/bottom-sheet.spec.json +168 -0
  23. package/agents/components/bubble/bubble.family.json +29 -0
  24. package/agents/components/bubble/bubble.md +134 -0
  25. package/agents/components/bubble/bubble.spec.json +91 -0
  26. package/agents/components/button/button.family.json +76 -0
  27. package/agents/components/button/button.md +31 -0
  28. package/agents/components/button/check.md +138 -0
  29. package/agents/components/button/check.spec.json +161 -0
  30. package/agents/components/button/fab.md +161 -0
  31. package/agents/components/button/fab.spec.json +106 -0
  32. package/agents/components/button/icon.md +141 -0
  33. package/agents/components/button/icon.spec.json +164 -0
  34. package/agents/components/button/standard.md +219 -0
  35. package/agents/components/button/standard.spec.json +205 -0
  36. package/agents/components/button/text.md +186 -0
  37. package/agents/components/button/text.spec.json +215 -0
  38. package/agents/components/button/toggle.md +108 -0
  39. package/agents/components/button/toggle.spec.json +124 -0
  40. package/agents/components/button/toolbar.md +189 -0
  41. package/agents/components/button/toolbar.spec.json +109 -0
  42. package/agents/components/carousel/carousel.family.json +41 -0
  43. package/agents/components/carousel/carousel.md +40 -0
  44. package/agents/components/carousel/post.md +148 -0
  45. package/agents/components/carousel/post.spec.json +229 -0
  46. package/agents/components/carousel/profile.md +184 -0
  47. package/agents/components/carousel/profile.spec.json +219 -0
  48. package/agents/components/chip/chip.family.json +37 -0
  49. package/agents/components/chip/chip.md +10 -0
  50. package/agents/components/chip/filter.md +212 -0
  51. package/agents/components/chip/filter.spec.json +124 -0
  52. package/agents/components/chip/tag.md +137 -0
  53. package/agents/components/chip/tag.spec.json +104 -0
  54. package/agents/components/dialog/dialog.family.json +29 -0
  55. package/agents/components/dialog/dialog.md +113 -0
  56. package/agents/components/dialog/dialog.spec.json +156 -0
  57. package/agents/components/directory-list/directory-list.family.json +46 -0
  58. package/agents/components/directory-list/directory-list.md +87 -0
  59. package/agents/components/directory-list/directory-list.spec.json +104 -0
  60. package/agents/components/divider/divider.family.json +28 -0
  61. package/agents/components/divider/divider.md +78 -0
  62. package/agents/components/divider/divider.spec.json +51 -0
  63. package/agents/components/feed/ad.md +108 -0
  64. package/agents/components/feed/ad.spec.json +187 -0
  65. package/agents/components/feed/feed.family.json +48 -0
  66. package/agents/components/feed/feed.md +30 -0
  67. package/agents/components/feed/post.md +240 -0
  68. package/agents/components/feed/post.spec.json +361 -0
  69. package/agents/components/form-field/form-field.family.json +50 -0
  70. package/agents/components/form-field/form-field.md +11 -0
  71. package/agents/components/form-field/input.md +198 -0
  72. package/agents/components/form-field/input.spec.json +202 -0
  73. package/agents/components/form-field/search.md +81 -0
  74. package/agents/components/form-field/search.spec.json +135 -0
  75. package/agents/components/form-field/select.md +101 -0
  76. package/agents/components/form-field/select.spec.json +194 -0
  77. package/agents/components/form-field/textarea.md +89 -0
  78. package/agents/components/form-field/textarea.spec.json +176 -0
  79. package/agents/components/header/header.family.json +43 -0
  80. package/agents/components/header/header.md +18 -0
  81. package/agents/components/header/main.md +101 -0
  82. package/agents/components/header/main.spec.json +117 -0
  83. package/agents/components/header/sub.md +129 -0
  84. package/agents/components/header/sub.spec.json +81 -0
  85. package/agents/components/list/accordion.md +183 -0
  86. package/agents/components/list/accordion.spec.json +201 -0
  87. package/agents/components/list/entry.md +280 -0
  88. package/agents/components/list/entry.spec.json +237 -0
  89. package/agents/components/list/list.family.json +75 -0
  90. package/agents/components/list/list.md +24 -0
  91. package/agents/components/list/radio.md +144 -0
  92. package/agents/components/list/radio.spec.json +186 -0
  93. package/agents/components/list/standard.md +262 -0
  94. package/agents/components/list/standard.spec.json +221 -0
  95. package/agents/components/metadata/compact.md +69 -0
  96. package/agents/components/metadata/compact.spec.json +69 -0
  97. package/agents/components/metadata/metadata.family.json +42 -0
  98. package/agents/components/metadata/metadata.md +26 -0
  99. package/agents/components/metadata/standard.md +104 -0
  100. package/agents/components/metadata/standard.spec.json +152 -0
  101. package/agents/components/nav-card/nav-card.family.json +29 -0
  102. package/agents/components/nav-card/nav-card.md +179 -0
  103. package/agents/components/nav-card/nav-card.spec.json +161 -0
  104. package/agents/components/nav-list/nav-list.family.json +46 -0
  105. package/agents/components/nav-list/nav-list.md +91 -0
  106. package/agents/components/nav-list/nav-list.spec.json +107 -0
  107. package/agents/components/navigation-bar/main.md +201 -0
  108. package/agents/components/navigation-bar/main.spec.json +109 -0
  109. package/agents/components/navigation-bar/navigation-bar.family.json +44 -0
  110. package/agents/components/navigation-bar/navigation-bar.md +21 -0
  111. package/agents/components/navigation-bar/search.md +96 -0
  112. package/agents/components/navigation-bar/search.spec.json +142 -0
  113. package/agents/components/navigation-bar/sub.md +174 -0
  114. package/agents/components/navigation-bar/sub.spec.json +123 -0
  115. package/agents/components/page-shell/page-shell.family.json +22 -0
  116. package/agents/components/page-shell/page-shell.md +51 -0
  117. package/agents/components/profile-header/profile-header.family.json +29 -0
  118. package/agents/components/profile-header/profile-header.md +149 -0
  119. package/agents/components/profile-header/profile-header.spec.json +200 -0
  120. package/agents/components/progress/progress.family.json +27 -0
  121. package/agents/components/progress/progress.md +38 -0
  122. package/agents/components/progress/progress.spec.json +67 -0
  123. package/agents/components/side-sheet/side-sheet.family.json +30 -0
  124. package/agents/components/side-sheet/side-sheet.md +154 -0
  125. package/agents/components/side-sheet/side-sheet.spec.json +109 -0
  126. package/agents/components/skeleton/skeleton.family.json +28 -0
  127. package/agents/components/skeleton/skeleton.md +123 -0
  128. package/agents/components/skeleton/skeleton.spec.json +73 -0
  129. package/agents/components/status-tag/status-tag.family.json +26 -0
  130. package/agents/components/status-tag/status-tag.md +114 -0
  131. package/agents/components/status-tag/status-tag.spec.json +69 -0
  132. package/agents/components/suggestion-list/suggestion-list.family.json +46 -0
  133. package/agents/components/suggestion-list/suggestion-list.md +91 -0
  134. package/agents/components/suggestion-list/suggestion-list.spec.json +178 -0
  135. package/agents/components/switch/switch.family.json +27 -0
  136. package/agents/components/switch/switch.md +114 -0
  137. package/agents/components/switch/switch.spec.json +123 -0
  138. package/agents/components/tab-bar/tab-bar.family.json +27 -0
  139. package/agents/components/tab-bar/tab-bar.md +178 -0
  140. package/agents/components/tab-bar/tab-bar.spec.json +184 -0
  141. package/agents/components/tabs/rounded.md +150 -0
  142. package/agents/components/tabs/rounded.spec.json +140 -0
  143. package/agents/components/tabs/segmented.md +114 -0
  144. package/agents/components/tabs/segmented.spec.json +100 -0
  145. package/agents/components/tabs/tabs.family.json +59 -0
  146. package/agents/components/tabs/tabs.md +18 -0
  147. package/agents/components/tabs/underline.md +147 -0
  148. package/agents/components/tabs/underline.spec.json +139 -0
  149. package/agents/components/thumbnail/thumbnail.family.json +28 -0
  150. package/agents/components/thumbnail/thumbnail.md +152 -0
  151. package/agents/components/thumbnail/thumbnail.spec.json +172 -0
  152. package/agents/components/toast/toast.family.json +28 -0
  153. package/agents/components/toast/toast.md +133 -0
  154. package/agents/components/toast/toast.spec.json +89 -0
  155. package/agents/components/tooltip/tooltip.family.json +29 -0
  156. package/agents/components/tooltip/tooltip.md +139 -0
  157. package/agents/components/tooltip/tooltip.spec.json +110 -0
  158. package/agents/compose.md +240 -0
  159. package/agents/icons.json +831 -0
  160. package/agents/images.md +66 -0
  161. package/agents/manifest.json +87 -0
  162. package/agents/patterns/README.md +59 -0
  163. package/agents/patterns/actions.md +50 -0
  164. package/agents/patterns/browsing.md +52 -0
  165. package/agents/patterns/communications.md +56 -0
  166. package/agents/patterns/layout.md +72 -0
  167. package/agents/patterns/modals.md +50 -0
  168. package/agents/patterns/visual.md +55 -0
  169. package/agents/reconstruct.md +55 -0
  170. package/agents/scoped-adoption.md +111 -0
  171. package/agents/tokens.usage.json +1657 -0
  172. package/agents/usage.json +422 -0
  173. package/dist/icons/index.cjs +1332 -0
  174. package/dist/icons/index.cjs.map +1 -0
  175. package/dist/icons/index.d.cts +228 -0
  176. package/dist/icons/index.d.ts +228 -0
  177. package/dist/icons/index.js +1114 -0
  178. package/dist/icons/index.js.map +1 -0
  179. package/dist/index.cjs +5905 -0
  180. package/dist/index.cjs.map +1 -0
  181. package/dist/index.d.cts +896 -0
  182. package/dist/index.d.ts +896 -0
  183. package/dist/index.js +5847 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/styles.css +5765 -0
  186. package/eslint/README.md +79 -0
  187. package/eslint/index.js +78 -0
  188. package/eslint/rules.js +472 -0
  189. package/eslint/test.mjs +135 -0
  190. package/package.json +96 -0
  191. package/placeholder.png +0 -0
@@ -0,0 +1,11 @@
1
+ # Form field
2
+
3
+ The text-entry primitives — controls the user types into or picks a value from. **Input** is single-line text; **Search Bar** is its search-shaped sibling; **Select** is the Input-shaped picker that opens a sheet. Cross-family contract: bordered surface-toned container, hairline rest stroke that thickens while active, `error` appearance re-tones the whole field.
4
+
5
+ **Layout inset.** `inline` — slot atom. No page-rail responsibility; the surrounding container places it. Sits inside a labelled form layout (vertical FormField stack) or another component's slot (NavigationBar centre for Search, BottomSheet body for quick-entry) — never a sibling of `full-bleed` page rows. The form column pays the page gutter, not the field.
6
+
7
+ ## Sub-components
8
+
9
+ - **[Input](./input.md)** — Single-line text input. Surface-toned box, hairline `outlineVariant` rest stroke, `onSurface` border while active, optional trailing clear ("×"). Corner radius `sys.radius.md`. `error` appearance re-tones to error family. Compose multiple via the **Group** use case.
10
+ - **[Search bar](./search.md)** — Same anatomy and state model as Input with three deltas: leading `SearchIcon` glyph, corner radius stepped to `sys.radius.full` (pill), bare-box-only (no `label`, `helper`, or `maxLength`).
11
+ - **[Select](./select.md)** — Input-shaped picker. Read-only field with a trailing `ArrowDownIcon` (16px) chevron; clicking opens a `BottomSheet` with the option list. Supports the same optional leading icon as Input, plus a horizontal **Group** use case (country code + number, currency + amount).
@@ -0,0 +1,198 @@
1
+ # Input
2
+
3
+ Single-line text field — a bordered, transparent-fill box for short values. Optional `label`, `helper`, or `maxLength` compose it into a labeled group; `appearance="error"` re-tones fill, text, and stroke.
4
+
5
+ **Reach for this when** capturing a short single-line value — name, email, search query, comment subject. **Skip when** the value is multi-line (use a textarea), a one-of-many selection (use a select), or a free-form search with built-in results (use the [search](./search.md) sub).
6
+
7
+ **Layout inset.** inline — content-sized inside its surface's padding. With `label` / `helper` / `maxLength` it wraps in a `.chorus-field-group` flex column.
8
+
9
+ ## Default
10
+
11
+ Transparent fill, hairline `outlineVariant` stroke, placeholder in faint `outline`. Type to see the lifecycle: placeholder → value, stroke steps up, trailing clear (*×*) appears.
12
+
13
+ ```preview
14
+ form-field/input/default
15
+ ---
16
+ import { FormField } from '@teamblind-chorus/ui';
17
+
18
+ <FormField variant="input" placeholder="Place holder" />
19
+ ```
20
+
21
+ ## Use cases
22
+
23
+ ### Error
24
+
25
+ `errorContainer` wash, full-strength `error` stroke, `onErrorContainer` text. Optional helper rung re-tones to `sys.color.error`.
26
+
27
+ ```preview
28
+ form-field/input/error
29
+ ---
30
+ import { FormField } from '@teamblind-chorus/ui';
31
+
32
+ <FormField
33
+ variant="input"
34
+ appearance="error"
35
+ label="Label text"
36
+ placeholder="Place holder"
37
+ helper="Assistive text"
38
+ />
39
+ ```
40
+
41
+ Omit `helper` when the surrounding row already carries the failure message — the box still re-tones.
42
+
43
+ ```preview
44
+ form-field/input/error-no-helper
45
+ ---
46
+ import { FormField } from '@teamblind-chorus/ui';
47
+
48
+ <FormField
49
+ variant="input"
50
+ appearance="error"
51
+ label="Label text"
52
+ placeholder="Place holder"
53
+ />
54
+ ```
55
+
56
+ ### Label, assistive text & Count
57
+
58
+ When any of `label` / `helper` / `maxLength` is set, the box wraps in a `.chorus-field-group` flex column at `sys.layout.stack.xs` between rungs — label above, helper left or count right below. Helper and count are mutually exclusive; pass both and count wins.
59
+
60
+ ```preview
61
+ form-field/input/with-label
62
+ ---
63
+ import { FormField } from '@teamblind-chorus/ui';
64
+
65
+ <FormField
66
+ variant="input"
67
+ label="Label text"
68
+ placeholder="Place holder"
69
+ helper="Assistive text"
70
+ />
71
+ ```
72
+
73
+ Same shape with a character count:
74
+
75
+ ```preview
76
+ form-field/input/with-count
77
+ ---
78
+ import { FormField } from '@teamblind-chorus/ui';
79
+
80
+ <FormField
81
+ variant="input"
82
+ label="Label text"
83
+ placeholder="Place holder"
84
+ defaultValue="Text"
85
+ maxLength={30}
86
+ />
87
+ ```
88
+
89
+ ### Leading icon
90
+
91
+ Optional `leadingIcon` (16px / `sys.icon.md`) pinned inner-left. Decorative (`aria-hidden`); tracks the field's active text colour. Also available on [`select`](./select.md).
92
+
93
+ ```preview
94
+ form-field/input/with-leading-icon
95
+ ---
96
+ import { FormField } from '@teamblind-chorus/ui';
97
+ import { LocationIcon } from '@teamblind-chorus/ui/icons';
98
+
99
+ <FormField
100
+ variant="input"
101
+ label="Location"
102
+ leadingIcon={<LocationIcon />}
103
+ placeholder="City, region"
104
+ helper="Used to suggest local channels"
105
+ />
106
+ ```
107
+
108
+ ### Group
109
+
110
+ Compose multiple Inputs into a column via `<FormFieldGroup>`. Each rung keeps its own label / helper / count; group inserts `sys.layout.stack.md` (16px) between rungs — sign-up and profile forms.
111
+
112
+ ```preview
113
+ form-field/input/group
114
+ ---
115
+ import { FormField, FormFieldGroup } from '@teamblind-chorus/ui';
116
+
117
+ <FormFieldGroup direction="vertical">
118
+ <FormField variant="input" label="Name" placeholder="Your name" />
119
+ <FormField variant="input" label="Email" placeholder="you@example.com" helper="We'll send a confirmation" />
120
+ <FormField variant="input" label="Bio" placeholder="One sentence about you" />
121
+ </FormFieldGroup>
122
+ ```
123
+
124
+ ### Focus indicator
125
+
126
+ Focus ring layered on top of the `active` border re-tone.
127
+
128
+ ```preview
129
+ form-field/input/focused
130
+ ---
131
+ import { FormField } from '@teamblind-chorus/ui';
132
+
133
+ <FormField
134
+ variant="input"
135
+ placeholder="Place holder"
136
+ state="focused"
137
+ />
138
+ ```
139
+
140
+ ## Slots
141
+
142
+ - **container** — the box. Owns transparent fill, the inset-`box-shadow` stroke, radius, padding, and focus ring.
143
+ - **input** — editable text. Single line; value-driven placeholder swap.
144
+ - **clear** — trailing *×* button (`XCircleFillIcon`). Shown only while the box is active and holds a non-empty value.
145
+ - **group** *(optional)* — wrapper holding label / box / helper / count when any is supplied.
146
+ - **label** *(optional)* — visible label above the box. `sys.typo.label.md` / `sys.color.onSurface`, bound via `htmlFor`.
147
+ - **helper** *(optional)* — assistive text below the box, left-aligned. `sys.typo.body.sm` / `sys.color.onSurfaceVariant`, referenced by `aria-describedby`; re-tones to `error` on the error appearance. Mutually exclusive with `count`.
148
+ - **count** *(optional)* — `current/max` count below the box, right-aligned. `sys.typo.body.sm` / `sys.color.onSurfaceVariant`; live digit steps to `label.md` weight. `aria-live="polite"`. Mutually exclusive with `helper`.
149
+
150
+ ## Appearance
151
+
152
+ | Appearance | Background | Border (rest) | Text | Placeholder |
153
+ |------------|-------------------------------|-----------------------------------------------------|-------------------------------|-------------------------------|
154
+ | `default` | `transparent` | `sys.color.outlineVariant` (`borderWidth.hairline`) | `sys.color.onSurface` | `sys.color.outline` |
155
+ | `error` | `sys.color.errorContainer` | `sys.color.error` (`borderWidth.hairline`) | `sys.color.onErrorContainer` | `sys.color.onErrorContainer` |
156
+
157
+ Placeholder vs. value is value-driven, not focus-driven.
158
+
159
+ ## Sizes
160
+
161
+ A single fixed footprint.
162
+
163
+ | Property | Value | Token |
164
+ |-----------------------------------|----------------------|-------------------------------------|
165
+ | Height | 40px ‡ | `ref.space.500` |
166
+ | Padding (block × inline) | 8 × 12 | `sys.layout.container.xs` × `sys.layout.container.sm` |
167
+ | Slot gap (input ↔ clear) | 8px | `sys.layout.inline.md` |
168
+ | Radius | 8px | `sys.radius.md` |
169
+ | Stroke (rest / hover) | 1px ⁋ | `sys.borderWidth.hairline` |
170
+ | Stroke (active) | 2px ⁋ | `sys.borderWidth.thin` |
171
+ | Text / placeholder | 16 / Regular | `sys.typo.body.md` |
172
+ | Clear icon | 16px | `sys.icon.md` |
173
+
174
+ ‡ Height is exactly `content + padding` (24px line-box + 2 × 8px). `min-height` binds raw `ref.space.500`.
175
+
176
+ ⁋ Stroke is an inset `box-shadow`, never a `border` — zero layout cost; footprint pixel-stable in every state. See [DESIGN.md → Border & Stroke](../../DESIGN.md#border-scale).
177
+
178
+ ## States
179
+
180
+ Four interactive states. Load-bearing: `active` — the field has the caret; stroke re-tones to `sys.color.onSurface` at 2px.
181
+
182
+ | State | Stroke (inset box-shadow) | Additional |
183
+ |------------|---------------------------------------------------------------------------|------------|
184
+ | `default` | 1px, `borderRest` (`outlineVariant` / `error`) | Caret hidden. |
185
+ | `hovered` | 1px, `sys.color.outline` (error: stays `error`) | `:hover`. |
186
+ | `pressed` | 1px, `sys.color.outline` + `text` overlay at `sys.state.pressed` | `:active`. |
187
+ | `active` | 2px, `sys.color.onSurface` (error: `error`) | Caret visible per the [system caret rule](../../DESIGN.md#caret). |
188
+ | `disabled` | 1px, `borderRest`; container at `sys.state.disabled` opacity | Fill steps to `surfaceContainerLow`; overlays / clear suppressed; `cursor: not-allowed`. |
189
+
190
+ ## Focus indicator
191
+
192
+ Standard outward ring on a `position: absolute` pseudo-element — never affects layout. Suppressed while `disabled`. Trigger: `:focus-visible`. See [Focus ring composition](../../DESIGN.md#focus-ring-composition).
193
+
194
+ ## Behavior
195
+
196
+ - **Clear button** shown only while active and holding a non-empty value. Click empties, fires `onClear`, returns focus. Real `<button>`, keyboard-reachable, `aria-label="Clear"`.
197
+ - **Placeholder vs. value** value-driven. Placeholder shows only while empty; never the field's only accessible name — pair with a visible label or `aria-label`.
198
+ - **Single line.** Long values scroll horizontally.
@@ -0,0 +1,202 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "FormField",
4
+ "family": "form-field",
5
+ "subcomponent": "input",
6
+ "exportAlias": "Input",
7
+ "description": "Single-line text field. Transparent-fill box (so it adopts whatever surface it sits on — a bottom sheet, a card, the page) whose visible stroke is an inset box-shadow, never a `border`, so it costs zero layout in every state — the field's height is exactly content + padding (a 24px line-box + 2 × 8px = 40px). Rest/hover stroke is 1px `hairline`, the active stroke steps to 2px `thin`; going active never moves the field's footprint or caret. The placeholder shows in a faint `placeholder` colour while the field is empty and the value in `text` colour once it isn't (value-driven). A trailing clear button appears only while the field is active and holds text. An optional `label` above and `helper` text or `maxLength` character count below (the two are mutually exclusive) wrap the box in a field group. The Chorus focus ring is a position:absolute overlay layer; an error appearance re-tones the whole container.",
8
+ "element": "input",
9
+ "props": {
10
+ "variant": {
11
+ "type": "literal",
12
+ "value": "input"
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
+ "description": "Visible label rendered above the field box and associated with it (`<label htmlFor>`)."
38
+ },
39
+ "helper": {
40
+ "type": "node",
41
+ "optional": true,
42
+ "description": "Assistive text rendered below the field box, left-aligned. **Optional on every appearance** — omit the prop to render the field without an assistive rung; the box and label keep their footprint. On the `error` appearance the helper re-tones to `sys.color.error` so the message reads as the error caption; an error field may still be shown without a helper, in which case only the field box re-tones. Mutually exclusive with `maxLength` — if both are given, the character count is shown and `helper` is ignored."
43
+ },
44
+ "maxLength": {
45
+ "type": "number",
46
+ "optional": true,
47
+ "description": "Caps the input length and renders a `current/max` character count below the field box, right-aligned. Mutually exclusive with `helper`."
48
+ },
49
+ "disabled": {
50
+ "type": "boolean",
51
+ "default": false
52
+ }
53
+ },
54
+ "slots": {
55
+ "group": {
56
+ "required": false,
57
+ "description": "Wrapper around the label + box + helper/count, present only when at least one of `label` / `helper` / `maxLength` is supplied. A `display: flex` column at `groupGap` between rungs.",
58
+ "intrinsic": true
59
+ },
60
+ "label": {
61
+ "required": false,
62
+ "description": "Visible label above the box. `sys.typo.label.md`, `sys.color.onSurface`. Associated with the input via `htmlFor`.",
63
+ "accepts": [
64
+ "text"
65
+ ]
66
+ },
67
+ "container": {
68
+ "required": true,
69
+ "description": "The box — owns the transparent fill, the stroke (an inset box-shadow, not a `border`, so it never affects layout), radius, padding, focus ring.",
70
+ "intrinsic": true
71
+ },
72
+ "input": {
73
+ "required": true,
74
+ "description": "The editable single-line text. Renders the value in the appearance's `text` colour when present, or the appearance's faint `placeholder` colour when empty — value-driven, not focus-driven. Carries `aria-describedby` pointing at the helper / count when present.",
75
+ "accepts": [
76
+ "text"
77
+ ]
78
+ },
79
+ "clear": {
80
+ "required": false,
81
+ "description": "Trailing '×' button (XCircleFillIcon) that wipes the value and returns focus to the input. Rendered into the DOM whenever the value is non-empty and the field isn't disabled, but **shown only while the box is focused** — so it appears only in the active state with text to clear; hidden on an empty / blurred / disabled field.",
82
+ "intrinsic": true
83
+ },
84
+ "helper": {
85
+ "required": false,
86
+ "description": "Assistive text below the box, left-aligned. `sys.typo.body.sm`, `sys.color.onSurfaceVariant`; on the `error` appearance the colour re-tones to `sys.color.error` so the message reads as the error caption. Referenced by the input's `aria-describedby`. Not rendered when a `maxLength` count is present, and intentionally omittable on every appearance (including `error`) — pass nothing and the field renders without an assistive rung.",
87
+ "accepts": [
88
+ "text"
89
+ ]
90
+ },
91
+ "count": {
92
+ "required": false,
93
+ "description": "`current/max` character count below the box, right-aligned, present when `maxLength` is set. `sys.typo.body.sm`, `sys.color.onSurfaceVariant`; the current-count number is `sys.typo.label.md` weight in `sys.color.onSurface`. Referenced by the input's `aria-describedby`; updates `aria-live=\"polite\"`.",
94
+ "accepts": [
95
+ "text"
96
+ ]
97
+ }
98
+ },
99
+ "sizing": {
100
+ "minHeight": "ref.space.500",
101
+ "paddingBlock": "sys.layout.container.xs",
102
+ "paddingInline": "sys.layout.container.sm",
103
+ "slotGap": "sys.layout.inline.md",
104
+ "radius": "sys.radius.md",
105
+ "borderWidth": "sys.borderWidth.hairline",
106
+ "activeStrokeWeight": "sys.borderWidth.thin",
107
+ "groupGap": "sys.layout.stack.xs",
108
+ "labelTypo": "sys.typo.label.md",
109
+ "helperTypo": "sys.typo.body.sm",
110
+ "countTypo": "sys.typo.body.sm",
111
+ "countCurrentTypo": "sys.typo.label.md",
112
+ "textTypo": "sys.typo.body.md",
113
+ "iconSize": "sys.icon.md"
114
+ },
115
+ "groupColors": {
116
+ "label": "sys.color.onSurface",
117
+ "helper": "sys.color.onSurfaceVariant",
118
+ "helperError": "sys.color.error",
119
+ "count": "sys.color.onSurfaceVariant",
120
+ "countCurrent": "sys.color.onSurface"
121
+ },
122
+ "appearances": {
123
+ "default": {
124
+ "background": "transparent",
125
+ "text": "sys.color.onSurface",
126
+ "placeholder": "sys.color.outline",
127
+ "borderRest": "sys.color.outlineVariant",
128
+ "borderHover": "sys.color.outline",
129
+ "borderActive": "sys.color.onSurface"
130
+ },
131
+ "error": {
132
+ "background": "sys.color.errorContainer",
133
+ "text": "sys.color.onErrorContainer",
134
+ "placeholder": "sys.color.onErrorContainer",
135
+ "borderRest": "sys.color.error",
136
+ "borderHover": "sys.color.error",
137
+ "borderActive": "sys.color.error"
138
+ }
139
+ },
140
+ "states": {
141
+ "default": {
142
+ "overlay": null,
143
+ "border": "borderRest"
144
+ },
145
+ "hovered": {
146
+ "overlay": null,
147
+ "border": "borderHover"
148
+ },
149
+ "pressed": {
150
+ "border": "borderHover",
151
+ "overlay": {
152
+ "color": "text",
153
+ "opacity": "sys.state.pressed"
154
+ }
155
+ },
156
+ "active": {
157
+ "overlay": null,
158
+ "border": "borderActive",
159
+ "strokeWeight": "activeStrokeWeight",
160
+ "caret": "visible",
161
+ "showsClearWhenValue": true,
162
+ "note": "The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` — but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
163
+ },
164
+ "disabled": {
165
+ "overlay": null,
166
+ "background": "sys.color.surfaceContainerLow",
167
+ "containerOpacity": "sys.state.disabled",
168
+ "suppressClear": true,
169
+ "suppressFocusRing": true,
170
+ "cursor": "not-allowed"
171
+ }
172
+ },
173
+ "focusIndicator": {
174
+ "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the field is in (most commonly `active` since focus implies the caret is in the box). The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
175
+ "composition": "outward",
176
+ "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
177
+ "overlay": {
178
+ "color": "label",
179
+ "opacity": "sys.state.focus"
180
+ },
181
+ "ring": {
182
+ "outerWidth": "sys.borderWidth.thin",
183
+ "outerColor": "sys.color.focus",
184
+ "insetWidth": "sys.borderWidth.hairline",
185
+ "insetColor": "sys.color.focusInset"
186
+ },
187
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
188
+ },
189
+ "accessibility": {
190
+ "labelling": "The visible label associates with the input via `<label htmlFor>`. When no visible label is composed, the consumer supplies an aria-label.",
191
+ "describedby": "The input's aria-describedby points at the helper id (when a helper is rendered) or the count id (when a maxLength count is rendered) — the two are mutually exclusive.",
192
+ "invalid": "On the `error` appearance the input carries aria-invalid='true' — color alone does not expose invalidity to assistive tech. aria-invalid clears when the appearance returns to default.",
193
+ "errorAnnouncement": "When the helper is carrying an error message (error appearance), its container is a polite live region (role='alert' / aria-live='polite') so the message is announced when it appears without yanking focus. The neutral (non-error) helper is not a live region.",
194
+ "count": "The character count updates aria-live='polite' (see the count slot)."
195
+ },
196
+ "forbidden": [
197
+ "raw <input> styled with Tailwind / inline color — the input is wrapped in the chorus-field chrome that owns the stroke",
198
+ "active-state stroke painted with sys.color.primary / a container tier — the active stroke is sys.color.onSurface (default appearance) or sys.color.error (error appearance), never primary or a container tier",
199
+ "stroke painted via `border:` — stroke is an inset box-shadow on the field",
200
+ "helper text rendered outside the helperText slot"
201
+ ]
202
+ }
@@ -0,0 +1,81 @@
1
+ # Search bar
2
+
3
+ Search-shaped single-line field — sibling of [Input](./input.md) with a leading `SearchIcon` and `sys.radius.full` pill corner. Box, stroke, placeholder rule, clear button, and focus ring inherited from Input unchanged. **Bare box only — no `label`, `helper`, `maxLength`, or `error` appearance.** Error reporting belongs to a labelled Input.
4
+
5
+ **Reach for this when** the rung is a query against an open set — directory search, post filter, command palette entry. **Skip when** the value is a labelled form field ([Input](./input.md)), the user picks from a known closed set ([Select](./select.md)), or the surface needs error reporting (a bare search rung has nowhere to host it — use a labelled [Input](./input.md)).
6
+
7
+ **Layout inset.** `inline` — ships no padding outside its own pill chrome. Sits inside a host slot (NavigationBar search row, filter sheet header, page-body search row) with the host paying surrounding 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 search bar — transparent fill, hairline `outlineVariant` stroke, `SearchIcon` left, placeholder in faint `outline` colour. Type into the specimen: placeholder → full-strength `onSurface` text, stroke steps to `active`, trailing clear ("×") appears at the right edge.
12
+
13
+ ```preview
14
+ form-field/search/default
15
+ ---
16
+ import { FormField } from '@teamblind-chorus/ui';
17
+
18
+ <FormField variant="search" placeholder="Search" />
19
+ ```
20
+
21
+ ## Use cases
22
+
23
+ ### Focus indicator
24
+
25
+ Focus ring layered on top of the `active` pill border. Same composition as [Input → Focus indicator](./input.md#focus-indicator).
26
+
27
+ ```preview
28
+ form-field/search/focused
29
+ ---
30
+ import { FormField } from '@teamblind-chorus/ui';
31
+
32
+ <FormField
33
+ variant="search"
34
+ placeholder="Search"
35
+ state="focused"
36
+ />
37
+ ```
38
+
39
+ ## Appearance
40
+
41
+ Single `default` appearance — Search Bar does **not** carry an `error` form. Error reporting belongs to a labelled [Input](./input.md#appearance), which can pair the re-tone with assistive text; a bare search rung has nowhere to host that message.
42
+
43
+ ## Slots
44
+
45
+ - **container** — pill box. Same as Input's, radius stepped to `sys.radius.full`.
46
+ - **leading** — `SearchIcon` at `sys.icon.md` (16px), pinned inner-left with an 8px gap. Inherits field text colour; decorative (`aria-hidden`).
47
+ - **input** — same as Input. Pair with `aria-label` at the call site.
48
+ - **clear** — same as Input. Trailing "×" shown only while active and holding a value.
49
+
50
+ ## Sizes
51
+
52
+ Same fixed footprint as Input — 40px tall — radius stepped to full. Inline column reads `[icon 16] [text] [clear 16]`.
53
+
54
+ | Property | Value | Token |
55
+ |-----------------------------------|----------------------|-------------------------------------|
56
+ | Height | 40px | `ref.space.500` |
57
+ | Padding (block × inline) | 8 × 12 | `sys.layout.container.xs` × `sys.layout.container.sm` |
58
+ | Slot gap (leading ↔ input ↔ clear)| 8px | `sys.layout.inline.md` |
59
+ | Radius | full (pill) | `sys.radius.full` |
60
+ | Leading icon | 16px | `sys.icon.md` |
61
+ | Clear icon | 16px | `sys.icon.md` |
62
+ | Stroke (rest / hover) | 1px | `sys.borderWidth.hairline` |
63
+ | Stroke (active) | 2px | `sys.borderWidth.thin` |
64
+ | Text / placeholder | 16 / Regular | `sys.typo.body.md` |
65
+
66
+ Stroke is an inset `box-shadow`, focus ring is a `position: absolute` overlay — footprint pixel-stable in every state. See [Input → Sizes](./input.md#sizes).
67
+
68
+ ## States
69
+
70
+ Identical to Input — see [Input → States](./input.md#states). The leading glyph follows the field's text colour.
71
+
72
+ ## Focus indicator
73
+
74
+ Composition: Outward (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)). On a `position: absolute` `::after` overlay, sits above the `active` border. Trigger: `:focus-visible`.
75
+
76
+ ## Behavior
77
+
78
+ - **Search action.** Glyph is decorative; search fires from the input via `onChange` or `Enter`.
79
+ - **Clear button.** Same as Input — see [Input → Behavior](./input.md#behavior).
80
+ - **Placeholder vs. value.** Same value-driven swap; pair with `aria-label`.
81
+ - **Single line.** Long values scroll horizontally.
@@ -0,0 +1,135 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "FormField",
4
+ "family": "form-field",
5
+ "subcomponent": "search",
6
+ "exportAlias": "SearchBar",
7
+ "description": "Single-line search field — a search-shaped sibling of Input. Same transparent-fill box, same hairline-at-rest / thin-at-active inset box-shadow stroke (so the box model is untouched in every state), same placeholder-vs-value colour rules, same focus ring, same trailing clear button. Two anatomy differences from Input — a leading `SearchIcon` glyph pinned at the box's inner-left edge so the field reads as a search affordance before the user types, and the corner radius steps from `sys.radius.md` to `sys.radius.full` (pill). **Search Bar is a bare box only — it does not accept a `label`, `helper`, or `maxLength`**, and so never renders the field-group wrapper; it also carries no `error` appearance — error reporting belongs to a labelled field. A search rung sits in chrome (a header bar, a list filter, a sheet's top) where the affordance is carried by the leading glyph and the placeholder; stacking a visible label or a `current/max` count on top of that competes with the search affordance instead of reinforcing it. Reach for [Input](./input.md) when the rung needs a labelled / counted form field or an error treatment.",
8
+ "element": "input",
9
+ "props": {
10
+ "variant": {
11
+ "type": "literal",
12
+ "value": "search"
13
+ },
14
+ "appearance": {
15
+ "type": "literal",
16
+ "value": "default",
17
+ "description": "Search Bar carries only the `default` appearance — there is no `error` form (error reporting belongs to a labelled Input)."
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
+ "disabled": {
32
+ "type": "boolean",
33
+ "default": false
34
+ }
35
+ },
36
+ "slots": {
37
+ "container": {
38
+ "required": true,
39
+ "description": "The pill box — owns the transparent fill, the stroke (an inset box-shadow, not a `border`, so it never affects layout), full radius, padding, focus ring.",
40
+ "intrinsic": true
41
+ },
42
+ "leading": {
43
+ "required": true,
44
+ "description": "The leading `SearchIcon` glyph pinned at the box's inner-left edge. Inherits the field's text colour (`sys.color.onSurface`); decorative — not a real button, has `aria-hidden`. 16px (`sys.icon.md`), matching the clear button's footprint so the two affixes balance.",
45
+ "intrinsic": true
46
+ },
47
+ "input": {
48
+ "required": true,
49
+ "description": "The editable single-line text. Renders the value in the appearance's `text` colour when present, or the appearance's faint `placeholder` colour when empty — value-driven, not focus-driven.",
50
+ "accepts": [
51
+ "text"
52
+ ]
53
+ },
54
+ "clear": {
55
+ "required": false,
56
+ "description": "Trailing '×' button (XCircleFillIcon) that wipes the value and returns focus to the input. Rendered into the DOM whenever the value is non-empty and the field isn't disabled, but **shown only while the box is focused** — so it appears only in the active state with text to clear; hidden on an empty / blurred / disabled field.",
57
+ "intrinsic": true
58
+ }
59
+ },
60
+ "sizing": {
61
+ "minHeight": "ref.space.500",
62
+ "paddingBlock": "sys.layout.container.xs",
63
+ "paddingInline": "sys.layout.container.sm",
64
+ "slotGap": "sys.layout.inline.md",
65
+ "radius": "sys.radius.full",
66
+ "borderWidth": "sys.borderWidth.hairline",
67
+ "activeStrokeWeight": "sys.borderWidth.thin",
68
+ "textTypo": "sys.typo.body.md",
69
+ "iconSize": "sys.icon.md"
70
+ },
71
+ "appearances": {
72
+ "default": {
73
+ "background": "transparent",
74
+ "text": "sys.color.onSurface",
75
+ "placeholder": "sys.color.outline",
76
+ "borderRest": "sys.color.outlineVariant",
77
+ "borderHover": "sys.color.outline",
78
+ "borderActive": "sys.color.onSurface"
79
+ }
80
+ },
81
+ "states": {
82
+ "default": {
83
+ "overlay": null,
84
+ "border": "borderRest"
85
+ },
86
+ "hovered": {
87
+ "overlay": null,
88
+ "border": "borderHover"
89
+ },
90
+ "pressed": {
91
+ "border": "borderHover",
92
+ "overlay": {
93
+ "color": "text",
94
+ "opacity": "sys.state.pressed"
95
+ }
96
+ },
97
+ "active": {
98
+ "overlay": null,
99
+ "border": "borderActive",
100
+ "strokeWeight": "activeStrokeWeight",
101
+ "caret": "visible",
102
+ "showsClearWhenValue": true,
103
+ "note": "The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` — but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
104
+ },
105
+ "disabled": {
106
+ "overlay": null,
107
+ "background": "sys.color.surfaceContainerLow",
108
+ "containerOpacity": "sys.state.disabled",
109
+ "suppressClear": true,
110
+ "suppressFocusRing": true,
111
+ "cursor": "not-allowed"
112
+ }
113
+ },
114
+ "focusIndicator": {
115
+ "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the field is in. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
116
+ "composition": "outward",
117
+ "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
118
+ "overlay": {
119
+ "color": "label",
120
+ "opacity": "sys.state.focus"
121
+ },
122
+ "ring": {
123
+ "outerWidth": "sys.borderWidth.thin",
124
+ "outerColor": "sys.color.focus",
125
+ "insetWidth": "sys.borderWidth.hairline",
126
+ "insetColor": "sys.color.focusInset"
127
+ },
128
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
129
+ },
130
+ "forbidden": [
131
+ "leading search glyph as a separate hit area — the entire field is the click / focus target",
132
+ "clear glyph rendered when value is empty — the clear affordance only appears with content",
133
+ "search field stacked under a navigation-bar/search — the bar variant already embeds the search input"
134
+ ]
135
+ }