@horon-ai/react 0.1.1 → 0.1.3

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 (98) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -2
  3. package/src/components/agent-activity.tsx +75 -7
  4. package/src/components/agent-lane.tsx +32 -3
  5. package/src/components/agent-lanes-timeline.tsx +85 -7
  6. package/src/components/agent-panel.tsx +46 -4
  7. package/src/components/agent-steering-panel.tsx +53 -3
  8. package/src/components/approval-card.tsx +34 -2
  9. package/src/components/avatar.tsx +20 -4
  10. package/src/components/badge.tsx +24 -3
  11. package/src/components/banner.tsx +19 -3
  12. package/src/components/bot-instructions-panel.tsx +86 -7
  13. package/src/components/bot-list-row.tsx +13 -1
  14. package/src/components/bot-memory-panel.tsx +58 -3
  15. package/src/components/bottom-sheet.tsx +61 -7
  16. package/src/components/breadcrumb-header.tsx +32 -2
  17. package/src/components/breadcrumb.tsx +31 -1
  18. package/src/components/button.tsx +46 -1
  19. package/src/components/calendar.tsx +76 -6
  20. package/src/components/card.tsx +30 -1
  21. package/src/components/chat-bubble.tsx +32 -3
  22. package/src/components/chat-controls.tsx +60 -7
  23. package/src/components/checkbox.tsx +22 -1
  24. package/src/components/chip.tsx +29 -2
  25. package/src/components/choice-card.tsx +25 -1
  26. package/src/components/code-panel.tsx +28 -3
  27. package/src/components/combobox.tsx +52 -2
  28. package/src/components/command-palette.tsx +65 -1
  29. package/src/components/composer.tsx +28 -1
  30. package/src/components/data-table.tsx +198 -16
  31. package/src/components/date-field.tsx +31 -2
  32. package/src/components/dialog.tsx +45 -3
  33. package/src/components/directory-card.tsx +23 -2
  34. package/src/components/document-extract-panel.tsx +83 -5
  35. package/src/components/empty-state.tsx +21 -2
  36. package/src/components/field.tsx +49 -3
  37. package/src/components/file-drop.tsx +39 -4
  38. package/src/components/filter-bar.tsx +38 -1
  39. package/src/components/flue-background.tsx +88 -4
  40. package/src/components/horon-icon.tsx +50 -3
  41. package/src/components/horon-wordmark.tsx +26 -1
  42. package/src/components/icon-button.tsx +38 -1
  43. package/src/components/input-group.tsx +57 -6
  44. package/src/components/input.tsx +48 -2
  45. package/src/components/integration-row.tsx +43 -2
  46. package/src/components/kbd.tsx +28 -2
  47. package/src/components/knowledge-base-panel.tsx +40 -2
  48. package/src/components/liquid-glass.tsx +16 -0
  49. package/src/components/list-detail.tsx +89 -7
  50. package/src/components/marketplace-card.tsx +40 -4
  51. package/src/components/mcp-server-row.tsx +49 -3
  52. package/src/components/menu.tsx +83 -3
  53. package/src/components/message-extras.tsx +50 -4
  54. package/src/components/modal.tsx +71 -4
  55. package/src/components/orchestrator-panel.tsx +48 -4
  56. package/src/components/page-header.tsx +26 -3
  57. package/src/components/plan-card.tsx +28 -1
  58. package/src/components/plan-step.tsx +41 -1
  59. package/src/components/plugin-row.tsx +61 -3
  60. package/src/components/progress.tsx +32 -2
  61. package/src/components/radio-group.tsx +55 -2
  62. package/src/components/research-run-panel.tsx +73 -3
  63. package/src/components/schedule.tsx +163 -12
  64. package/src/components/segmented-control.tsx +57 -2
  65. package/src/components/select.tsx +60 -0
  66. package/src/components/settings-list.tsx +66 -8
  67. package/src/components/sidebar.tsx +214 -23
  68. package/src/components/skeleton.tsx +61 -4
  69. package/src/components/slider.tsx +30 -0
  70. package/src/components/source-row.tsx +43 -4
  71. package/src/components/spinner.tsx +29 -2
  72. package/src/components/split-button.tsx +37 -3
  73. package/src/components/switch.tsx +31 -1
  74. package/src/components/tab-bar.tsx +47 -2
  75. package/src/components/tabs.tsx +59 -5
  76. package/src/components/task-row.tsx +49 -3
  77. package/src/components/textarea-field.tsx +36 -2
  78. package/src/components/textarea.tsx +35 -2
  79. package/src/components/toast.tsx +52 -3
  80. package/src/components/tool-call.tsx +87 -4
  81. package/src/components/tool-calls-header.tsx +32 -1
  82. package/src/components/tool-permissions.tsx +50 -2
  83. package/src/components/tooltip.tsx +33 -1
  84. package/src/components/top-bar.tsx +35 -2
  85. package/src/components/usage-meter.tsx +28 -2
  86. package/src/components/workspace-menu.tsx +46 -1
  87. package/src/hooks/use-active-indicator.ts +44 -1
  88. package/src/hooks/use-color-scheme.ts +24 -1
  89. package/src/hooks/use-copy-to-clipboard.ts +23 -0
  90. package/src/hooks/use-device.ts +29 -1
  91. package/src/hooks/use-hotkey.ts +21 -0
  92. package/src/hooks/use-media-query.ts +18 -0
  93. package/src/hooks/use-platform.ts +30 -1
  94. package/src/hooks/use-reduced-motion.ts +18 -1
  95. package/src/index.ts +31 -0
  96. package/src/lib/cn.ts +13 -1
  97. package/src/lib/cursor-ghost.ts +16 -6
  98. package/src/lib/platform.ts +71 -4
@@ -5,42 +5,112 @@ import { cn } from "../lib/cn";
5
5
  import { ChatBubble } from "./chat-bubble";
6
6
  import { Spinner } from "./spinner";
7
7
 
8
+ /**
9
+ * One query row in the list under the status line.
10
+ */
8
11
  export type ResearchQuery = {
12
+ /** Stable key for the row. */
9
13
  id: string;
10
- /** `searching` shows a spinner and the muted text; `done` a check. */
14
+ /**
15
+ * `searching` shows a spinner and the muted text; `done` a check. Anything else (omitted, and
16
+ * any other value) renders as `done`.
17
+ * @default "done"
18
+ */
11
19
  state?: "done" | "searching";
20
+ /** The query text, truncated with an ellipsis when the row runs out of width. */
12
21
  text: ReactNode;
13
22
  /** Result count shown at the end of the row, such as "8 results". */
14
23
  results?: ReactNode;
15
24
  };
16
25
 
26
+ /**
27
+ * Props for {@link ResearchRunPanel}. Extra `section` props (`id`, `data-*`, handlers) land on the
28
+ * outer panel element.
29
+ */
17
30
  export type ResearchRunPanelProps = Omit<ComponentProps<"section">, "title"> & {
31
+ /**
32
+ * Header title next to the magnifying glass.
33
+ * @default "Research"
34
+ */
18
35
  title?: ReactNode;
36
+ /**
37
+ * Text of the header button that starts a new run.
38
+ * @default "New research"
39
+ */
19
40
  newLabel?: ReactNode;
41
+ /** Fires when the header button is clicked; the button is still rendered without it. */
20
42
  onNewResearch?: () => void;
21
43
  /** The question bubble, right-aligned. */
22
44
  question: ReactNode;
23
- /** Shows the spinner, status line and caret while the run works. */
45
+ /**
46
+ * Shows the spinner, status line and caret while the run works; when false the status block
47
+ * collapses and the submit button becomes a send arrow.
48
+ * @default true
49
+ */
24
50
  running?: boolean;
51
+ /**
52
+ * Label in front of the caret in the status line.
53
+ * @default "Researching"
54
+ */
25
55
  statusLabel?: ReactNode;
56
+ /** First meta value after the status label, such as "12s"; joins `read`/`total` with a middot. */
26
57
  elapsed?: ReactNode;
58
+ /** Pages read so far; with `total` it renders "N of M read" and drives the sources pill label. */
27
59
  read?: number;
60
+ /** Pages in the run; must be set together with `read` for the count to appear. */
28
61
  total?: number;
62
+ /** Queries resolved so far, drawn under a 1.5px rail with a 30ms stagger per row. */
29
63
  queries?: ResearchQuery[];
30
64
  /** Initials for the stacked source avatars under the queries. */
31
65
  sources?: string[];
66
+ /** Fires from the round button while `running`, which then shows a stop glyph instead of an arrow. */
32
67
  onStop?: () => void;
33
68
  /** Makes the sources pill actionable; without it the pill is a plain label. */
34
69
  onOpenSources?: () => void;
35
70
  /** Makes the running status line expandable; without it the row carries no caret. */
36
71
  onToggleStatus?: () => void;
72
+ /**
73
+ * Placeholder and accessible name of the follow-up input.
74
+ * @default "Ask a follow-up"
75
+ */
37
76
  followUpPlaceholder?: string;
77
+ /** Fires with the raw input value on submit; the field is cleared afterwards. */
38
78
  onFollowUp?: (value: string) => void;
39
79
  };
40
80
 
41
81
  const stagger = (index: number) => ({ "--i": index, animationDelay: `calc(var(--i) * 30ms)` }) as CSSProperties;
42
82
 
43
- /** Paper `Research run` (Agents N10): a 24px-radius panel with a header, the question, a live status line and the queries read so far. */
83
+ /**
84
+ * Paper `Research run` (Agents N10): a 24px-radius panel with a header, the question, a live status line and the queries read so far.
85
+ *
86
+ * The panel is presentational: it holds no run state of its own. Submitted follow-ups are handed
87
+ * to `onFollowUp` and the input is cleared, so the caller appends the new question. While
88
+ * `running` the status line is announced politely and the round button stops the run instead of
89
+ * submitting.
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * import { ResearchRunPanel } from "@horon-ai/react";
94
+ *
95
+ * function Example() {
96
+ * return (
97
+ * <ResearchRunPanel
98
+ * question="What changed in the EU AI Act this year?"
99
+ * statusLabel="Reading sources"
100
+ * read={4}
101
+ * total={12}
102
+ * queries={[
103
+ * { id: "1", text: "EU AI Act amendments", results: "8 results" },
104
+ * { id: "2", text: "Enforcement timeline", state: "searching" },
105
+ * ]}
106
+ * sources={["EU", "CE"]}
107
+ * onStop={() => {}}
108
+ * onFollowUp={(value) => console.log(value)}
109
+ * />
110
+ * );
111
+ * }
112
+ * ```
113
+ */
44
114
  export function ResearchRunPanel({
45
115
  title = "Research",
46
116
  newLabel = "New research",
@@ -8,23 +8,46 @@ import { cn } from "../lib/cn";
8
8
  import type { ControlSize } from "../lib/size";
9
9
  import { LiquidGlass } from "./liquid-glass";
10
10
 
11
+ /**
12
+ * Field size of a picker, on the shared control scale: `sm` 32px, `md` 40px, `lg` 48px.
13
+ */
11
14
  export type PickerFieldSize = ControlSize;
12
15
 
16
+ /**
17
+ * Props for {@link PickerField}. The type is closed: it declares no `disabled` or `name` (and no
18
+ * other Base UI `Select.Root` prop), so TypeScript rejects them before they reach the root; `items`
19
+ * is the label list the trigger reads.
20
+ */
13
21
  export type PickerFieldProps = {
14
- items: { value: string; label: ReactNode }[];
22
+ /** Choices in the popup; `value` is what `onValueChange` returns, `label` is what the row and trigger show. */
23
+ items: {
24
+ /** Value reported when the item is picked. */
25
+ value: string;
26
+ /** Text shown on the trigger and in the popup row. */
27
+ label: ReactNode;
28
+ }[];
29
+ /** Selected value in controlled mode. */
15
30
  value?: string;
31
+ /** Starting value in uncontrolled mode. */
16
32
  defaultValue?: string;
33
+ /** Fires with the newly selected value, or `null` when the choice is cleared, after a pick. */
17
34
  onValueChange?: (value: string | null) => void;
18
- /** Leading 16px icon; the Time field uses a clock. */
35
+ /** Leading icon, 16px on `sm` and `md` and 20px on `lg`; the Time field uses a clock. */
19
36
  icon?: ReactNode;
20
37
  /**
21
38
  * `solid` (default) is the opaque surface field with a default border, `glass` the veil field,
22
39
  * `inset` the 4% ink field inside the schedule card.
40
+ * @default "solid"
23
41
  */
24
42
  surface?: "solid" | "glass" | "inset";
25
- /** Field size: `sm` 32px, `md` 40px (default), `lg` 48px; scales width, icon and menu rows. */
43
+ /**
44
+ * Field size: `sm` 32px, `md` 40px (default), `lg` 48px; scales width, icon and menu rows.
45
+ * @default "md"
46
+ */
26
47
  size?: PickerFieldSize;
48
+ /** Accessible name of the trigger when no visible label is associated with it. */
27
49
  "aria-label"?: string;
50
+ /** Classes merged onto the trigger. */
28
51
  className?: string;
29
52
  };
30
53
 
@@ -73,7 +96,34 @@ const itemIndicatorSizeClass: Record<PickerFieldSize, string> = {
73
96
  lg: "[&_svg]:size-5",
74
97
  };
75
98
 
76
- /** Figma `Time field` (Routine schedule 110:765): a 160px picker on the same 32/40/48px and 14/16/18 text rungs as `Input`. */
99
+ /**
100
+ * Figma `Time field` (Routine schedule 110:765): a 160px picker on the same 32/40/48px and 14/16/18 text rungs as `Input`.
101
+ *
102
+ * The popup is portalled to the body and aligned under the trigger with an 8px offset, so it can
103
+ * escape a scrolling card; the caret rotates while it is open. The field never grows past its
104
+ * rung width but shrinks with a narrow parent.
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * import { useState } from "react";
109
+ * import { PickerField } from "@horon-ai/react";
110
+ *
111
+ * function Example() {
112
+ * const [time, setTime] = useState("09:00");
113
+ * return (
114
+ * <PickerField
115
+ * aria-label="Start time"
116
+ * value={time}
117
+ * onValueChange={(next) => setTime(next ?? time)}
118
+ * items={[
119
+ * { value: "09:00", label: "9:00 AM" },
120
+ * { value: "10:00", label: "10:00 AM" },
121
+ * ]}
122
+ * />
123
+ * );
124
+ * }
125
+ * ```
126
+ */
77
127
  export function PickerField({
78
128
  items,
79
129
  icon,
@@ -145,17 +195,39 @@ export function PickerField({
145
195
  );
146
196
  }
147
197
 
198
+ /**
199
+ * Day circle size: `sm` 32px, `md` 36px, `lg` 44px.
200
+ */
148
201
  export type WeekdayPickerSize = ControlSize;
149
202
 
203
+ /**
204
+ * Props for {@link WeekdayPicker}. It is a multiple-selection toggle group, so `value` and
205
+ * `onValueChange` carry an array of day values.
206
+ */
150
207
  export type WeekdayPickerProps = {
151
208
  /** One entry per day, in display order. */
152
- days: { value: string; label: string; name: string }[];
209
+ days: {
210
+ /** Value that goes into `value` and `onValueChange`. */
211
+ value: string;
212
+ /** Short text on the circle, such as "M". */
213
+ label: string;
214
+ /** Accessible name of the toggle, such as "Monday". */
215
+ name: string;
216
+ }[];
217
+ /** Selected day values in controlled mode. */
153
218
  value?: string[];
219
+ /** Selected day values at first render in uncontrolled mode. */
154
220
  defaultValue?: string[];
221
+ /** Fires with the complete next selection whenever a day is toggled on or off. */
155
222
  onValueChange?: (value: string[]) => void;
156
- /** Day circle size: `sm` 32px, `md` 36px (default), `lg` 44px. */
223
+ /**
224
+ * Day circle size: `sm` 32px, `md` 36px (default), `lg` 44px.
225
+ * @default "md"
226
+ */
157
227
  size?: WeekdayPickerSize;
228
+ /** Accessible name of the group. */
158
229
  "aria-label"?: string;
230
+ /** Classes merged onto the wrapping flex row. */
159
231
  className?: string;
160
232
  };
161
233
 
@@ -172,7 +244,32 @@ const weekdayToggleSizeClass: Record<WeekdayPickerSize, string> = {
172
244
  lg: "size-11 text-label-lg",
173
245
  };
174
246
 
175
- /** Figma `Weekday picker` (110:765): 36px round toggles, ink when on and 6% ink when off. */
247
+ /**
248
+ * Figma `Weekday picker` (110:765): 36px round toggles, ink when on and 6% ink when off.
249
+ *
250
+ * Toggling one day does not close the group; each press reports the whole next selection. The
251
+ * group wraps instead of shrinking its circles, so touch targets stay at least 32px. It is
252
+ * multiple-selection, so several days can be on at once.
253
+ *
254
+ * @example
255
+ * ```tsx
256
+ * import { WeekdayPicker } from "@horon-ai/react";
257
+ *
258
+ * function Example() {
259
+ * return (
260
+ * <WeekdayPicker
261
+ * aria-label="Repeat on"
262
+ * defaultValue={["mon", "wed", "fri"]}
263
+ * days={[
264
+ * { value: "mon", label: "M", name: "Monday" },
265
+ * { value: "tue", label: "T", name: "Tuesday" },
266
+ * { value: "wed", label: "W", name: "Wednesday" },
267
+ * ]}
268
+ * />
269
+ * );
270
+ * }
271
+ * ```
272
+ */
176
273
  export function WeekdayPicker({
177
274
  days,
178
275
  value,
@@ -210,15 +307,28 @@ export function WeekdayPicker({
210
307
  );
211
308
  }
212
309
 
310
+ /**
311
+ * Props for {@link ScheduleCard}. The card is a plain section: it lays out what you pass and
312
+ * draws the frame, header and next-run strip around it.
313
+ */
213
314
  export type ScheduleCardProps = {
315
+ /** Header title next to the repeat icon. */
214
316
  title: ReactNode;
215
- /** The "Next run" line at the bottom. */
317
+ /** The "Next run" line at the bottom; without it the strip is not rendered. */
216
318
  nextRun?: ReactNode;
319
+ /** Body of the card, usually a stack of `ScheduleSection` blocks and pickers. */
217
320
  children: ReactNode;
218
- /** Card size: `sm` 340px, `md` 380px (default), `lg` 420px; scales padding, gaps and text. */
321
+ /**
322
+ * Card size: `sm` 340px, `md` 380px (default), `lg` 420px; scales padding, gaps and text.
323
+ * @default "md"
324
+ */
219
325
  size?: ControlSize;
220
- /** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. */
326
+ /**
327
+ * `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card.
328
+ * @default "solid"
329
+ */
221
330
  surface?: "solid" | "glass";
331
+ /** Classes merged onto the card. */
222
332
  className?: string;
223
333
  };
224
334
 
@@ -259,7 +369,37 @@ const nextRunIconSizeClass: Record<ControlSize, string> = {
259
369
  lg: "size-5",
260
370
  };
261
371
 
262
- /** Figma `Routine schedule card` (110:765): a 380px card, 20px padding, 16px gaps, solid or thick glass. */
372
+ /**
373
+ * Figma `Routine schedule card` (110:765): a 380px card, 20px padding, 16px gaps, solid or thick glass.
374
+ *
375
+ * Compose it from `ScheduleSection` blocks holding `WeekdayPicker`, `PickerField` or `Input`
376
+ * controls. On a narrow column the card folds to the available width instead of overflowing.
377
+ *
378
+ * @example
379
+ * ```tsx
380
+ * import { PickerField, ScheduleCard, ScheduleSection, WeekdayPicker } from "@horon-ai/react";
381
+ *
382
+ * function Example() {
383
+ * return (
384
+ * <ScheduleCard title="Morning digest" nextRun="Next run tomorrow at 9:00 AM">
385
+ * <ScheduleSection label="Repeat">
386
+ * <PickerField
387
+ * aria-label="Repeat"
388
+ * surface="inset"
389
+ * items={[
390
+ * { value: "daily", label: "Daily" },
391
+ * { value: "weekly", label: "Weekly" },
392
+ * ]}
393
+ * />
394
+ * </ScheduleSection>
395
+ * <ScheduleSection label="Days">
396
+ * <WeekdayPicker days={[{ value: "mon", label: "M", name: "Monday" }]} />
397
+ * </ScheduleSection>
398
+ * </ScheduleCard>
399
+ * );
400
+ * }
401
+ * ```
402
+ */
263
403
  export function ScheduleCard({
264
404
  title,
265
405
  nextRun,
@@ -298,7 +438,7 @@ export function ScheduleCard({
298
438
  );
299
439
  }
300
440
 
301
- /** A captioned block inside the schedule card, such as "Repeat" or "Days"; the caption-to-control gap is `Field`'s. */
441
+ /** Vertical gap between a section's caption and its control, per size. */
302
442
  const sectionSizeClass: Record<ControlSize, string> = {
303
443
  sm: "gap-1",
304
444
  md: "gap-2",
@@ -311,6 +451,10 @@ const sectionLabelSizeClass: Record<ControlSize, string> = {
311
451
  lg: "text-label-sm",
312
452
  };
313
453
 
454
+ /**
455
+ * A captioned block inside the schedule card, such as "Repeat" or "Days". Renders the caption in
456
+ * secondary text above its children, so a card body becomes a stack of labelled controls.
457
+ */
314
458
  export function ScheduleSection({
315
459
  label,
316
460
  children,
@@ -318,9 +462,16 @@ export function ScheduleSection({
318
462
  size = "md",
319
463
  className,
320
464
  }: {
465
+ /** Caption above the control, in secondary text. */
321
466
  label: ReactNode;
467
+ /** The control the caption belongs to. */
322
468
  children: ReactNode;
469
+ /**
470
+ * Matches the size of the controls inside the section; scales the caption rung and the caption gap.
471
+ * @default "md"
472
+ */
323
473
  size?: ControlSize;
474
+ /** Classes merged onto the block. */
324
475
  className?: string;
325
476
  }) {
326
477
  return (
@@ -9,18 +9,43 @@ import { LiquidGlass } from "./liquid-glass";
9
9
 
10
10
  /** `tab` keeps Paper's page-tab rung beside the three control sizes. */
11
11
  export type SegmentedControlSize = ControlSize | "tab";
12
+
13
+ /**
14
+ * Track treatment of a segmented control.
15
+ *
16
+ * - `solid` — opaque surface track with a default border and a 10% ink selection.
17
+ * - `glass` — regular glass; the selection is a thick glass pill over it.
18
+ * - `inset` — 5% ink track, for use inside a routine card.
19
+ * - `inset-faint` — 4% ink track, for the header filter row.
20
+ * - `subtle` — `bg-subtle` page-tab tray whose selection is a plain surface pill.
21
+ */
12
22
  export type SegmentedControlSurface = "solid" | "glass" | "inset" | "inset-faint" | "subtle";
13
23
 
24
+ /**
25
+ * Props for {@link SegmentedControl}. The control is single-choice: only one item value is selected.
26
+ */
14
27
  export type SegmentedControlProps = {
15
- items: { value: string; label: ReactNode; disabled?: boolean }[];
28
+ /** Segments in display order; `value` is what `onValueChange` returns and `disabled` dims one segment. */
29
+ items: {
30
+ /** Value reported when this segment is chosen. */
31
+ value: string;
32
+ /** Text on the segment. */
33
+ label: ReactNode;
34
+ /** Disables only this segment; it cannot be selected or focused. */
35
+ disabled?: boolean;
36
+ }[];
37
+ /** Selected value in controlled mode. */
16
38
  value?: string;
39
+ /** Selected value at first render in uncontrolled mode. */
17
40
  defaultValue?: string;
41
+ /** Fires with the chosen value when the selection changes. */
18
42
  onValueChange?: (value: string) => void;
19
43
  /**
20
44
  * `md`: Segmented control (62:379), 36px track, 30px segments, 16px padding.
21
45
  * `sm`: one rung down, a 32px track with 26px segments and 10px padding.
22
46
  * `lg`: Segmented filter (88:723), 44px track, 36px segments.
23
47
  * `tab`: Paper page tabs (Projects Active/Archived), 4px track padding and gap, 32px segments with 14px padding.
48
+ * @default "md"
24
49
  */
25
50
  size?: SegmentedControlSize;
26
51
  /**
@@ -28,11 +53,17 @@ export type SegmentedControlProps = {
28
53
  * `glass` is regular glass. `inset` is the 5% ink track inside the routine card,
29
54
  * `inset-faint` the 4% ink track inside the header filter. `subtle` is the bg-subtle page-tab tray whose
30
55
  * selection is a plain white pill.
56
+ * @default "solid"
31
57
  */
32
58
  surface?: SegmentedControlSurface;
33
- /** Segments share the width equally instead of hugging their labels. */
59
+ /**
60
+ * Segments share the width equally instead of hugging their labels.
61
+ * @default size === "md"
62
+ */
34
63
  fill?: boolean;
64
+ /** Accessible name of the toggle group. */
35
65
  "aria-label"?: string;
66
+ /** Classes merged onto the track. */
36
67
  className?: string;
37
68
  };
38
69
 
@@ -64,6 +95,30 @@ const surfaceClass: Record<SegmentedControlSurface, string> = {
64
95
  * thick fill and a 0 1 3 8% shadow, the iOS tab bar selection; dark mode keeps a
65
96
  * 10% fill. On the solid and inset tracks it is 10% ink. The pill is the same on
66
97
  * every rung, as in `Tabs`.
98
+ *
99
+ * The track scrolls horizontally when the segments do not fit, so a long label set never spills.
100
+ * Pass `aria-label` because the track has no visible name of its own.
101
+ *
102
+ * @example
103
+ * ```tsx
104
+ * import { useState } from "react";
105
+ * import { SegmentedControl } from "@horon-ai/react";
106
+ *
107
+ * function Example() {
108
+ * const [view, setView] = useState("active");
109
+ * return (
110
+ * <SegmentedControl
111
+ * aria-label="Projects"
112
+ * value={view}
113
+ * onValueChange={setView}
114
+ * items={[
115
+ * { value: "active", label: "Active" },
116
+ * { value: "archived", label: "Archived" },
117
+ * ]}
118
+ * />
119
+ * );
120
+ * }
121
+ * ```
67
122
  */
68
123
  export function SegmentedControl({
69
124
  items,
@@ -7,28 +7,65 @@ import type { ControlSize } from "../lib/size";
7
7
  import { fieldBoxClass, fieldSizeClass, fieldTextClass, type InputSurface } from "./input";
8
8
  import { inputGroupSegmentClass, useInputGroup } from "./input-group";
9
9
 
10
+ /** Field height and text rung of a `field` select: 32/40/48px with 14/16/18px text. */
10
11
  export type SelectSize = ControlSize;
12
+
13
+ /**
14
+ * One choice in the list.
15
+ */
11
16
  export type SelectItemData = {
17
+ /** Value reported on selection. */
12
18
  value: string;
19
+ /** Text shown both in the trigger and in the popup row. */
13
20
  label: ReactNode;
14
21
  /** Shorter text for the trigger once chosen, such as `+82` for "+82 South Korea". */
15
22
  selectedLabel?: ReactNode;
23
+ /** Dims the row and blocks picking it. */
16
24
  disabled?: boolean;
17
25
  };
18
26
 
27
+ /**
28
+ * Props for {@link Select}. Selection props (`value`, `defaultValue`, `onValueChange`, `name`) pass
29
+ * through to Base UI's select root.
30
+ */
19
31
  export type SelectProps = Omit<ComponentProps<typeof BaseSelect.Root>, "items" | "children"> & {
32
+ /** Choices in the popup, in display order. */
20
33
  items: SelectItemData[];
34
+ /**
35
+ * Text shown in the trigger before a choice is made.
36
+ * @default "Select"
37
+ */
21
38
  placeholder?: ReactNode;
39
+ /**
40
+ * Field height and text rung for `variant="field"`; a `chip` trigger keeps its fixed 32px,
41
+ * `label-md` size and still uses `size` for its popup rows, and a `segment` takes the enclosing
42
+ * `InputGroup` size instead.
43
+ * @default "md"
44
+ */
22
45
  size?: SelectSize;
46
+ /**
47
+ * Fill of the field and of its popup; `glass` also switches the popup to the veil. A `chip` uses
48
+ * it for the popup only, since the chip has its own subtle fill.
49
+ * @default "solid"
50
+ */
23
51
  surface?: InputSurface;
52
+ /** Paints the danger border and sets `aria-invalid` on the trigger. */
24
53
  invalid?: boolean;
25
54
  /**
26
55
  * `chip` is the Paper filter chip: a 32px bg-subtle pill, 12px padding, 13/18 medium, sized to its label.
27
56
  * `segment` is the tinted prefix or suffix of an `InputGroup`; it takes the group's size and disabled state.
57
+ * @default "field"
28
58
  */
29
59
  variant?: "field" | "chip" | "segment";
60
+ /**
61
+ * Draws the caret at the right of the trigger; set it to false when the trigger supplies its own
62
+ * icon.
63
+ * @default true
64
+ */
30
65
  showIndicator?: boolean;
66
+ /** Classes merged onto the trigger. */
31
67
  className?: string;
68
+ /** Accessible name of the trigger. */
32
69
  "aria-label"?: string;
33
70
  };
34
71
 
@@ -53,6 +90,29 @@ const triggerFocusClass = "focus-visible:outline-2 focus-visible:outline-offset-
53
90
  * text size. The popup is the veil (`veil-overlay`), 20px radius, 40px rows; the chosen row
54
91
  * is 6% ink with a check. `variant="chip"` keeps its single 32px chip size;
55
92
  * `variant="segment"` follows its `InputGroup`.
93
+ *
94
+ * The popup is portalled and positioned under the trigger; rows are highlighted with the keyboard
95
+ * and marked with a check when chosen. The trigger is a button, not a native `select`, so give it an
96
+ * `aria-label` or associate a visible label yourself; a wrapping `<label>` does not name it.
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * import { Select } from "@horon-ai/react";
101
+ *
102
+ * function Example() {
103
+ * return (
104
+ * <Select
105
+ * aria-label="Country"
106
+ * defaultValue="kr"
107
+ * items={[
108
+ * { value: "kr", label: "South Korea", selectedLabel: "+82" },
109
+ * { value: "jp", label: "Japan", selectedLabel: "+81" },
110
+ * { value: "us", label: "United States", disabled: true },
111
+ * ]}
112
+ * />
113
+ * );
114
+ * }
115
+ * ```
56
116
  */
57
117
  export function Select({
58
118
  items,