@you-agent-factory/components 0.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 (92) hide show
  1. package/README.md +223 -0
  2. package/docs/README.md +83 -0
  3. package/docs/button.md +119 -0
  4. package/docs/charts.md +325 -0
  5. package/docs/data-display-code-panel.md +120 -0
  6. package/docs/feedback-alert-panel.md +117 -0
  7. package/docs/feedback-skeleton.md +54 -0
  8. package/docs/forms-form-field.md +490 -0
  9. package/docs/forms-input-primitives.md +311 -0
  10. package/docs/forms-select-primitives.md +362 -0
  11. package/docs/graphs.md +250 -0
  12. package/docs/layout-display-primitives.md +211 -0
  13. package/docs/overlays.md +414 -0
  14. package/docs/table-data-table.md +331 -0
  15. package/docs/typography-roles.md +182 -0
  16. package/docs/widget-frame-recipes.md +273 -0
  17. package/package.json +161 -0
  18. package/src/category-paths.ts +20 -0
  19. package/src/charts/chart-state-panel.tsx +103 -0
  20. package/src/charts/chart.tsx +287 -0
  21. package/src/charts/index.ts +21 -0
  22. package/src/data-display/code-panel.tsx +67 -0
  23. package/src/data-display/data-table.tsx +180 -0
  24. package/src/data-display/description-list.tsx +24 -0
  25. package/src/data-display/index.ts +34 -0
  26. package/src/data-display/table-layout.ts +12 -0
  27. package/src/data-display/table.tsx +131 -0
  28. package/src/feedback/alert-panel-semantics.ts +75 -0
  29. package/src/feedback/alert-panel.tsx +187 -0
  30. package/src/feedback/index.ts +21 -0
  31. package/src/feedback/skeleton.tsx +16 -0
  32. package/src/forms/index.ts +73 -0
  33. package/src/forms/package-checkbox.tsx +50 -0
  34. package/src/forms/package-enum-select.tsx +185 -0
  35. package/src/forms/package-file-input.tsx +25 -0
  36. package/src/forms/package-form-field.tsx +202 -0
  37. package/src/forms/package-input.tsx +25 -0
  38. package/src/forms/package-native-select.tsx +25 -0
  39. package/src/forms/package-select.tsx +209 -0
  40. package/src/forms/package-textarea.tsx +35 -0
  41. package/src/forms/select-icons.tsx +35 -0
  42. package/src/graphs/graph-edge-path.ts +241 -0
  43. package/src/graphs/graph-edge.tsx +130 -0
  44. package/src/graphs/graph-interactive-example.tsx +187 -0
  45. package/src/graphs/graph-node-button.tsx +51 -0
  46. package/src/graphs/graph-node-handle-badge.tsx +108 -0
  47. package/src/graphs/graph-node-handle.ts +28 -0
  48. package/src/graphs/graph-node-shell.tsx +111 -0
  49. package/src/graphs/graph-node-state-indicator.tsx +47 -0
  50. package/src/graphs/graph-node-state.ts +111 -0
  51. package/src/graphs/graph-viewport-surface.tsx +27 -0
  52. package/src/graphs/index.ts +41 -0
  53. package/src/icons/index.ts +4 -0
  54. package/src/index.ts +209 -0
  55. package/src/layout/action-row.tsx +53 -0
  56. package/src/layout/index.ts +9 -0
  57. package/src/layout/surface-panel.tsx +91 -0
  58. package/src/navigation/index.ts +4 -0
  59. package/src/overlays/collapsible.tsx +22 -0
  60. package/src/overlays/dialog.tsx +148 -0
  61. package/src/overlays/index.ts +31 -0
  62. package/src/overlays/overlay-layout.ts +9 -0
  63. package/src/overlays/popover.tsx +29 -0
  64. package/src/overlays/scroll-area.tsx +78 -0
  65. package/src/primitives/button-link.tsx +22 -0
  66. package/src/primitives/button.tsx +258 -0
  67. package/src/primitives/icon-button-shell.tsx +49 -0
  68. package/src/primitives/index.ts +31 -0
  69. package/src/primitives/package-text.tsx +29 -0
  70. package/src/primitives/typography-roles.ts +12 -0
  71. package/src/primitives/typography.tsx +165 -0
  72. package/src/recipes/index.ts +61 -0
  73. package/src/recipes/widget-frame-content.tsx +114 -0
  74. package/src/recipes/widget-frame-disclosure.tsx +161 -0
  75. package/src/recipes/widget-frame-layout.ts +40 -0
  76. package/src/recipes/widget-frame-skeleton.tsx +16 -0
  77. package/src/recipes/widget-frame-states.tsx +99 -0
  78. package/src/recipes/widget-frame-typography.ts +19 -0
  79. package/src/recipes/widget-frame.tsx +84 -0
  80. package/src/styles/color-palette-presets.css +167 -0
  81. package/src/styles/color-role-tokens.css +163 -0
  82. package/src/styles/layout-role-tokens.css +27 -0
  83. package/src/styles/text-color-role-tokens.css +29 -0
  84. package/src/styles/typography-role-tokens.css +72 -0
  85. package/src/styles/typography-role-utilities.css +73 -0
  86. package/src/styles/typography-roles.css +53 -0
  87. package/src/styles.css +32 -0
  88. package/src/testing/index.ts +13 -0
  89. package/src/testing/render.tsx +16 -0
  90. package/src/tokens/index.ts +4 -0
  91. package/src/utilities/cn.ts +5 -0
  92. package/src/utilities/index.ts +6 -0
@@ -0,0 +1,311 @@
1
+ # Form input primitives
2
+
3
+ Domain-free text input, textarea, checkbox, and file input controls for reusable
4
+ forms. Use these primitives when you need accessible native form semantics with
5
+ package styling, without importing dashboard features, API clients, or app state.
6
+
7
+ These components are **presentation controls**. They render styled native inputs
8
+ and forward standard React form props. They do not fetch data, perform validation,
9
+ upload files, or own durable application state.
10
+
11
+ ## Required setup
12
+
13
+ Import package styles once in your host application before rendering primitives:
14
+
15
+ ```css
16
+ @import "@you-agent-factory/components/styles.css";
17
+ ```
18
+
19
+ Primitives depend on package role tokens and Tailwind utility classes compiled
20
+ from those tokens. They do not require dashboard `styles.css`, dashboard providers,
21
+ generated OpenAPI types, React Query, Zustand, or app localization context.
22
+
23
+ ## Import paths
24
+
25
+ Prefer the forms category entrypoint for tree-shaking and explicit category
26
+ boundaries:
27
+
28
+ ```ts
29
+ import {
30
+ PackageCheckbox,
31
+ PackageFileInput,
32
+ PackageInput,
33
+ PackageTextarea,
34
+ inputVariants,
35
+ textareaVariants,
36
+ } from "@you-agent-factory/components/forms";
37
+ ```
38
+
39
+ The same exports are also available from the package root:
40
+
41
+ ```ts
42
+ import { PackageInput } from "@you-agent-factory/components";
43
+ ```
44
+
45
+ ## Host ownership
46
+
47
+ The package owns presentation markup, focus rings, disabled styling, and native
48
+ control semantics. Host applications own everything domain-specific:
49
+
50
+ | Concern | Owner |
51
+ | --- | --- |
52
+ | Visible labels and placeholder copy | Host |
53
+ | Helper text and validation error messages | Host |
54
+ | `id`, `name`, `required`, and `disabled` values | Host |
55
+ | `aria-describedby`, `aria-invalid`, and `aria-label` wiring | Host |
56
+ | Controlled value or checked state | Host (when using controlled mode) |
57
+ | Form submission, validation rules, and error mapping | Host |
58
+ | File upload, persistence, and storage side effects | Host |
59
+ | Selected-file display copy (for example helper text) | Host |
60
+ | Data fetching, caching, and durable cross-route state | Host |
61
+
62
+ Pass values, handlers, and accessibility attributes into primitives as props.
63
+ Do not expect primitives to read from your routers, stores, or API layers.
64
+
65
+ ## Controlled versus uncontrolled values
66
+
67
+ All four primitives forward native React form props. Choose the mode that matches
68
+ how your host form manages state.
69
+
70
+ ### Text input (`PackageInput`)
71
+
72
+ | Mode | When to use | Key props |
73
+ | --- | --- | --- |
74
+ | Controlled | Host state must mirror every keystroke (live validation, dependent fields, reset) | `value` + `onChange` |
75
+ | Uncontrolled | Native form submission or refs are enough; host does not need per-keystroke state | `defaultValue` + optional `ref` |
76
+
77
+ ```tsx
78
+ import { useState } from "react";
79
+ import { PackageInput } from "@you-agent-factory/components/forms";
80
+
81
+ export function ControlledNameField() {
82
+ const [name, setName] = useState("");
83
+
84
+ return (
85
+ <PackageInput
86
+ aria-label="Display name"
87
+ onChange={(event) => {
88
+ setName(event.target.value);
89
+ }}
90
+ value={name}
91
+ />
92
+ );
93
+ }
94
+
95
+ export function UncontrolledNameField() {
96
+ return (
97
+ <PackageInput
98
+ aria-label="Display name"
99
+ defaultValue="Initial name"
100
+ name="displayName"
101
+ />
102
+ );
103
+ }
104
+ ```
105
+
106
+ Do not pass both `value` and `defaultValue` on the same control.
107
+
108
+ ### Textarea (`PackageTextarea`)
109
+
110
+ Same controlled/uncontrolled rules as text input. `PackageTextarea` also accepts
111
+ `variant`:
112
+
113
+ - `field` (default) — bordered field shell with scrollable body (`textareaVariants`).
114
+ - `plain` — borderless transparent textarea for embedding inside custom shells.
115
+
116
+ ### Checkbox (`PackageCheckbox`)
117
+
118
+ | Mode | When to use | Key props |
119
+ | --- | --- | --- |
120
+ | Controlled | Host must react to every toggle (conditional sections, select-all logic) | `checked` + `onChange` |
121
+ | Uncontrolled | Checkbox state can live in the native form until submit | `defaultChecked` + optional `ref` |
122
+
123
+ The visible check indicator is decorative (`aria-hidden`). Assistive technology
124
+ reads the native `input type="checkbox"` inside the component. Always provide an
125
+ accessible name with a visible `<label htmlFor={id}>`, `aria-label`, or
126
+ `aria-labelledby`.
127
+
128
+ ### File input (`PackageFileInput`)
129
+
130
+ | Mode | When to use | Key props |
131
+ | --- | --- | --- |
132
+ | Controlled selection tracking | Host must show selected-file feedback or gate submit buttons | Read `event.target.files` in `onChange`; optionally clear with `value=""` on re-render |
133
+ | Uncontrolled | File selection is handled entirely by native form submit | `name` on the input; read files from `FormData` on submit |
134
+
135
+ The primitive opens the native file picker and exposes selected `File` objects
136
+ through standard `onChange` events. It does **not** upload, persist, or validate
137
+ file contents. Display selected file names in host-owned helper text or adjacent
138
+ copy when your UX requires feedback.
139
+
140
+ ```tsx
141
+ import { useState } from "react";
142
+ import { PackageFileInput } from "@you-agent-factory/components/forms";
143
+
144
+ export function CoverImageField() {
145
+ const [selectedName, setSelectedName] = useState<string | undefined>();
146
+
147
+ return (
148
+ <>
149
+ <PackageFileInput
150
+ accept="image/png,image/jpeg"
151
+ aria-describedby="cover-image-helper"
152
+ id="cover-image"
153
+ onChange={(event) => {
154
+ setSelectedName(event.target.files?.[0]?.name);
155
+ }}
156
+ />
157
+ <p id="cover-image-helper">
158
+ {selectedName ? `Selected: ${selectedName}` : "PNG or JPEG up to 2 MB."}
159
+ </p>
160
+ </>
161
+ );
162
+ }
163
+ ```
164
+
165
+ ## Labels, descriptions, and error relationships
166
+
167
+ Primitives render the control only. Host applications wire labels and supporting
168
+ text:
169
+
170
+ 1. Generate stable `id` values (for example `useId()`).
171
+ 2. Associate visible labels with `<label htmlFor={controlId}>`.
172
+ 3. Render helper text in an element with its own `id`.
173
+ 4. Render error text in an element with `role="alert"` and its own `id`.
174
+ 5. Pass `aria-describedby` listing helper and/or error element ids.
175
+ 6. Pass `aria-invalid={true}` when the field fails validation.
176
+
177
+ ```tsx
178
+ import { useId } from "react";
179
+ import { PackageInput } from "@you-agent-factory/components/forms";
180
+
181
+ export function NameField({
182
+ errorText,
183
+ helperText,
184
+ invalid,
185
+ value,
186
+ onChange,
187
+ }: {
188
+ errorText?: string;
189
+ helperText?: string;
190
+ invalid?: boolean;
191
+ value: string;
192
+ onChange: (value: string) => void;
193
+ }) {
194
+ const reactId = useId();
195
+ const controlId = `${reactId}-control`;
196
+ const helperId = helperText ? `${reactId}-helper` : undefined;
197
+ const errorId = errorText ? `${reactId}-error` : undefined;
198
+ const describedBy =
199
+ [helperId, errorId].filter(Boolean).join(" ") || undefined;
200
+
201
+ return (
202
+ <div>
203
+ <label htmlFor={controlId}>Display name</label>
204
+ <PackageInput
205
+ aria-describedby={describedBy}
206
+ aria-invalid={invalid || Boolean(errorText) || undefined}
207
+ id={controlId}
208
+ onChange={(event) => {
209
+ onChange(event.target.value);
210
+ }}
211
+ value={value}
212
+ />
213
+ {helperText ? <p id={helperId}>{helperText}</p> : null}
214
+ {errorText ? (
215
+ <p id={errorId} role="alert">
216
+ {errorText}
217
+ </p>
218
+ ) : null}
219
+ </div>
220
+ );
221
+ }
222
+ ```
223
+
224
+ Package Storybook uses `PackageFormStoryField` for the same wiring pattern in
225
+ demos. That helper is story support only; host applications should compose
226
+ labels and messages in their own form-field layer.
227
+
228
+ ## Required, disabled, and invalid state
229
+
230
+ | State | Host responsibility | Primitive behavior |
231
+ | --- | --- | --- |
232
+ | `required` | Set `required` when the field must be submitted | Native constraint validation semantics |
233
+ | `disabled` | Set `disabled` to block interaction | Pointer and keyboard changes are ignored; disabled styling and semantics apply |
234
+ | Invalid | Set `aria-invalid={true}` when validation fails | Danger border and focus ring via `aria-invalid` styles |
235
+ | Error copy | Render error text and link with `aria-describedby` | Does not invent error messages |
236
+
237
+ Disabled file inputs ignore `onChange` and keep an empty `files` list until
238
+ re-enabled.
239
+
240
+ ## Accessibility expectations
241
+
242
+ - Text input, textarea, and file input expose visible `focus-visible` rings for
243
+ keyboard users.
244
+ - Checkbox keyboard focus targets the native input; the styled indicator shows a
245
+ focus ring via `peer-focus-visible` styles.
246
+ - Invalid states use `aria-invalid` styling on the native control (checkbox uses
247
+ `peer-aria-invalid` on the indicator).
248
+ - Host-supplied header actions, labels, and error text must use accessible names
249
+ and programmatic relationships (`htmlFor`, `aria-describedby`, `role="alert"`).
250
+ - Do not rely on placeholder text as the only accessible name.
251
+
252
+ ## Variant helpers
253
+
254
+ `inputVariants` and `textareaVariants` export the shared field class strings for
255
+ host compositions that need matching shells around sibling elements:
256
+
257
+ ```ts
258
+ import { inputVariants, textareaVariants } from "@you-agent-factory/components/forms";
259
+ ```
260
+
261
+ ## Storybook visual reference
262
+
263
+ Package Storybook lives under `Forms/PackageInput`, `Forms/PackageTextarea`,
264
+ `Forms/PackageCheckbox`, and `Forms/PackageFileInput`. Stories use package
265
+ imports and package token decorators only — no dashboard providers.
266
+
267
+ | Primitive | Example stories |
268
+ | --- | --- |
269
+ | `PackageInput` | Controlled, Uncontrolled, Disabled, Invalid, ErrorState, Focus, HelperText, MobileWidth |
270
+ | `PackageTextarea` | Controlled, Uncontrolled, Disabled, Invalid, ErrorState, Focus, HelperText, MobileWidth |
271
+ | `PackageCheckbox` | Controlled, Uncontrolled, Disabled, Invalid, ErrorState, Focus, HelperText, MobileWidth |
272
+ | `PackageFileInput` | Default, Disabled, Invalid, ErrorState, Focus, HelperText, SelectedFile, MobileWidth |
273
+
274
+ Run package Storybook locally:
275
+
276
+ ```bash
277
+ cd ui/packages/components
278
+ bun run storybook
279
+ ```
280
+
281
+ Browser verification for responsive and focus stories:
282
+
283
+ ```bash
284
+ cd ui/packages/components
285
+ bun run verify:storybook-browser
286
+ ```
287
+
288
+ ## Allowed dependencies
289
+
290
+ Primitive source may import:
291
+
292
+ - Package utilities (`cn` from `@you-agent-factory/components/utilities`)
293
+ - Package token CSS via the host `styles.css` import
294
+ - React and `react-dom` peer dependencies
295
+
296
+ Primitive source must **not** import:
297
+
298
+ - Dashboard feature modules, routes, or providers
299
+ - Generated OpenAPI clients or dashboard API adapters
300
+ - React Query, Zustand, Monaco, Sonner, or dashboard i18n/session providers
301
+ - Factory, work, session, or provider domain types
302
+
303
+ `check:package-boundary` enforces these rules in CI.
304
+
305
+ ## Dashboard integration note
306
+
307
+ The dashboard re-exports these primitives from `components/ui` (`Input`,
308
+ `Textarea`, `Checkbox`, `FileInput`) so existing feature imports keep working
309
+ while resolving to package implementations. New host applications should import
310
+ directly from `@you-agent-factory/components` or
311
+ `@you-agent-factory/components/forms`.
@@ -0,0 +1,362 @@
1
+ # Form select primitives
2
+
3
+ Domain-free select controls for reusable forms. Use these primitives when you need
4
+ accessible combobox or native `<select>` semantics with package styling, without
5
+ importing dashboard features, API clients, generated OpenAPI types, or app
6
+ localization providers.
7
+
8
+ The package ships three layers:
9
+
10
+ | Component | Role |
11
+ | --- | --- |
12
+ | `Select` primitives | Radix-backed combobox building blocks (`Select`, `SelectTrigger`, `SelectContent`, `SelectItem`, and helpers). |
13
+ | `NativeSelect` | Styled native `<select>` for simple option lists and progressive enhancement. |
14
+ | `EnumSelect` helpers | Opinionated wrappers that map host-supplied option arrays into `Select` markup. |
15
+
16
+ These components are **presentation controls**. They render styled select UI and
17
+ forward standard React props. They do not fetch options, perform validation,
18
+ localize labels, or own durable application state.
19
+
20
+ ## Required setup
21
+
22
+ Import package styles once in your host application before rendering selects:
23
+
24
+ ```css
25
+ @import "@you-agent-factory/components/styles.css";
26
+ ```
27
+
28
+ Select primitives depend on package role tokens and Tailwind utility classes
29
+ compiled from those tokens. They do not require dashboard `styles.css`, dashboard
30
+ providers, generated OpenAPI types, React Query, Zustand, or app localization
31
+ context.
32
+
33
+ ## Import paths
34
+
35
+ Prefer the forms category entrypoint for tree-shaking and explicit category
36
+ boundaries:
37
+
38
+ ```ts
39
+ import {
40
+ EnumSelect,
41
+ ENUM_SELECT_EMPTY_VALUE,
42
+ NativeSelect,
43
+ OptionalEnumSelect,
44
+ ResetEnumSelect,
45
+ Select,
46
+ SELECT_EMPTY_STATE_VALUE,
47
+ SelectContent,
48
+ SelectEmpty,
49
+ SelectField,
50
+ SelectItem,
51
+ SelectTrigger,
52
+ SelectValue,
53
+ type EnumSelectOption,
54
+ } from "@you-agent-factory/components/forms";
55
+ ```
56
+
57
+ The same exports are also available from the package root:
58
+
59
+ ```ts
60
+ import {
61
+ EnumSelect,
62
+ NativeSelect,
63
+ Select,
64
+ } from "@you-agent-factory/components";
65
+ ```
66
+
67
+ ## Host ownership
68
+
69
+ The package owns presentation markup, focus rings, disabled styling, keyboard
70
+ interaction, and combobox semantics. Host applications own everything
71
+ domain-specific:
72
+
73
+ | Concern | Owner |
74
+ | --- | --- |
75
+ | Option values and visible labels | Host |
76
+ | Placeholder, empty, and loading copy | Host |
77
+ | Visible labels, helper text, and validation errors | Host |
78
+ | `id`, `disabled`, and `aria-*` wiring | Host |
79
+ | Controlled `value` and `open` state | Host (when using controlled mode) |
80
+ | `onValueChange` and `onOpenChange` handlers | Host |
81
+ | Fetching, caching, and refreshing option lists | Host |
82
+ | Localization and enum label mapping | Host |
83
+ | Form submission, validation rules, and error mapping | Host |
84
+
85
+ Pass data and callbacks into select components as props. Do not expect the
86
+ package to read from your routers, stores, or API layers.
87
+
88
+ ## Option contract
89
+
90
+ ### `EnumSelectOption`
91
+
92
+ `EnumSelect`, `OptionalEnumSelect`, and `ResetEnumSelect` accept a readonly
93
+ array of domain-free options:
94
+
95
+ ```ts
96
+ type EnumSelectOption = {
97
+ value: string;
98
+ label: string;
99
+ disabled?: boolean;
100
+ };
101
+ ```
102
+
103
+ | Field | Contract |
104
+ | --- | --- |
105
+ | `value` | Stable string identifier used in `onValueChange`. Must be unique within the option list. |
106
+ | `label` | Host-supplied visible text. The package does not translate or format enum values. |
107
+ | `disabled` | When `true`, the option is not selectable by pointer or keyboard and exposes disabled semantics to assistive technology. |
108
+
109
+ ### Empty option lists
110
+
111
+ When `options` is empty and `loading` is `false`, `EnumSelect` renders a
112
+ non-selectable `SelectEmpty` row using `emptyOptionsLabel` (default:
113
+ `"No options available"`). The row is a disabled menu item backed by
114
+ `SELECT_EMPTY_STATE_VALUE`, so users cannot accidentally select a fake value.
115
+
116
+ For custom Radix compositions, render `SelectEmpty` inside `SelectContent`
117
+ instead of mapping interactive `SelectItem` rows over an empty array.
118
+
119
+ ### Optional and reset helpers
120
+
121
+ | Helper | Use when |
122
+ | --- | --- |
123
+ | `OptionalEnumSelect` | The host needs a nullable selection. Uses `ENUM_SELECT_EMPTY_VALUE` as a dedicated empty row and maps it back to `null` in `onValueChange`. |
124
+ | `ResetEnumSelect` | The host needs a select that always returns to the placeholder after each selection (for example one-shot filters). Remounts internally after each change. |
125
+
126
+ ## Controlled value and open state
127
+
128
+ `Select` forwards Radix `Select.Root` props. Choose controlled or uncontrolled
129
+ mode based on how your host form manages state.
130
+
131
+ ### Controlled value
132
+
133
+ Use controlled value when host state must mirror the current selection (dependent
134
+ fields, validation, reset, or async reconciliation):
135
+
136
+ ```tsx
137
+ import { useState } from "react";
138
+ import {
139
+ EnumSelect,
140
+ type EnumSelectOption,
141
+ } from "@you-agent-factory/components/forms";
142
+
143
+ const options: EnumSelectOption[] = [
144
+ { label: "Story", value: "story" },
145
+ { label: "Task", value: "task" },
146
+ ];
147
+
148
+ function WorkTypeField() {
149
+ const [value, setValue] = useState("story");
150
+
151
+ return (
152
+ <EnumSelect
153
+ id="work-type"
154
+ onValueChange={setValue}
155
+ options={options}
156
+ placeholder="Select a work type"
157
+ value={value}
158
+ />
159
+ );
160
+ }
161
+ ```
162
+
163
+ Host responsibilities for controlled value:
164
+
165
+ - Provide the current `value` on every render.
166
+ - Update host state inside `onValueChange` when the user selects a different
167
+ **enabled** option.
168
+ - Expect `onValueChange` not to fire when the user re-selects the current value
169
+ or activates a disabled option.
170
+
171
+ ### Controlled open
172
+
173
+ Use controlled open when the host must coordinate menu visibility with other UI
174
+ (for example closing on route change or synchronizing with a parent disclosure):
175
+
176
+ ```tsx
177
+ import { useState } from "react";
178
+ import {
179
+ Select,
180
+ SelectContent,
181
+ SelectItem,
182
+ SelectTrigger,
183
+ SelectValue,
184
+ } from "@you-agent-factory/components/forms";
185
+
186
+ function ControlledOpenExample() {
187
+ const [open, setOpen] = useState(false);
188
+
189
+ return (
190
+ <Select onOpenChange={setOpen} open={open} value="story">
191
+ <SelectTrigger aria-label="Work type">
192
+ <SelectValue placeholder="Select a work type" />
193
+ </SelectTrigger>
194
+ <SelectContent>
195
+ <SelectItem value="story">Story</SelectItem>
196
+ </SelectContent>
197
+ </Select>
198
+ );
199
+ }
200
+ ```
201
+
202
+ Host responsibilities for controlled open:
203
+
204
+ - Provide the current `open` boolean on every render.
205
+ - Update host state inside `onOpenChange` when the user opens or closes the
206
+ menu.
207
+
208
+ ### Uncontrolled usage
209
+
210
+ Omit `value` and `open` to let Radix manage internal state. `defaultValue` still
211
+ sets the initial selection. Prefer controlled mode when host validation or
212
+ cross-field logic depends on the current selection.
213
+
214
+ ## Keyboard behavior and focus
215
+
216
+ Radix select primitives provide combobox semantics on the trigger:
217
+
218
+ | Key | Behavior |
219
+ | --- | --- |
220
+ | `Tab` / `Shift+Tab` | Move focus to and from the trigger. |
221
+ | `Space`, `Enter`, `ArrowDown`, `ArrowUp` | Open the menu when closed. |
222
+ | `ArrowDown` / `ArrowUp` | Move highlight across enabled options while open. |
223
+ | `Enter` / `Space` | Select the highlighted enabled option and close the menu. |
224
+ | `Escape` | Close the menu without changing the selection. |
225
+ | `Home` / `End` | Jump to the first or last enabled option while open. |
226
+
227
+ After a successful selection, focus returns to the trigger. Package styles keep
228
+ `focus-visible` rings on the trigger and highlighted menu items during keyboard
229
+ interaction.
230
+
231
+ `NativeSelect` uses native platform keyboard behavior for `<select>` elements.
232
+
233
+ ## Disabled behavior
234
+
235
+ | Surface | Behavior |
236
+ | --- | --- |
237
+ | Disabled field (`disabled` on `Select` or `EnumSelect`) | Trigger is not interactive, exposes disabled semantics, and cannot open the menu. |
238
+ | Disabled option (`disabled` on `SelectItem` or `EnumSelectOption`) | Skipped by keyboard navigation and not selectable by pointer. |
239
+ | Loading `EnumSelect` (`loading`) | Trigger is disabled, sets `aria-busy`, and shows `loadingLabel` as the placeholder. Controlled value is routed through `SELECT_EMPTY_STATE_VALUE` so the loading label stays visible. |
240
+
241
+ ## Loading and empty states
242
+
243
+ `EnumSelect` supports asynchronous option loading without dashboard wiring:
244
+
245
+ ```tsx
246
+ <EnumSelect
247
+ id="work-type"
248
+ loading={isLoading}
249
+ loadingLabel="Loading work types..."
250
+ emptyOptionsLabel="No work types available"
251
+ onValueChange={setValue}
252
+ options={options}
253
+ value={value}
254
+ />
255
+ ```
256
+
257
+ While `loading` is `true`:
258
+
259
+ - The trigger is disabled and marked busy.
260
+ - The menu shows a non-selectable `SelectEmpty` row with `loadingLabel`.
261
+ - Host `value` is not shown in the trigger; the loading label takes precedence.
262
+
263
+ When loading completes and `options` is still empty, `emptyOptionsLabel` is used
264
+ for both the menu affordance and the visible empty state.
265
+
266
+ ## Error and description relationships
267
+
268
+ Use host-owned helper and error text with explicit ARIA relationships on the
269
+ trigger. `SelectField` is a small layout helper when you want label, description,
270
+ and error regions colocated with the control:
271
+
272
+ ```tsx
273
+ import {
274
+ Select,
275
+ SelectContent,
276
+ SelectField,
277
+ SelectItem,
278
+ SelectTrigger,
279
+ SelectValue,
280
+ } from "@you-agent-factory/components/forms";
281
+
282
+ function WorkTypeField({ error }: { error?: string }) {
283
+ const controlId = "work-type";
284
+ const helperId = "work-type-helper";
285
+ const errorId = "work-type-error";
286
+
287
+ return (
288
+ <SelectField
289
+ description="Choose the work type for this request."
290
+ descriptionId={helperId}
291
+ error={error}
292
+ errorId={errorId}
293
+ inputId={controlId}
294
+ label="Work type"
295
+ >
296
+ <Select value="story">
297
+ <SelectTrigger
298
+ aria-describedby={`${helperId} ${errorId}`}
299
+ aria-invalid={Boolean(error)}
300
+ id={controlId}
301
+ >
302
+ <SelectValue placeholder="Select a work type" />
303
+ </SelectTrigger>
304
+ <SelectContent>
305
+ <SelectItem value="story">Story</SelectItem>
306
+ </SelectContent>
307
+ </Select>
308
+ </SelectField>
309
+ );
310
+ }
311
+ ```
312
+
313
+ Error text renders with `role="alert"` so assistive technology announces
314
+ validation failures without relying on color alone. Wire `aria-describedby` to
315
+ both helper and error element ids when both are present.
316
+
317
+ ## Long labels and compact layouts
318
+
319
+ `SelectTrigger` clamps the selected value with `line-clamp-1`. `SelectItem` text
320
+ uses `line-clamp-2` with `break-words` so long option labels stay inside the
321
+ menu without overlapping neighboring UI. Prefer concise labels in product copy,
322
+ but the package constrains overflow for narrow containers and mobile widths.
323
+
324
+ ## NativeSelect
325
+
326
+ Use `NativeSelect` when a native `<select>` is acceptable and you only need
327
+ package field styling:
328
+
329
+ ```tsx
330
+ import { NativeSelect } from "@you-agent-factory/components/forms";
331
+
332
+ function PriorityField() {
333
+ return (
334
+ <NativeSelect aria-label="Priority" defaultValue="medium" id="priority">
335
+ <option value="low">Low</option>
336
+ <option value="medium">Medium</option>
337
+ <option value="high">High</option>
338
+ </NativeSelect>
339
+ );
340
+ }
341
+ ```
342
+
343
+ `NativeSelect` forwards standard `<select>` props. Host applications still own
344
+ `<option>` children, labels, validation messaging, and change handlers.
345
+
346
+ ## Storybook examples
347
+
348
+ Package Storybook stories demonstrate expected states with package imports only.
349
+ Run `bun run storybook` from `ui/packages/components` or inspect the static
350
+ build with `bun run build-storybook`.
351
+
352
+ | Story | What it shows |
353
+ | --- | --- |
354
+ | `Forms/PackageSelect/ControlledValue` | Host-controlled selection |
355
+ | `Forms/PackageSelect/ControlledOpen` | Host-controlled menu open state |
356
+ | `Forms/PackageSelect/KeyboardInteraction` | Keyboard open, navigate, select, and focus return |
357
+ | `Forms/PackageSelect/DisabledField` | Disabled trigger |
358
+ | `Forms/PackageSelect/EmptyOptions` | Non-selectable empty menu affordance |
359
+ | `Forms/PackageSelect/LoadingOptions` | Loading trigger and menu state |
360
+ | `Forms/PackageSelect/ErrorState` | `aria-invalid` and visible error text |
361
+ | `Forms/PackageSelect/LongLabel` | Long labels at desktop width |
362
+ | `Forms/PackageSelect/LongLabelMobile` | Long labels at mobile width |