@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
@@ -6,26 +6,56 @@ import { BreadcrumbItem } from "./breadcrumb";
6
6
  import { IconButton } from "./icon-button";
7
7
  import { LiquidGlass } from "./liquid-glass";
8
8
 
9
+ /** One crumb of {@link BreadcrumbHeader}; the last crumb in `crumbs` is the current page. */
9
10
  export type BreadcrumbHeaderCrumb = {
11
+ /** Crumb text; also its React key, so crumbs should have distinct labels. */
10
12
  label: string;
13
+ /** Makes the crumb a link; ignored for the last (current) crumb, which stays text. */
11
14
  href?: string;
12
15
  /** Leading 20px ink tile, such as the workspace initial. */
13
16
  tile?: ReactNode;
14
17
  };
15
18
 
19
+ /** Props of {@link BreadcrumbHeader}. */
16
20
  export type BreadcrumbHeaderProps = Omit<ComponentProps<"header">, "className"> & {
17
21
  /** Crumbs in order; the last one is the current page. */
18
22
  crumbs: BreadcrumbHeaderCrumb[];
23
+ /** Adds the search icon button; without it the button is not rendered at all. */
19
24
  onSearch?: () => void;
25
+ /** Adds the "more" (three dots) icon button; without it the button is not rendered at all. */
20
26
  onMore?: () => void;
27
+ /** Accessible label of the search button. @default "Search" */
21
28
  searchLabel?: string;
29
+ /** Accessible label of the more button. @default "More" */
22
30
  moreLabel?: string;
23
- /** `solid` (default) on the page, `glass` over imagery and glass panels. */
31
+ /** `solid` (default) on the page, `glass` over imagery and glass panels. @default "solid" */
24
32
  surface?: "solid" | "glass";
33
+ /** Extra classes merged onto the header. */
25
34
  className?: string;
26
35
  };
27
36
 
28
- /** Paper `Header with breadcrumb`: a 52px regular-glass pill with a crumb row, search and more buttons. */
37
+ /**
38
+ * Paper `Header with breadcrumb`: a 52px regular-glass pill with a crumb row, search and more buttons.
39
+ *
40
+ * The crumb trail scrolls sideways inside the pill instead of truncating; only the last crumb truncates.
41
+ * The two icon buttons (36px) are only rendered when their handlers are passed. On `glass` a
42
+ * {@link LiquidGlass} layer sits under the content, so the header must be placed over imagery.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * import { BreadcrumbHeader } from "@horon-ai/react";
47
+ *
48
+ * function Example() {
49
+ * return (
50
+ * <BreadcrumbHeader
51
+ * crumbs={[{ label: "H", tile: "H", href: "/" }, { label: "Projects", href: "/projects" }, { label: "Horon" }]}
52
+ * onSearch={() => {}}
53
+ * onMore={() => {}}
54
+ * />
55
+ * );
56
+ * }
57
+ * ```
58
+ */
29
59
  export function BreadcrumbHeader({
30
60
  crumbs,
31
61
  onSearch,
@@ -4,15 +4,39 @@ import { Children, Fragment, type ComponentProps, type ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { LiquidGlass } from "./liquid-glass";
6
6
 
7
+ /** Tray material behind a {@link Breadcrumb} trail: `glass` is the regular glass tray, `solid` the opaque one. */
7
8
  export type BreadcrumbSurface = "glass" | "solid";
8
9
 
10
+ /** Props of {@link Breadcrumb}. */
9
11
  export type BreadcrumbProps = Omit<ComponentProps<"nav">, "className"> & {
10
12
  /** `glass` wraps the crumbs in a regular-glass tray, `solid` in an opaque tray. Omitted keeps them bare. */
11
13
  surface?: BreadcrumbSurface;
14
+ /** Extra classes merged onto the navigation element. */
12
15
  className?: string;
13
16
  };
14
17
 
15
- /** Figma `Breadcrumb` (110:948): crumbs 6px apart with 14px carets; the last crumb is the current page. */
18
+ /**
19
+ * Figma `Breadcrumb` (110:948): crumbs 6px apart with 14px carets; the last crumb is the current page.
20
+ *
21
+ * Renders a `<nav aria-label>` (default `"Breadcrumb"`) holding an ordered list, and inserts an
22
+ * `aria-hidden` caret between entries. The tray is a grid box so it can shrink below its trail's
23
+ * min-content; an over-wide trail scrolls inside it rather than truncating labels.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { Breadcrumb, BreadcrumbItem } from "@horon-ai/react";
28
+ *
29
+ * function Example() {
30
+ * return (
31
+ * <Breadcrumb surface="solid">
32
+ * <BreadcrumbItem href="/" tile="H">Home</BreadcrumbItem>
33
+ * <BreadcrumbItem href="/projects">Projects</BreadcrumbItem>
34
+ * <BreadcrumbItem current>Horon</BreadcrumbItem>
35
+ * </Breadcrumb>
36
+ * );
37
+ * }
38
+ * ```
39
+ */
16
40
  export function Breadcrumb({
17
41
  surface,
18
42
  className,
@@ -53,14 +77,20 @@ export function Breadcrumb({
53
77
  );
54
78
  }
55
79
 
80
+ /** Props of {@link BreadcrumbItem}. */
56
81
  export type BreadcrumbItemProps = Omit<ComponentProps<"a">, "className"> & {
57
82
  /** Marks the current page: primary text and `aria-current`. */
58
83
  current?: boolean;
59
84
  /** Leading 20px ink tile, such as the workspace initial. */
60
85
  tile?: ReactNode;
86
+ /** Extra classes merged onto the crumb element. */
61
87
  className?: string;
62
88
  };
63
89
 
90
+ /**
91
+ * One crumb of {@link Breadcrumb}: an anchor when `href` is set and the crumb is not current,
92
+ * otherwise a `<span>`. The current crumb renders primary text with `aria-current="page"`.
93
+ */
64
94
  export function BreadcrumbItem({ current, tile, href, className, children, ...props }: BreadcrumbItemProps) {
65
95
  const Element = href && !current ? "a" : "span";
66
96
  return (
@@ -4,14 +4,37 @@ import { Children, isValidElement, type ComponentProps, type ReactNode } from "r
4
4
  import { cn } from "../lib/cn";
5
5
  import { LiquidGlass } from "./liquid-glass";
6
6
 
7
+ /**
8
+ * Visual treatment of {@link Button}.
9
+ *
10
+ * - `primary` — filled brand button, for the single main action of a screen or dialog.
11
+ * - `glass` — clear glass fill with a {@link LiquidGlass} layer, for chrome floating over imagery.
12
+ * - `solid` — bordered surface button, for secondary actions on a page.
13
+ * - `danger` — filled destructive action, such as delete.
14
+ * - `link` — size-independent text-only button; its `aria-current="page"` / `aria-current="true"` state
15
+ * colors it like primary text, which is how an active navigation link is marked.
16
+ */
7
17
  export type ButtonVariant = "primary" | "glass" | "solid" | "danger" | "link";
18
+ /**
19
+ * Height and text rung of a non-`link` {@link Button}, identical to {@link ControlSize}:
20
+ * `sm` 32px `label-md`, `md` 40px `label-lg`, `lg` 48px `label-lg`.
21
+ */
8
22
  export type ButtonSize = "sm" | "md" | "lg";
9
23
 
10
24
  type ButtonCommonProps = {
25
+ /**
26
+ * Visual treatment; `link` ignores the size rungs.
27
+ * @default "primary"
28
+ */
11
29
  variant?: ButtonVariant;
30
+ /**
31
+ * Height and label rung of the control; `link` stays text-only.
32
+ * @default "md"
33
+ */
12
34
  size?: ButtonSize;
13
35
  /** Leading icon; sized 16px (sm, md) or 20px (lg) and colored like the label. */
14
36
  icon?: ReactNode;
37
+ /** Extra classes appended last, so a caller can add width, margins or overrides. */
15
38
  className?: string;
16
39
  };
17
40
 
@@ -49,7 +72,29 @@ const variantClass: Record<ButtonVariant, string> = {
49
72
  link: "h-auto gap-1.5 rounded-sm px-1 text-label-sm text-text-secondary motion-colors hover:not-data-disabled:text-text-primary aria-[current=page]:text-text-primary aria-[current=true]:text-text-primary [&_svg]:size-3.5",
50
73
  };
51
74
 
52
- /** Figma `Button` (55:339): Primary, Glass, Solid and Danger in three sizes, plus a text-only `link` variant (size-independent). */
75
+ /**
76
+ * Figma `Button` (55:339): Primary, Glass, Solid and Danger in three sizes, plus a text-only `link` variant (size-independent).
77
+ *
78
+ * Renders a Base UI button, or a real `<a>` once `href` is set (button-only attributes such as `type`,
79
+ * `disabled` and `form*` are then dropped instead of reaching the DOM). Sizes `sm` and `md` paint an
80
+ * invisible 44px hit area around the 32/40px control without moving it. A non-`link` button also accepts
81
+ * an `icon` before the label and, on `glass`, a {@link LiquidGlass} layer beneath its content.
82
+ *
83
+ * @example
84
+ * ```tsx
85
+ * import { Button } from "@horon-ai/react";
86
+ *
87
+ * function Example() {
88
+ * return (
89
+ * <div className="flex items-center gap-2">
90
+ * <Button onClick={() => save()}>Save changes</Button>
91
+ * <Button variant="solid" size="sm">Cancel</Button>
92
+ * <Button variant="link" href="/settings">Settings</Button>
93
+ * </div>
94
+ * );
95
+ * }
96
+ * ```
97
+ */
53
98
  export function Button({
54
99
  variant = "primary",
55
100
  size = "md",
@@ -4,31 +4,52 @@ import { useMemo, useRef, useState, type KeyboardEvent } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import type { ControlSize } from "../lib/size";
6
6
 
7
- export type DateRange = { start: Date | null; end: Date | null };
7
+ /**
8
+ * An inclusive date range for {@link Calendar} in `range` mode: both `start` and `end` are selected days,
9
+ * only those two endpoints are stored and the days between them are derived on render. Both ends are
10
+ * `null` before the first click, and `end` stays `null` while the user is picking the second day.
11
+ */
12
+ export type DateRange = {
13
+ /** First selected day, or `null` when the range is empty. */
14
+ start: Date | null;
15
+ /** Last selected day; stays `null` until the second click completes the range. */
16
+ end: Date | null;
17
+ };
18
+ /** Day-cell rung of {@link Calendar}; identical to {@link ControlSize} (`sm` 32px, `md` 40px, `lg` 48px cells). */
8
19
  export type CalendarSize = ControlSize;
9
20
 
10
21
  type CommonProps = {
11
22
  /** First visible month; defaults to the selected date or today. */
12
23
  defaultMonth?: Date;
24
+ /**
25
+ * Controlled visible month. While set, the arrows and keyboard month changes only report
26
+ * through `onMonthChange` and the component keeps no month state of its own.
27
+ */
13
28
  month?: Date;
29
+ /** Fires with the first day of the month the user navigated to (arrows, or keyboard focus crossing a boundary). */
14
30
  onMonthChange?: (month: Date) => void;
31
+ /** Days for which this returns true render disabled and cannot be selected. */
15
32
  isDateDisabled?: (date: Date) => boolean;
16
33
  /** BCP 47 locale for the month title, weekdays and day labels. */
17
34
  locale?: string;
18
- /** 0 = Sunday (the Figma calendar), 1 = Monday. */
35
+ /** 0 = Sunday (the Figma calendar), 1 = Monday. @default 0 */
19
36
  weekStartsOn?: 0 | 1;
37
+ /** Extra classes merged onto the calendar frame. */
20
38
  className?: string;
39
+ /** The day drawn as today, with the brand text color and the live dot; defaults to the real date at mount. */
21
40
  today?: Date;
22
41
  /**
23
42
  * `default` is the day picker calendar (60:245): centered title-sm month.
24
43
  * `popover` is the `Calendar popover` (88:495): 24px radius, left label-lg
25
44
  * month and 32px round arrow buttons on the right.
45
+ * @default "default"
26
46
  */
27
47
  variant?: "default" | "popover";
28
48
  /**
29
49
  * `solid` (default) is an opaque surface with a default border. `glass` puts it on the veil (`veil-overlay`),
30
50
  * the material of the `DateField` popup; `flat` keeps the 80% white fill of the old thick glass
31
51
  * without it (Paper Schedule mini calendar).
52
+ * @default "solid"
32
53
  */
33
54
  surface?: "glass" | "flat" | "solid";
34
55
  /** Days drawn as subtle ink circles in the day cell size, e.g. the week the schedule shows; selection still draws on top. */
@@ -36,11 +57,20 @@ type CommonProps = {
36
57
  /**
37
58
  * Day cell size: `sm` 32px, `md` 40px (default, the Figma size), `lg` 48px.
38
59
  * Scales the header, padding, weekday row and day text with the cells.
60
+ * @default "md"
39
61
  */
40
62
  size?: CalendarSize;
41
63
  };
42
64
 
43
- const containerSizeClass: Record<CalendarSize, { default: string; popover: string }> = {
65
+ const containerSizeClass: Record<
66
+ CalendarSize,
67
+ {
68
+ /** Classes for the `default` variant: fills the parent up to the Figma width. */
69
+ default: string;
70
+ /** Classes for the `popover` variant: fixed Figma width, capped by the viewport. */
71
+ popover: string;
72
+ }
73
+ > = {
44
74
  // The default calendar fills its parent up to the Figma width (250/314/374px: seven cells plus padding),
45
75
  // so it lands on that size on desktop and folds to the column on a narrow screen. `w-full` rather than a
46
76
  // fixed `w-*` keeps its min-content at zero, which is what lets a flex parent shrink it. The popover is
@@ -140,10 +170,31 @@ const popoverDayTextClass: Record<CalendarSize, string> = {
140
170
  lg: "[&_[role=gridcell]]:text-label-lg",
141
171
  };
142
172
 
173
+ /**
174
+ * Props of {@link Calendar}. `mode` switches the value shape: `single` (the default) works with one
175
+ * `Date`, `range` with a {@link DateRange}; the callback always receives the same shape.
176
+ */
143
177
  export type CalendarProps = CommonProps &
144
178
  (
145
- | { mode?: "single"; value?: Date | null; onValueChange?: (value: Date) => void }
146
- | { mode: "range"; value?: DateRange; onValueChange?: (value: DateRange) => void }
179
+ | {
180
+ /** Pick one day. @default "single" */
181
+ mode?: "single";
182
+ /** Selected day, or `null` when nothing is selected. */
183
+ value?: Date | null;
184
+ /** Fires when a day is clicked, with the newly selected day. */
185
+ onValueChange?: (value: Date) => void;
186
+ }
187
+ | {
188
+ /** Pick a start and an end day. */
189
+ mode: "range";
190
+ /** Current range; omitted means both ends are empty. */
191
+ value?: DateRange;
192
+ /**
193
+ * Fires on every click: the first click sets `start` and clears `end`, the second sets `end`,
194
+ * and a click before `start` restarts the range at that day.
195
+ */
196
+ onValueChange?: (value: DateRange) => void;
197
+ }
147
198
  );
148
199
 
149
200
  const startOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
@@ -162,7 +213,26 @@ function monthGrid(month: Date, weekStartsOn: 0 | 1): Date[] {
162
213
  return Array.from({ length: weeks * 7 }, (_, index) => addDays(start, index));
163
214
  }
164
215
 
165
- /** Figma `Day` (60:125) inside the `Calendar` of the day picker example (60:245). */
216
+ /**
217
+ * Figma `Day` (60:125) inside the `Calendar` of the day picker example (60:245).
218
+ *
219
+ * A one-month grid with `grid`/`row`/`gridcell` roles and an `aria-label` on every day. The month starts
220
+ * from `defaultMonth`, else the selected day, else today, and slides between months with the arrow
221
+ * buttons; `month` makes that controlled. Arrow keys move focus by a day, a week vertically, and bring
222
+ * the focused month into view; only the focused day is tabbable. The variant controls the frame
223
+ * (`default` centered header, `popover` left title with round arrows) and `surface` its material.
224
+ *
225
+ * @example
226
+ * ```tsx
227
+ * import { useState } from "react";
228
+ * import { Calendar, type DateRange } from "@horon-ai/react";
229
+ *
230
+ * function Example() {
231
+ * const [range, setRange] = useState<DateRange>({ start: null, end: null });
232
+ * return <Calendar mode="range" value={range} onValueChange={setRange} weekStartsOn={1} />;
233
+ * }
234
+ * ```
235
+ */
166
236
  export function Calendar(props: CalendarProps) {
167
237
  const {
168
238
  defaultMonth,
@@ -5,8 +5,11 @@ import type { ComponentProps, ReactElement, ReactNode } from "react";
5
5
  import { cn } from "../lib/cn";
6
6
  import { LiquidGlass } from "./liquid-glass";
7
7
 
8
+ /** Props of {@link Card}. */
8
9
  export type CardProps = Omit<ComponentProps<"div">, "className" | "title"> & {
10
+ /** Card heading on the title row, wrapping inside the card instead of truncating; the caret joins this row. */
9
11
  title?: ReactNode;
12
+ /** Body line under the title, in body-sm secondary text. */
10
13
  description?: ReactNode;
11
14
  /** Shows the trailing caret, for cards that open something. */
12
15
  interactive?: boolean;
@@ -17,12 +20,38 @@ export type CardProps = Omit<ComponentProps<"div">, "className" | "title"> & {
17
20
  /**
18
21
  * `solid` (default) is an opaque surface with a default border. `glass` is the thick glass.
19
22
  * `frost` is the Paper home board card: the same glass with the denser frost fill and a 0 8 24 6% drop.
23
+ * @default "solid"
20
24
  */
21
25
  surface?: "glass" | "frost" | "solid";
26
+ /** Extra classes merged onto the rendered root. */
22
27
  className?: string;
23
28
  };
24
29
 
25
- /** Figma `Card` (63:310): a 320px thick-glass card, 20px padding and radius, Default / Interactive / Selected. */
30
+ /**
31
+ * Figma `Card` (63:310): a 320px thick-glass card, 20px padding and radius, Default / Interactive / Selected.
32
+ *
33
+ * `render` swaps the root element through Base UI's `useRender` (e.g. `<button />` or `<a />`); the glass
34
+ * layer is isolated behind the content, so the card never needs a stacking-context patch from the caller.
35
+ * `interactive` adds the caret, hover rim and press scale, `selected` the brand rim
36
+ * (`[data-selected]` is set too), and children render in a full-width block under the description.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * import { Card } from "@horon-ai/react";
41
+ *
42
+ * function Example() {
43
+ * return (
44
+ * <Card
45
+ * interactive
46
+ * surface="frost"
47
+ * title="Weekly digest"
48
+ * description="A summary of everything the assistant saved this week."
49
+ * render={<button type="button" />}
50
+ * />
51
+ * );
52
+ * }
53
+ * ```
54
+ */
26
55
  export function Card({
27
56
  title,
28
57
  description,
@@ -3,13 +3,22 @@ import type { ComponentProps } from "react";
3
3
  import { cn } from "../lib/cn";
4
4
  import type { ControlSize } from "../lib/size";
5
5
 
6
+ /** Padding and text rung of {@link ChatBubble}; identical to {@link ControlSize}. */
6
7
  export type ChatBubbleSize = ControlSize;
7
8
 
9
+ /** Props of {@link ChatBubble}. */
8
10
  export type ChatBubbleProps = Omit<ComponentProps<"div">, "className"> & {
9
- /** `me` is the ink bubble, `bot` the flat subtle bubble. Neither uses glass. */
11
+ /**
12
+ * `me` is the ink bubble, `bot` the flat subtle bubble. Neither uses glass.
13
+ * @default "me"
14
+ */
10
15
  from?: "me" | "bot";
11
- /** Bubble size: `sm` 12/8px padding with body-sm, `md` 16/12px with body-md (default), `lg` 20/14px with body-lg. */
16
+ /**
17
+ * Bubble size: `sm` 12/8px padding with body-sm, `md` 16/12px with body-md (default), `lg` 20/14px with body-lg.
18
+ * @default "md"
19
+ */
12
20
  size?: ChatBubbleSize;
21
+ /** Extra classes merged onto the bubble. */
13
22
  className?: string;
14
23
  };
15
24
 
@@ -19,7 +28,27 @@ const sizeClass: Record<ChatBubbleSize, string> = {
19
28
  lg: "px-5 py-3.5 text-body-lg",
20
29
  };
21
30
 
22
- /** Figma `Chat bubble` (64:359): 16/12px padding, 20px radius, body-md text. */
31
+ /**
32
+ * Figma `Chat bubble` (64:359): 16/12px padding, 20px radius, body-md text.
33
+ *
34
+ * `me` bubbles push to the right edge with the ink fill, `bot` bubbles stay left on the subtle fill;
35
+ * both cap at 560px or the parent's width, wrap long words and rise in with `motion-rise`. The `from`
36
+ * value is mirrored to `data-from` for styling hooks.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * import { ChatBubble } from "@horon-ai/react";
41
+ *
42
+ * function Example() {
43
+ * return (
44
+ * <div className="flex flex-col gap-2">
45
+ * <ChatBubble from="bot">What should I look up?</ChatBubble>
46
+ * <ChatBubble from="me">The Q3 usage report, please.</ChatBubble>
47
+ * </div>
48
+ * );
49
+ * }
50
+ * ```
51
+ */
23
52
  export function ChatBubble({ from = "me", size = "md", className, ...props }: ChatBubbleProps) {
24
53
  return (
25
54
  <div
@@ -4,11 +4,16 @@ import type { ComponentProps, ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { LiquidGlass } from "./liquid-glass";
6
6
 
7
+ /** Material of the chat controls: `solid` on raised surfaces, `glass` over imagery and glass panels. */
7
8
  export type ChatControlSurface = "solid" | "glass";
8
9
 
9
10
  type ButtonProps = Omit<ComponentProps<"button">, "className"> & {
11
+ /** Extra classes merged onto the control button. */
10
12
  className?: string;
11
- /** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. */
13
+ /**
14
+ * `solid` (default) on raised surfaces, `glass` over imagery and glass panels.
15
+ * @default "solid"
16
+ */
12
17
  surface?: ChatControlSurface;
13
18
  };
14
19
 
@@ -20,7 +25,22 @@ const surfaceClass: Record<ChatControlSurface, string> = {
20
25
  solid: "border border-border-default bg-bg-surface hover:bg-bg-subtle active:bg-interactive-secondary-hover",
21
26
  };
22
27
 
23
- /** Figma `Jump to latest` (Chat controls 88:659): a 44px clear-glass circle with a 20px caret. */
28
+ /**
29
+ * Figma `Jump to latest` (Chat controls 88:659): a 44px clear-glass circle with a 20px caret.
30
+ *
31
+ * A square icon button with `aria-label` (default `"Jump to latest"`) that surfaces with the
32
+ * `motion-rise` entrance and forwards `onClick`; the caller's handler performs the scroll to the
33
+ * newest message.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { JumpToLatest } from "@horon-ai/react";
38
+ *
39
+ * function Example() {
40
+ * return <JumpToLatest surface="glass" onClick={() => scrollToLatest()} />;
41
+ * }
42
+ * ```
43
+ */
24
44
  export function JumpToLatest({
25
45
  surface = "solid",
26
46
  className,
@@ -40,7 +60,12 @@ export function JumpToLatest({
40
60
  );
41
61
  }
42
62
 
43
- /** Figma `New messages` (88:659): a 40px clear-glass pill with a 16px caret and a label. */
63
+ /**
64
+ * Figma `New messages` (88:659): a 40px clear-glass pill with a 16px caret and a label.
65
+ *
66
+ * A peer of {@link JumpToLatest} for the "there are unread messages above" case; the child is the label
67
+ * and all button attributes are forwarded.
68
+ */
44
69
  export function NewMessagesPill({ surface = "solid", className, children, ...props }: ButtonProps) {
45
70
  return (
46
71
  <button
@@ -58,21 +83,49 @@ export function NewMessagesPill({ surface = "solid", className, children, ...pro
58
83
  /** Bar heights of the Figma waveform, in px. */
59
84
  const WAVEFORM = [8, 14, 22, 12, 26, 18, 10, 20, 28, 16, 9, 18, 24, 12, 7, 15, 21, 11];
60
85
 
86
+ /** Props of {@link VoiceInput}. */
61
87
  export type VoiceInputProps = {
62
88
  /** Elapsed time, such as "0:07". */
63
89
  elapsed: ReactNode;
64
- /** 0-1 levels for the 18 bars; defaults to the Figma waveform. */
90
+ /** 0-1 levels for the 18 bars; defaults to the Figma waveform. Each level is scaled to 4–28px. */
65
91
  levels?: number[];
66
- /** Animates the waveform while true; set false once recording stops. */
92
+ /**
93
+ * Animates the waveform while true; set false once recording stops.
94
+ * @default true
95
+ */
67
96
  recording?: boolean;
97
+ /** Fires when the red stop button is pressed. */
68
98
  onStop?: () => void;
99
+ /**
100
+ * Accessible label of the stop button.
101
+ * @default "Stop recording"
102
+ */
69
103
  stopLabel?: string;
70
- /** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. */
104
+ /**
105
+ * `solid` (default) on raised surfaces, `glass` over imagery and glass panels.
106
+ * @default "solid"
107
+ */
71
108
  surface?: ChatControlSurface;
109
+ /** Extra classes merged onto the pill. */
72
110
  className?: string;
73
111
  };
74
112
 
75
- /** Figma `Voice input` (88:659): a 52px pill with a red stop button, waveform and timer. */
113
+ /**
114
+ * Figma `Voice input` (88:659): a 52px pill with a red stop button, waveform and timer.
115
+ *
116
+ * The 18 bars keep the Figma heights unless `levels` is passed, in which case each 0-1 level maps to a
117
+ * 4–28px bar. With `recording` the bars run the `wave` animation on a 120ms stagger; the whole waveform
118
+ * is `aria-hidden`, so only the stop button and the elapsed text reach assistive technology.
119
+ *
120
+ * @example
121
+ * ```tsx
122
+ * import { VoiceInput } from "@horon-ai/react";
123
+ *
124
+ * function Example() {
125
+ * return <VoiceInput elapsed="0:07" recording onStop={() => stopRecording()} />;
126
+ * }
127
+ * ```
128
+ */
76
129
  export function VoiceInput({
77
130
  elapsed,
78
131
  levels,
@@ -5,15 +5,20 @@ import type { ComponentProps, ReactNode } from "react";
5
5
  import { cn } from "../lib/cn";
6
6
  import type { ControlSize } from "../lib/size";
7
7
 
8
+ /** Box size of {@link Checkbox}; identical to {@link ControlSize}. */
8
9
  export type CheckboxSize = ControlSize;
9
10
 
11
+ /** Props of {@link Checkbox}. */
10
12
  export type CheckboxProps = Omit<ComponentProps<typeof BaseCheckbox.Root>, "className"> & {
13
+ /** Label rendered after the box inside the same clickable `<label>`. */
11
14
  label?: ReactNode;
12
15
  /**
13
16
  * `md` is the 20px Figma Checkbox, `sm` the 18px box of the Data table (192:9046),
14
17
  * `lg` the 24px box.
18
+ * @default "md"
15
19
  */
16
20
  size?: CheckboxSize;
21
+ /** Extra classes merged onto the label wrapper. */
17
22
  className?: string;
18
23
  };
19
24
 
@@ -36,7 +41,23 @@ const indicatorClass: Record<CheckboxSize, string> = {
36
41
  lg: "[&_svg]:size-4",
37
42
  };
38
43
 
39
- /** Figma `Checkbox` (58:217): a 20px box, 6px radius, with a 10px gap to its label. */
44
+ /**
45
+ * Figma `Checkbox` (58:217): a 20px box, 6px radius, with a 10px gap to its label.
46
+ *
47
+ * A Base UI checkbox: `checked` makes it controlled, otherwise it keeps its own state, and
48
+ * `defaultChecked` / `onCheckedChange` / `indeterminate` work as in Base UI. Checked and indeterminate
49
+ * boxes fill with the brand color and show a check or a dash; the wrapper `<label>` dims the whole control
50
+ * when the input is disabled.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * import { Checkbox } from "@horon-ai/react";
55
+ *
56
+ * function Example() {
57
+ * return <Checkbox label="Remember this chat" defaultChecked />;
58
+ * }
59
+ * ```
60
+ */
40
61
  export function Checkbox({ label, size = "md", className, ...props }: CheckboxProps) {
41
62
  return (
42
63
  <label
@@ -5,11 +5,17 @@ import { Children, type ComponentProps, useState } from "react";
5
5
  import { cn } from "../lib/cn";
6
6
  import type { ControlSize } from "../lib/size";
7
7
 
8
+ /** Height and label rung of {@link Chip}; identical to {@link ControlSize} (`sm` 28px, `md` 32px, `lg` 36px). */
8
9
  export type ChipSize = ControlSize;
9
10
 
11
+ /** Props of {@link Chip}. */
10
12
  export type ChipProps = Omit<ComponentProps<typeof Toggle>, "className"> & {
11
- /** `md` is the 32px Figma Chip; `sm` is 28px and `lg` 36px. */
13
+ /**
14
+ * `md` is the 32px Figma Chip; `sm` is 28px and `lg` 36px.
15
+ * @default "md"
16
+ */
12
17
  size?: ChipSize;
18
+ /** Extra classes merged onto the toggle button. */
13
19
  className?: string;
14
20
  };
15
21
 
@@ -32,7 +38,28 @@ const checkIconClass: Record<ChipSize, string> = {
32
38
  lg: "size-4",
33
39
  };
34
40
 
35
- /** Figma `Chip` (61:302): a 32px solid toggle, no glass; selected shows a check, the brand border and brand text. */
41
+ /**
42
+ * Figma `Chip` (61:302): a 32px solid toggle, no glass; selected shows a check, the brand border and brand text.
43
+ *
44
+ * A Base UI toggle: `pressed` makes it controlled, otherwise it keeps its own state and reports through
45
+ * `onPressedChange`. The check is in a `0`-width slot that opens on press, so an unselected chip never
46
+ * reserves the icon space; the slot only animates after the first toggle, so a chip that mounts pressed
47
+ * does not flash it. An element child is laid out in a row at the label gap.
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * import { Chip } from "@horon-ai/react";
52
+ *
53
+ * function Example() {
54
+ * return (
55
+ * <div className="flex flex-wrap gap-2">
56
+ * <Chip defaultPressed>Documents</Chip>
57
+ * <Chip>Images</Chip>
58
+ * </div>
59
+ * );
60
+ * }
61
+ * ```
62
+ */
36
63
  export function Chip({ size = "md", className, children, onPressedChange, ...props }: ChipProps) {
37
64
  // The exit animation only plays after the first toggle, so a chip that mounts unpressed never flashes the check.
38
65
  const [toggled, setToggled] = useState(false);