@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
@@ -4,14 +4,44 @@ import { cn } from "../lib/cn";
4
4
  import { LiquidGlass } from "./liquid-glass";
5
5
  import { Switch, type SwitchProps } from "./switch";
6
6
 
7
- type WithClass<T> = Omit<T, "className"> & { className?: string };
7
+ /** A component's own prop set with `className` replaced by a plain optional string, so names never collide with the DOM attribute. */
8
+ type WithClass<T> = Omit<T, "className"> & {
9
+ /** Classes merged onto the rendered element. */
10
+ className?: string;
11
+ };
8
12
 
13
+ /**
14
+ * Props for {@link SettingsList}. Other `div` props (`id`, `data-*`, handlers) land on the card.
15
+ */
9
16
  export type SettingsListProps = WithClass<ComponentProps<"div">> & {
10
- /** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. */
17
+ /**
18
+ * `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card.
19
+ * @default "solid"
20
+ */
11
21
  surface?: "solid" | "glass";
12
22
  };
13
23
 
14
- /** Paper `Settings list` (1VD8-0): a 16px-radius card of divided rows, 20px inline padding, solid or thick glass. */
24
+ /**
25
+ * Paper `Settings list` (1VD8-0): a 16px-radius card of divided rows, 20px inline padding, solid or thick glass.
26
+ *
27
+ * Put only `SettingsRow` children inside: each row draws its own bottom divider, and the card clips
28
+ * the corners, so grouping rows here is enough to get the divided list. Glass keeps the rows above
29
+ * the liquid layer.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * import { SettingsList, SettingsRow, SettingsSwitch } from "@horon-ai/react";
34
+ *
35
+ * function Example() {
36
+ * return (
37
+ * <SettingsList>
38
+ * <SettingsRow title="Sync on Wi-Fi" description="Only use Wi-Fi for uploads" control={<SettingsSwitch defaultChecked />} />
39
+ * <SettingsRow title="Version" control={<span>2.4.1</span>} />
40
+ * </SettingsList>
41
+ * );
42
+ * }
43
+ * ```
44
+ */
15
45
  export function SettingsList({ surface = "solid", className, children, ...props }: SettingsListProps) {
16
46
  return (
17
47
  <div
@@ -28,17 +58,27 @@ export function SettingsList({ surface = "solid", className, children, ...props
28
58
  );
29
59
  }
30
60
 
61
+ /**
62
+ * Props for {@link SettingsRow}. Extra `div` props (`id`, `data-*`, handlers) land on the row.
63
+ */
31
64
  export type SettingsRowProps = Omit<ComponentProps<"div">, "className" | "title"> & {
65
+ /** Primary line of the row. */
32
66
  title: ReactNode;
67
+ /** Secondary line under the title, in secondary text. */
33
68
  description?: ReactNode;
34
69
  /** Leading slot before the title, usually a `SettingsRowIcon`; the row tightens to 12px gap and 13px block padding. */
35
70
  leading?: ReactNode;
36
71
  /** Trailing control slot: a `Switch`, a `SettingsKeys` pair, a button. */
37
72
  control?: ReactNode;
73
+ /** Classes merged onto the row. */
38
74
  className?: string;
39
75
  };
40
76
 
41
- /** One row: title and subtitle on the left, control on the right, 1px divider except on the last row. */
77
+ /**
78
+ * One row: title and subtitle on the left, control on the right, 1px divider except on the last row.
79
+ *
80
+ * Part of {@link SettingsList}.
81
+ */
42
82
  export function SettingsRow({ title, description, leading, control, className, ...props }: SettingsRowProps) {
43
83
  return (
44
84
  <div
@@ -59,7 +99,12 @@ export function SettingsRow({ title, description, leading, control, className, .
59
99
  );
60
100
  }
61
101
 
62
- /** Paper settings row icon tile: 32px, 9px radius, subtle fill, 16px glyph centered. */
102
+ /**
103
+ * Paper settings row icon tile: 32px, 9px radius, subtle fill, 16px glyph centered. Decorative,
104
+ * so it is hidden from assistive technology; the row title carries the meaning.
105
+ *
106
+ * Part of {@link SettingsList}.
107
+ */
63
108
  export function SettingsRowIcon({ className, ...props }: WithClass<ComponentProps<"span">>) {
64
109
  return (
65
110
  <span
@@ -73,12 +118,21 @@ export function SettingsRowIcon({ className, ...props }: WithClass<ComponentProp
73
118
  );
74
119
  }
75
120
 
76
- /** A `Switch` pre-wired for a row's control slot (md, 44x26). */
121
+ /**
122
+ * A `Switch` pre-wired for a row's control slot (md, 44x26).
123
+ *
124
+ * Part of {@link SettingsList}.
125
+ */
77
126
  export function SettingsSwitch(props: SwitchProps) {
78
127
  return <Switch size="md" {...props} />;
79
128
  }
80
129
 
81
- /** One keycap: 26px high, 6px radius, subtle fill with a 1px hairline (replaces `Kbd` in settings rows). */
130
+ /**
131
+ * One keycap: 26px high, 6px radius, subtle fill with a 1px hairline (replaces `Kbd` in settings rows).
132
+ * It renders a `<kbd>`, so screen readers announce it as keyboard input rather than body text.
133
+ *
134
+ * Part of {@link SettingsList}.
135
+ */
82
136
  export function SettingsKey({ className, children, ...props }: WithClass<ComponentProps<"kbd">>) {
83
137
  return (
84
138
  <kbd
@@ -93,7 +147,11 @@ export function SettingsKey({ className, children, ...props }: WithClass<Compone
93
147
  );
94
148
  }
95
149
 
96
- /** A keycap pair or chord, 4px apart. */
150
+ /**
151
+ * A keycap pair or chord, 4px apart; a plain flex wrapper, so it takes its children as-is.
152
+ *
153
+ * Part of {@link SettingsList}.
154
+ */
97
155
  export function SettingsKeys({ className, ...props }: WithClass<ComponentProps<"span">>) {
98
156
  return <span className={cn("inline-flex items-center gap-1", className)} {...props} />;
99
157
  }
@@ -15,28 +15,57 @@ import {
15
15
  import { cn } from "../lib/cn";
16
16
  import { LiquidGlass } from "./liquid-glass";
17
17
 
18
- type DivProps = Omit<ComponentProps<"div">, "className"> & { className?: string };
19
- type ButtonProps = Omit<ComponentProps<"button">, "className"> & { className?: string };
18
+ /** A `div` prop set with an optional `className`, used by the parts that render a plain `div`. */
19
+ type DivProps = Omit<ComponentProps<"div">, "className"> & {
20
+ /** Classes merged onto the element. */
21
+ className?: string;
22
+ };
23
+ /** A `button` prop set with an optional `className`, used by the parts that render a `button`. */
24
+ type ButtonProps = Omit<ComponentProps<"button">, "className"> & {
25
+ /** Classes merged onto the button. */
26
+ className?: string;
27
+ };
20
28
 
29
+ /**
30
+ * Props for {@link Sidebar}. Other `nav` props (`aria-label`, `id`, handlers) land on the column.
31
+ */
21
32
  export type SidebarProps = Omit<ComponentProps<"nav">, "className"> & {
33
+ /** Classes merged onto the column. */
22
34
  className?: string;
23
35
  /**
24
36
  * `glass` (default) is the thick-glass column. `photo` is the Paper app sidebar: the Horon photo
25
37
  * on #EEF3F8 under a 55% white veil, with the thick glass kept for dark mode. `plain` paints no
26
38
  * fill or glass, so the column takes the color of the layout behind it, divided by a 1px border.
39
+ * @default "glass"
27
40
  */
28
41
  surface?: "glass" | "photo" | "plain";
29
42
  /**
30
43
  * Drag the right edge to change the width, from the `md` breakpoint (768px) up.
31
44
  * Below `md` the handle is hidden and the sidebar keeps its fixed 280px width.
45
+ * @default false
32
46
  */
33
47
  resizable?: boolean;
34
- /** Starting width in px when `resizable`. Double-clicking the handle returns to it. */
48
+ /**
49
+ * Starting width in px when `resizable`. Double-clicking the handle returns to it.
50
+ * @default 280
51
+ */
35
52
  defaultWidth?: number;
53
+ /**
54
+ * Narrowest width in px the handle can drag to.
55
+ * @default 240
56
+ */
36
57
  minWidth?: number;
58
+ /**
59
+ * Widest width in px the handle can drag to.
60
+ * @default 400
61
+ */
37
62
  maxWidth?: number;
38
63
  /** Called with the clamped width as it changes, during a drag too. */
39
64
  onWidthChange?: (width: number) => void;
65
+ /**
66
+ * Accessible name of the resize handle.
67
+ * @default "Resize sidebar"
68
+ */
40
69
  resizeLabel?: string;
41
70
  };
42
71
 
@@ -49,6 +78,27 @@ function clamp(value: number, min: number, max: number): number {
49
78
  /**
50
79
  * Figma `Sidebar` (88:848): a 280px thick-glass column with a 1px right border, 16/12/12px padding.
51
80
  * With `resizable`, an 8px handle on the right edge drags the width between `minWidth` and `maxWidth`.
81
+ *
82
+ * Width is uncontrolled: the column keeps its own state, starts at `defaultWidth` and reports every
83
+ * clamped change through `onWidthChange`. The handle is keyboard operable (arrow keys step 16px,
84
+ * Home/End jump to the limits) and sits behind `md`; the column never grows past a narrower parent.
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * import { Sidebar, SidebarGroup, SidebarHeader, SidebarItem } from "@horon-ai/react";
89
+ *
90
+ * function Example() {
91
+ * return (
92
+ * <Sidebar resizable onWidthChange={(width) => console.log(width)}>
93
+ * <SidebarHeader initial="H" name="Horon" onCollapse={() => {}} />
94
+ * <SidebarGroup label="Workspace">
95
+ * <SidebarItem current>Home</SidebarItem>
96
+ * <SidebarItem count={3}>Inbox</SidebarItem>
97
+ * </SidebarGroup>
98
+ * </Sidebar>
99
+ * );
100
+ * }
101
+ * ```
52
102
  */
53
103
  export function Sidebar({
54
104
  className,
@@ -65,7 +115,12 @@ export function Sidebar({
65
115
  }: SidebarProps) {
66
116
  const [width, setWidth] = useState(() => clamp(defaultWidth, minWidth, maxWidth));
67
117
  const [dragging, setDragging] = useState(false);
68
- const drag = useRef<{ x: number; width: number } | null>(null);
118
+ const drag = useRef<{
119
+ /** Pointer x (px, client coordinate) where the drag started. */
120
+ x: number;
121
+ /** Column width (px) at the moment the drag started, so every move applies to a fixed base. */
122
+ width: number;
123
+ } | null>(null);
69
124
 
70
125
  const resize = (next: number) => {
71
126
  const clamped = clamp(next, minWidth, maxWidth);
@@ -165,22 +220,46 @@ export function Sidebar({
165
220
  );
166
221
  }
167
222
 
223
+ /**
224
+ * Props for {@link SidebarHeader}. Renders the workspace switch and an optional collapse button in
225
+ * one 36px row.
226
+ */
168
227
  export type SidebarHeaderProps = {
169
228
  /** Workspace initial shown on the 28px ink tile. */
170
229
  initial: ReactNode;
230
+ /** Workspace name next to the tile; truncated when the column is narrow. */
171
231
  name: ReactNode;
172
232
  /** Props for the workspace switch button, such as a `MenuTrigger` render target. */
173
233
  switchProps?: ButtonProps;
174
234
  /** Replaces the default switch, such as a `WorkspaceMenu` whose trigger is a `SidebarWorkspaceSwitch`. */
175
235
  switchSlot?: ReactNode;
236
+ /** Shows the collapse button when set; it receives the click and the caller hides the column. */
176
237
  onCollapse?: () => void;
238
+ /**
239
+ * Accessible name of the collapse button.
240
+ * @default "Collapse sidebar"
241
+ */
177
242
  collapseLabel?: string;
243
+ /** Classes merged onto the header row. */
178
244
  className?: string;
179
245
  };
180
246
 
181
- export type SidebarWorkspaceSwitchProps = Omit<ButtonProps, "name"> & { initial: ReactNode; name: ReactNode };
247
+ /**
248
+ * Props for {@link SidebarWorkspaceSwitch}: every button prop except `name`, which is the visible
249
+ * workspace label instead.
250
+ */
251
+ export type SidebarWorkspaceSwitchProps = Omit<ButtonProps, "name"> & {
252
+ /** Workspace initial on the 28px ink tile. */
253
+ initial: ReactNode;
254
+ /** Workspace name, truncated in the row. */
255
+ name: ReactNode;
256
+ };
182
257
 
183
- /** The workspace switch button: 28px ink tile, name and up-down caret. Highlights while its menu is open. */
258
+ /**
259
+ * The workspace switch button: 28px ink tile, name and up-down caret. Highlights while its menu is open.
260
+ *
261
+ * Part of {@link Sidebar}.
262
+ */
184
263
  export function SidebarWorkspaceSwitch({ initial, name, className, ...props }: SidebarWorkspaceSwitchProps) {
185
264
  return (
186
265
  <button
@@ -201,7 +280,12 @@ export function SidebarWorkspaceSwitch({ initial, name, className, ...props }: S
201
280
  );
202
281
  }
203
282
 
204
- /** Header: workspace switch (tile, name, up-down caret) and an optional 28px collapse button. 10px gap below. */
283
+ /**
284
+ * Header: workspace switch (tile, name, up-down caret) and an optional 28px collapse button. 10px gap below.
285
+ *
286
+ * Part of {@link Sidebar}. Pass `switchSlot` when the switch must open a menu; otherwise the built-in
287
+ * `SidebarWorkspaceSwitch` is rendered with `switchProps` spread onto it.
288
+ */
205
289
  export function SidebarHeader({
206
290
  initial,
207
291
  name,
@@ -228,25 +312,41 @@ export function SidebarHeader({
228
312
  );
229
313
  }
230
314
 
315
+ /**
316
+ * Props every sidebar row shares, whichever element it renders as.
317
+ */
231
318
  type SidebarItemCommonProps = {
232
319
  /** 18px icon, or a `SidebarProjectTile`. */
233
320
  icon?: ReactNode;
234
321
  /** Count on the right, such as unread items. */
235
322
  count?: ReactNode;
236
- /** Trailing 7px red dot for attention. */
323
+ /** Trailing 7px red dot marking the row for attention; the dot itself carries `aria-label="Needs attention"` but no role. */
237
324
  attention?: boolean;
325
+ /** Marks the active row: 6% ink fill and `aria-current="page"`. */
238
326
  current?: boolean;
327
+ /** Classes merged onto the row. */
239
328
  className?: string;
240
329
  };
241
330
 
242
- /** With `href`, the row is a real link and takes anchor props; without it, a `type="button"` button. */
331
+ /**
332
+ * With `href`, the row is a real link and takes anchor props; without it, a `type="button"` button.
333
+ * The `current` flag adds `aria-current="page"` to either.
334
+ */
243
335
  export type SidebarItemProps =
244
- | (SidebarItemCommonProps & { href: string } & Omit<ComponentProps<"a">, "className" | "href" | "type">)
245
- | (SidebarItemCommonProps & { href?: undefined } & ButtonProps);
336
+ | (SidebarItemCommonProps & {
337
+ /** Destination; setting it switches the row to an anchor. */
338
+ href: string;
339
+ } & Omit<ComponentProps<"a">, "className" | "href" | "type">)
340
+ | (SidebarItemCommonProps & {
341
+ /** Leave it out to render a button instead of a link. */
342
+ href?: undefined;
343
+ } & ButtonProps);
246
344
 
247
345
  /**
248
346
  * Figma sidebar `Item`: 32px row, 12px radius, 10px gap; the current item is 6% ink.
249
347
  * Pass `href` to render an `<a>` with the same look.
348
+ *
349
+ * Part of {@link Sidebar}.
250
350
  */
251
351
  export function SidebarItem({ icon, count, attention, current, className, children, ...props }: SidebarItemProps) {
252
352
  const classes = cn(
@@ -286,11 +386,18 @@ export function SidebarItem({ icon, count, attention, current, className, childr
286
386
  );
287
387
  }
288
388
 
289
- /** A 22px rounded tile with a 14px inverse icon, for project rows. Set the fill with `className`. */
389
+ /**
390
+ * A 22px rounded tile with a 14px inverse icon, for project rows. Set the fill with `className`.
391
+ *
392
+ * Part of {@link Sidebar}.
393
+ */
290
394
  export function SidebarProjectTile({
291
395
  className,
292
396
  ...props
293
- }: Omit<ComponentProps<"span">, "className"> & { className?: string }) {
397
+ }: Omit<ComponentProps<"span">, "className"> & {
398
+ /** Classes merged onto the tile; the fill color comes from here. */
399
+ className?: string;
400
+ }) {
294
401
  return (
295
402
  <span
296
403
  className={cn(
@@ -302,14 +409,29 @@ export function SidebarProjectTile({
302
409
  );
303
410
  }
304
411
 
412
+ /**
413
+ * Props for {@link SidebarGroup}.
414
+ */
305
415
  export type SidebarGroupProps = {
416
+ /** Group caption; also the collapsible trigger's label. */
306
417
  label: ReactNode;
418
+ /**
419
+ * Whether the group starts expanded.
420
+ * @default true
421
+ */
307
422
  defaultOpen?: boolean;
423
+ /** Rows or projects inside the group. */
308
424
  children: ReactNode;
425
+ /** Classes merged onto the group. */
309
426
  className?: string;
310
427
  };
311
428
 
312
- /** A collapsible group: 28px label in secondary text with a 14px caret, 14px above. */
429
+ /**
430
+ * A collapsible group: 28px label in secondary text with a 14px caret, 14px above.
431
+ *
432
+ * Part of {@link Sidebar}. The group is uncontrolled after mount; the caret rotates from pointing
433
+ * right (collapsed) to down (open) and the panel height animates.
434
+ */
313
435
  export function SidebarGroup({ label, defaultOpen = true, children, className }: SidebarGroupProps) {
314
436
  return (
315
437
  <Collapsible.Root defaultOpen={defaultOpen} className={cn("relative mt-3.5 flex flex-col gap-0.5", className)}>
@@ -340,8 +462,19 @@ function branchesPath(count: number): string {
340
462
  return [trunk, ...branches].join("");
341
463
  }
342
464
 
343
- /** Figma `Tree`: nested sidebar items with 16px icons, 20px in, joined to the parent by curved connectors. */
344
- export function SidebarTree({ children, className }: { children: ReactNode; className?: string }) {
465
+ /**
466
+ * Figma `Tree`: nested sidebar items with 16px icons, 20px in, joined to the parent by curved connectors.
467
+ *
468
+ * Part of {@link Sidebar}. The connector is one SVG path computed from the child count, so it
469
+ * assumes 32px rows with a 2px gap; pass the child rows directly rather than wrapping them.
470
+ */
471
+ // prettier-ignore
472
+ export function SidebarTree({ children, className }: {
473
+ /** Child rows; their count and order decide the connector shape. */
474
+ children: ReactNode;
475
+ /** Classes merged onto the tree wrapper. */
476
+ className?: string;
477
+ }) {
345
478
  const count = Children.count(children);
346
479
  const height = ROW / 2 + (count - 1) * (ROW + GAP) + 0.75;
347
480
  return (
@@ -361,18 +494,33 @@ export function SidebarTree({ children, className }: { children: ReactNode; clas
361
494
  );
362
495
  }
363
496
 
364
- /** Pushes the account card to the bottom. */
497
+ /**
498
+ * Pushes the account card to the bottom.
499
+ *
500
+ * Part of {@link Sidebar}; it grows to fill the remaining column height.
501
+ */
365
502
  export function SidebarSpacer() {
366
503
  return <div aria-hidden className="min-h-px flex-1" />;
367
504
  }
368
505
 
506
+ /**
507
+ * Props for {@link SidebarAccount}: every button prop plus the identity lines it shows.
508
+ */
369
509
  export type SidebarAccountProps = ButtonProps & {
510
+ /** Initials on the 36px sky avatar tile. */
370
511
  initials: ReactNode;
512
+ /** Account name. */
371
513
  name: ReactNode;
514
+ /** Email line under the name. */
372
515
  email?: ReactNode;
373
516
  };
374
517
 
375
- /** Figma sidebar `Account`: a 54px row with a 36px sky avatar tile and an up-down caret. */
518
+ /**
519
+ * Figma sidebar `Account`: a 54px row with a 36px sky avatar tile and an up-down caret.
520
+ *
521
+ * Part of {@link Sidebar}. The row is a button, so pair it with a menu when it should open an
522
+ * account switcher.
523
+ */
376
524
  export function SidebarAccount({ initials, name, email, className, ...props }: SidebarAccountProps) {
377
525
  return (
378
526
  <button
@@ -396,13 +544,24 @@ export function SidebarAccount({ initials, name, email, className, ...props }: S
396
544
  );
397
545
  }
398
546
 
547
+ /**
548
+ * Props for {@link SidebarProject}.
549
+ */
399
550
  export type SidebarProjectProps = {
400
551
  /** A `SidebarProjectTile`. */
401
552
  icon: ReactNode;
553
+ /** Project name; truncated in the row. */
402
554
  label: ReactNode;
555
+ /** Marks the active project: 6% ink row and `aria-current="page"`. */
403
556
  current?: boolean;
557
+ /**
558
+ * Whether the project starts expanded in uncontrolled mode.
559
+ * @default false
560
+ */
404
561
  defaultOpen?: boolean;
562
+ /** Expanded state in controlled mode; pass `onOpenChange` with it. */
405
563
  open?: boolean;
564
+ /** Fires with the next expanded state when the row is toggled. */
406
565
  onOpenChange?: (open: boolean) => void;
407
566
  /**
408
567
  * The project's "More" control, such as a `MenuTrigger` whose render target is
@@ -411,6 +570,7 @@ export type SidebarProjectProps = {
411
570
  actions?: ReactNode;
412
571
  /** Chats and documents in the project, shown in a `SidebarTree` when open. */
413
572
  children?: ReactNode;
573
+ /** Classes merged onto the project block. */
414
574
  className?: string;
415
575
  };
416
576
 
@@ -418,6 +578,10 @@ export type SidebarProjectProps = {
418
578
  * Figma sidebar project row (88:848, Menu=Project 314:12407): tile, name and a
419
579
  * 14px chevron (down when open). An open project shows the 16px "More" button in
420
580
  * place of the chevron, on a 6% ink row; a collapsed project keeps the chevron.
581
+ *
582
+ * Part of {@link Sidebar}. The expanded state works uncontrolled (`defaultOpen`) or controlled
583
+ * (`open` + `onOpenChange`). A project without children renders no panel and no chevron; the
584
+ * trigger still toggles the open state, and any `actions` stay visible.
421
585
  */
422
586
  export function SidebarProject({
423
587
  icon,
@@ -481,9 +645,23 @@ export function SidebarProject({
481
645
  );
482
646
  }
483
647
 
484
- export type SidebarProjectMoreProps = ButtonProps & { label?: string };
648
+ /**
649
+ * Props for {@link SidebarProjectMore}: every button prop plus the accessible label.
650
+ */
651
+ export type SidebarProjectMoreProps = ButtonProps & {
652
+ /**
653
+ * Accessible name of the button.
654
+ * @default "Project actions"
655
+ */
656
+ label?: string;
657
+ };
485
658
 
486
- /** The 16px "More" button of a project row. */
659
+ /**
660
+ * The 16px "More" button of a project row.
661
+ *
662
+ * Part of {@link Sidebar}; its tap target is widened to 44px by a pseudo-element so the small glyph
663
+ * stays easy to hit.
664
+ */
487
665
  export function SidebarProjectMore({ label = "Project actions", className, ...props }: SidebarProjectMoreProps) {
488
666
  return (
489
667
  <button
@@ -500,15 +678,24 @@ export function SidebarProjectMore({ label = "Project actions", className, ...pr
500
678
  );
501
679
  }
502
680
 
503
- export type SidebarAddButtonProps = ButtonProps & { icon?: ReactNode };
681
+ /**
682
+ * Props for {@link SidebarAddButton}: every button prop plus the trailing icon.
683
+ */
684
+ export type SidebarAddButtonProps = ButtonProps & {
685
+ /** Icon after the label; defaults to a 16px plus circle. @default a 16px plus-circle (`PlusCircle`) icon */
686
+ icon?: ReactNode;
687
+ };
688
+
689
+ /** Default trailing icon of {@link SidebarAddButton}. */
690
+ const addIcon = <PlusCircle />;
504
691
 
505
692
  /**
506
693
  * Figma `Add project`: a 36px button with a 1px inside dashed border (4/3 dash,
507
694
  * 20% ink), label then a 16px plus-circle, 12px above. The border is an SVG rect
508
695
  * so the dash matches; its 2px stroke is clipped to the inner 1px.
696
+ *
697
+ * Part of {@link Sidebar}.
509
698
  */
510
- const addIcon = <PlusCircle />;
511
-
512
699
  export function SidebarAddButton({ icon = addIcon, className, children, ...props }: SidebarAddButtonProps) {
513
700
  return (
514
701
  <button
@@ -539,4 +726,8 @@ export function SidebarAddButton({ icon = addIcon, className, children, ...props
539
726
  );
540
727
  }
541
728
 
729
+ /**
730
+ * A `div` prop set with an optional `className`, exported so callers can type their own sidebar
731
+ * pieces the same way the internal parts do.
732
+ */
542
733
  export type { DivProps as SidebarDivProps };
@@ -3,13 +3,39 @@ import type { ComponentProps } from "react";
3
3
  import { cn } from "../lib/cn";
4
4
  import { LiquidGlass } from "./liquid-glass";
5
5
 
6
+ /**
7
+ * Props for {@link Skeleton}. Other `div` props (`style`, `data-*`) land on the placeholder.
8
+ */
6
9
  export type SkeletonProps = Omit<ComponentProps<"div">, "className"> & {
7
- /** `block` has the Figma 6px radius, `pill` is fully round (avatars, buttons). */
10
+ /**
11
+ * `block` has the Figma 6px radius, `pill` is fully round (avatars, buttons).
12
+ * @default "block"
13
+ */
8
14
  shape?: "block" | "pill";
15
+ /** Classes that give the placeholder its size, such as `h-2 w-40`. */
9
16
  className?: string;
10
17
  };
11
18
 
12
- /** A 7% ink placeholder; size it with classes. */
19
+ /**
20
+ * A 7% ink placeholder; size it with classes.
21
+ *
22
+ * The block is `aria-hidden` and shimmers with a translucent highlight, so it shows layout while
23
+ * content loads without being read out.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { Skeleton } from "@horon-ai/react";
28
+ *
29
+ * function Example() {
30
+ * return (
31
+ * <div className="flex items-center gap-3">
32
+ * <Skeleton shape="pill" className="size-9" />
33
+ * <Skeleton className="h-3 w-48" />
34
+ * </div>
35
+ * );
36
+ * }
37
+ * ```
38
+ */
13
39
  export function Skeleton({ shape = "block", className, ...props }: SkeletonProps) {
14
40
  return (
15
41
  <div
@@ -24,14 +50,45 @@ export function Skeleton({ shape = "block", className, ...props }: SkeletonProps
24
50
  );
25
51
  }
26
52
 
53
+ /**
54
+ * Props for {@link SkeletonCard}.
55
+ */
27
56
  export type SkeletonCardProps = {
57
+ /**
58
+ * Placeholder arrangement: `row` an avatar with two lines, `card` a heading, three lines and a
59
+ * button, `text` three lines only.
60
+ * @default "row"
61
+ */
28
62
  type?: "row" | "card" | "text";
29
- /** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. */
63
+ /**
64
+ * `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card.
65
+ * @default "solid"
66
+ */
30
67
  surface?: "solid" | "glass";
68
+ /** Classes merged onto the card. */
31
69
  className?: string;
32
70
  };
33
71
 
34
- /** Figma `Skeleton` (in Loading 109:733): Row, Card and Text placeholders on a solid or thick-glass card. */
72
+ /**
73
+ * Figma `Skeleton` (in Loading 109:733): Row, Card and Text placeholders on a solid or thick-glass card.
74
+ *
75
+ * Use it while a list, a detail card or a paragraph loads. The card announces itself once with
76
+ * `role="status"` and the label "Loading"; the individual bars stay hidden from assistive tech.
77
+ *
78
+ * @example
79
+ * ```tsx
80
+ * import { SkeletonCard } from "@horon-ai/react";
81
+ *
82
+ * function Example() {
83
+ * return (
84
+ * <div className="flex flex-col gap-3">
85
+ * <SkeletonCard />
86
+ * <SkeletonCard type="text" />
87
+ * </div>
88
+ * );
89
+ * }
90
+ * ```
91
+ */
35
92
  export function SkeletonCard({ type = "row", surface = "solid", className }: SkeletonCardProps) {
36
93
  return (
37
94
  <div
@@ -3,14 +3,44 @@ import type { ComponentProps } from "react";
3
3
 
4
4
  import { cn } from "../lib/cn";
5
5
 
6
+ /**
7
+ * Props of {@link Slider}: every Base UI `Slider.Root` prop (`value`, `defaultValue`,
8
+ * `onValueChange`, `min`, `max`, `step`, `disabled`, `orientation`) plus the two overrides below.
9
+ */
6
10
  export type SliderProps = Omit<ComponentProps<typeof BaseSlider.Root>, "className" | "children"> & {
11
+ /** Extra classes for the root, which is 220px wide (`w-55`) and caps at its container by default. */
7
12
  className?: string;
13
+ /** Accessible name of the thumb, the only focusable part of the control. */
8
14
  "aria-label"?: string;
9
15
  };
10
16
 
11
17
  /**
12
18
  * Paper settings slider (Voice / Speed): a 20px control, 6px track at 8% ink with an ink fill,
13
19
  * and an 18px white thumb with a 1px hairline and a 0 1 3 12% shadow. Width comes from the caller.
20
+ *
21
+ * Wraps Base UI's slider: the whole 20px row is draggable and clickable, the thumb is the
22
+ * focusable element (focus ring on `text-brand`, it scales up on hover and press), and a
23
+ * disabled slider dims to 40% with a not-allowed cursor.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { useState } from "react";
28
+ * import { Slider } from "@horon-ai/react";
29
+ *
30
+ * function VoiceSpeed() {
31
+ * const [speed, setSpeed] = useState(1);
32
+ * return (
33
+ * <Slider
34
+ * aria-label="Voice speed"
35
+ * min={0.5}
36
+ * max={2}
37
+ * step={0.1}
38
+ * value={speed}
39
+ * onValueChange={(next) => setSpeed(next as number)}
40
+ * />
41
+ * );
42
+ * }
43
+ * ```
14
44
  */
15
45
  export function Slider({ className, "aria-label": ariaLabel, ...props }: SliderProps) {
16
46
  return (