@horon-ai/react 0.1.1 → 0.1.2

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 (97) hide show
  1. package/package.json +2 -2
  2. package/src/components/agent-activity.tsx +75 -7
  3. package/src/components/agent-lane.tsx +32 -3
  4. package/src/components/agent-lanes-timeline.tsx +85 -7
  5. package/src/components/agent-panel.tsx +46 -4
  6. package/src/components/agent-steering-panel.tsx +53 -3
  7. package/src/components/approval-card.tsx +34 -2
  8. package/src/components/avatar.tsx +20 -4
  9. package/src/components/badge.tsx +24 -3
  10. package/src/components/banner.tsx +19 -3
  11. package/src/components/bot-instructions-panel.tsx +86 -7
  12. package/src/components/bot-list-row.tsx +13 -1
  13. package/src/components/bot-memory-panel.tsx +58 -3
  14. package/src/components/bottom-sheet.tsx +61 -7
  15. package/src/components/breadcrumb-header.tsx +32 -2
  16. package/src/components/breadcrumb.tsx +31 -1
  17. package/src/components/button.tsx +46 -1
  18. package/src/components/calendar.tsx +76 -6
  19. package/src/components/card.tsx +30 -1
  20. package/src/components/chat-bubble.tsx +32 -3
  21. package/src/components/chat-controls.tsx +60 -7
  22. package/src/components/checkbox.tsx +22 -1
  23. package/src/components/chip.tsx +29 -2
  24. package/src/components/choice-card.tsx +25 -1
  25. package/src/components/code-panel.tsx +28 -3
  26. package/src/components/combobox.tsx +52 -2
  27. package/src/components/command-palette.tsx +65 -1
  28. package/src/components/composer.tsx +28 -1
  29. package/src/components/data-table.tsx +198 -16
  30. package/src/components/date-field.tsx +31 -2
  31. package/src/components/dialog.tsx +45 -3
  32. package/src/components/directory-card.tsx +23 -2
  33. package/src/components/document-extract-panel.tsx +83 -5
  34. package/src/components/empty-state.tsx +21 -2
  35. package/src/components/field.tsx +49 -3
  36. package/src/components/file-drop.tsx +39 -4
  37. package/src/components/filter-bar.tsx +38 -1
  38. package/src/components/flue-background.tsx +88 -4
  39. package/src/components/horon-icon.tsx +50 -3
  40. package/src/components/horon-wordmark.tsx +26 -1
  41. package/src/components/icon-button.tsx +38 -1
  42. package/src/components/input-group.tsx +57 -6
  43. package/src/components/input.tsx +48 -2
  44. package/src/components/integration-row.tsx +43 -2
  45. package/src/components/kbd.tsx +28 -2
  46. package/src/components/knowledge-base-panel.tsx +40 -2
  47. package/src/components/liquid-glass.tsx +16 -0
  48. package/src/components/list-detail.tsx +89 -7
  49. package/src/components/marketplace-card.tsx +40 -4
  50. package/src/components/mcp-server-row.tsx +49 -3
  51. package/src/components/menu.tsx +83 -3
  52. package/src/components/message-extras.tsx +50 -4
  53. package/src/components/modal.tsx +71 -4
  54. package/src/components/orchestrator-panel.tsx +48 -4
  55. package/src/components/page-header.tsx +26 -3
  56. package/src/components/plan-card.tsx +28 -1
  57. package/src/components/plan-step.tsx +41 -1
  58. package/src/components/plugin-row.tsx +61 -3
  59. package/src/components/progress.tsx +32 -2
  60. package/src/components/radio-group.tsx +55 -2
  61. package/src/components/research-run-panel.tsx +73 -3
  62. package/src/components/schedule.tsx +163 -12
  63. package/src/components/segmented-control.tsx +57 -2
  64. package/src/components/select.tsx +60 -0
  65. package/src/components/settings-list.tsx +66 -8
  66. package/src/components/sidebar.tsx +214 -23
  67. package/src/components/skeleton.tsx +61 -4
  68. package/src/components/slider.tsx +30 -0
  69. package/src/components/source-row.tsx +43 -4
  70. package/src/components/spinner.tsx +29 -2
  71. package/src/components/split-button.tsx +37 -3
  72. package/src/components/switch.tsx +31 -1
  73. package/src/components/tab-bar.tsx +47 -2
  74. package/src/components/tabs.tsx +59 -5
  75. package/src/components/task-row.tsx +49 -3
  76. package/src/components/textarea-field.tsx +36 -2
  77. package/src/components/textarea.tsx +35 -2
  78. package/src/components/toast.tsx +52 -3
  79. package/src/components/tool-call.tsx +87 -4
  80. package/src/components/tool-calls-header.tsx +32 -1
  81. package/src/components/tool-permissions.tsx +50 -2
  82. package/src/components/tooltip.tsx +33 -1
  83. package/src/components/top-bar.tsx +35 -2
  84. package/src/components/usage-meter.tsx +28 -2
  85. package/src/components/workspace-menu.tsx +46 -1
  86. package/src/hooks/use-active-indicator.ts +44 -1
  87. package/src/hooks/use-color-scheme.ts +24 -1
  88. package/src/hooks/use-copy-to-clipboard.ts +23 -0
  89. package/src/hooks/use-device.ts +29 -1
  90. package/src/hooks/use-hotkey.ts +21 -0
  91. package/src/hooks/use-media-query.ts +18 -0
  92. package/src/hooks/use-platform.ts +30 -1
  93. package/src/hooks/use-reduced-motion.ts +18 -1
  94. package/src/index.ts +31 -0
  95. package/src/lib/cn.ts +13 -1
  96. package/src/lib/cursor-ghost.ts +16 -6
  97. package/src/lib/platform.ts +71 -4
@@ -6,20 +6,32 @@ import type { ControlSize } from "../lib/size";
6
6
  import { Calendar, type CalendarProps, type DateRange } from "./calendar";
7
7
  import { fieldBoxClass, fieldSizeClass, fieldTextClass, type InputSurface } from "./input";
8
8
 
9
+ /** Height and text rung of the trigger and the calendar inside: `sm` 32px, `md` 40px, `lg` 48px. */
9
10
  export type DateFieldSize = ControlSize;
11
+ /** Surface of the trigger: `glass` over imagery and glass panels, `solid` on raised surfaces, `solid-on-page` on the page background. */
10
12
  export type DateFieldSurface = InputSurface;
11
13
 
12
14
  type WithoutCalendarSurface<T> = T extends unknown ? Omit<T, "size" | "surface"> : never;
13
15
 
16
+ /**
17
+ * Props for {@link DateField}: every `Calendar` prop except its own `size` and `surface`, plus the trigger's
18
+ * placeholder, format and classes. `value` is the calendar's value too, so pass a `Date` in `single` mode and
19
+ * a `DateRange` in `range` mode; the trigger text is drawn from the same value.
20
+ */
14
21
  export type DateFieldProps = WithoutCalendarSurface<CalendarProps> & {
22
+ /** Rung of the trigger, and of the calendar in the popup. @default "md" */
15
23
  size?: DateFieldSize;
16
- /** `solid` (default) on raised surfaces, `glass` over imagery and glass panels, `solid-on-page` directly on the page background. */
24
+ /** `solid` (default) on raised surfaces, `glass` over imagery and glass panels, `solid-on-page` directly on the page background. @default "solid" */
17
25
  surface?: DateFieldSurface;
26
+ /** Text while nothing is picked; it also styles as placeholder text. @default "Pick a date" */
18
27
  placeholder?: string;
28
+ /** Marks the trigger `aria-invalid` and turns its border danger red. */
19
29
  invalid?: boolean;
30
+ /** Dims the trigger and blocks the popup. */
20
31
  disabled?: boolean;
21
32
  /** Formats the trigger text; defaults to "Oct 20, 2026" and "Oct 20 – Oct 24, 2026". */
22
33
  format?: (value: Date | DateRange) => string;
34
+ /** Extra classes on the trigger button, merged after the field surface and size classes. */
23
35
  triggerClassName?: string;
24
36
  };
25
37
 
@@ -42,7 +54,24 @@ const popupCalendarWidthClass: Record<DateFieldSize, string> = {
42
54
  lg: "w-93.5 max-w-[calc(100vw-2rem)]",
43
55
  };
44
56
 
45
- /** Figma `Date field` (60:244): a 280px field that opens the calendar, in three sizes and two surfaces. */
57
+ /**
58
+ * Figma `Date field` (60:244): a 280px field that opens the calendar, in three sizes and two surfaces.
59
+ *
60
+ * A `Popover` trigger, not an input: the button shows the formatted value or the placeholder and opens the
61
+ * calendar in a portal at the trigger's start edge, 4px below. The popup names its own width (250/314/374px
62
+ * by rung, capped to the viewport) because the calendar fills its parent and a positioner is auto-width.
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * const [date, setDate] = useState<Date | null>(new Date());
67
+ *
68
+ * <DateField
69
+ * value={date}
70
+ * onValueChange={setDate}
71
+ * placeholder="Pick a date"
72
+ * />
73
+ * ```
74
+ */
46
75
  export function DateField({
47
76
  size = "md",
48
77
  surface = "solid",
@@ -4,14 +4,44 @@ import type { ComponentProps, ReactNode } from "react";
4
4
 
5
5
  import { cn } from "../lib/cn";
6
6
 
7
+ /**
8
+ * State holder for a dialog: give it `open` / `onOpenChange` to control it, or neither plus a
9
+ * `defaultOpen` to leave it uncontrolled. It renders nothing itself; the parts below supply the surface.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const [open, setOpen] = useState(false);
14
+ *
15
+ * <Dialog open={open} onOpenChange={setOpen}>
16
+ * <DialogTrigger render={<Button />}>Rename bot</DialogTrigger>
17
+ * <DialogContent
18
+ * title="Rename bot"
19
+ * description="The new name shows up everywhere the bot is mentioned."
20
+ * >
21
+ * <Input defaultValue="Weekly digest" aria-label="Bot name" />
22
+ * <DialogFooter>
23
+ * <DialogClose render={<Button variant="solid" />}>Cancel</DialogClose>
24
+ * <Button onClick={() => setOpen(false)}>Save</Button>
25
+ * </DialogFooter>
26
+ * </DialogContent>
27
+ * </Dialog>
28
+ * ```
29
+ */
7
30
  export const Dialog = BaseDialog.Root;
31
+
32
+ /** The control that opens the dialog. It takes `render` to put the press behaviour on your own button. */
8
33
  export const DialogTrigger = BaseDialog.Trigger;
34
+
35
+ /** The control that closes the dialog: it sets the dialog's open state to false, so a controlled dialog also sees `onOpenChange`. */
9
36
  export const DialogClose = BaseDialog.Close;
10
37
 
38
+ /** Props for {@link DialogContent}: the Base UI popup attributes plus the heading, description and surface choices. */
11
39
  export type DialogContentProps = Omit<ComponentProps<typeof BaseDialog.Popup>, "className" | "title"> & {
40
+ /** Heading rendered as the popup's `title-md` title, which also names the dialog for assistive tech. */
12
41
  title: ReactNode;
42
+ /** Optional `body-md` secondary paragraph under the title, wired to the popup's description. */
13
43
  description?: ReactNode;
14
- /** Label for the close button; hides it when `null`. */
44
+ /** Label for the close button; hides it when `null`. @default "Close" */
15
45
  closeLabel?: string | null;
16
46
  /** Slot marker on the close control, for a surface its own app selects the control by. */
17
47
  closeSlot?: string;
@@ -21,14 +51,21 @@ export type DialogContentProps = Omit<ComponentProps<typeof BaseDialog.Popup>, "
21
51
  * `glass` is the veil dialog (`veil-overlay`). `solid` is the Paper workspace modal:
22
52
  * opaque default fill, 20px radius, 24px padding and a 0 24 64 shadow, no glass layer.
23
53
  * Both surfaces share the 16px block rhythm between title, description and actions.
54
+ * @default "solid"
24
55
  */
25
56
  surface?: "glass" | "solid";
57
+ /** Extra classes on the popup panel, merged after the surface, radius and motion classes. */
26
58
  className?: string;
27
59
  };
28
60
 
29
61
  /**
30
- * Figma `Dialog on scrim` (63:329): a 40% scrim and a 420px veil dialog,
62
+ * Figma `Dialog on scrim` (63:329): the `bg-scrim` ink at 32% alpha and a 420px veil dialog,
31
63
  * 24px padding, title-md heading with a 32px close target, body-md description.
64
+ *
65
+ * Part of {@link Dialog}. Portals to the body over a 6px-blurred scrim and centres through a fixed viewport
66
+ * with 16px gutters, so on a narrow screen the panel keeps its padding and shrinks to the gutters instead;
67
+ * the panel caps at `100dvh - 32px` and scrolls its own content. Base UI owns the focus trap, Escape to
68
+ * close and the scroll lock.
32
69
  */
33
70
  export function DialogContent({
34
71
  title,
@@ -84,10 +121,15 @@ export function DialogContent({
84
121
  /**
85
122
  * The action row: secondary first, the primary action at the right edge, 8px apart.
86
123
  * `w-full` is what right-aligns it — the dialog popup stacks children at their content width.
124
+ *
125
+ * Part of {@link Dialog}. Renders a plain `div`, so it can also close a `BottomSheet` body with the same rhythm.
87
126
  */
88
127
  export function DialogFooter({
89
128
  className,
90
129
  ...props
91
- }: Omit<ComponentProps<"div">, "className"> & { className?: string }) {
130
+ }: Omit<ComponentProps<"div">, "className"> & {
131
+ /** Extra classes on the row, merged after the alignment and gap classes. */
132
+ className?: string;
133
+ }) {
92
134
  return <div className={cn("relative flex w-full flex-wrap items-center justify-end gap-2", className)} {...props} />;
93
135
  }
@@ -5,19 +5,40 @@ import { cn } from "../lib/cn";
5
5
  import { IconButton } from "./icon-button";
6
6
  import { LiquidGlass } from "./liquid-glass";
7
7
 
8
+ /** Props for {@link DirectoryCard}: the tile icon and copy, plus an optional add action. */
8
9
  export type DirectoryCardProps = Omit<ComponentProps<"div">, "children"> & {
9
10
  /** 20px icon shown in inverse on the 40px tile. */
10
11
  icon: ReactNode;
12
+ /** Tile label, `label-md` primary text on one truncated line. */
11
13
  name: ReactNode;
12
14
  /** One line; longer text is clamped. */
13
15
  description?: ReactNode;
16
+ /** Adds an `IconButton` with a plus when set; the card shows no button without it. */
14
17
  onAdd?: () => void;
18
+ /** Accessible name of the add button (`aria-label`), since it shows only a plus icon. @default "Add" */
15
19
  addLabel?: string;
16
- /** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. */
20
+ /** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. @default "solid" */
17
21
  surface?: "solid" | "glass";
18
22
  };
19
23
 
20
- /** Paper `Directory card` (Settings 1VJJ-0): 40px tile, name and one-line description, plus an add button. */
24
+ /**
25
+ * Paper `Directory card` (Settings 1VJJ-0): 40px tile, name and one-line description, plus an add button.
26
+ *
27
+ * The `glass` surface paints the thick glass fill and mounts a `LiquidGlass` layer behind the content, so
28
+ * it belongs only over imagery or a glass panel. The add button inherits the same surface, which keeps the
29
+ * two materials matched.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * <DirectoryCard
34
+ * icon={<Folder />}
35
+ * name="Product"
36
+ * description="Specs, research and launch notes."
37
+ * onAdd={() => openFolderPicker()}
38
+ * addLabel="Connect folder"
39
+ * />
40
+ * ```
41
+ */
21
42
  export function DirectoryCard({
22
43
  icon,
23
44
  name,
@@ -4,52 +4,97 @@ import { type ComponentProps, type ReactNode, useMemo, useState } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { PanelPillTabs } from "./bot-instructions-panel";
6
6
 
7
- type WithClass<T> = Omit<T, "className"> & { className?: string };
7
+ /** Strips a section's own `className` so the panel can merge its classes with the caller's. */
8
+ type WithClass<T> = Omit<T, "className"> & {
9
+ /** Extra classes merged last onto the element. */
10
+ className?: string;
11
+ };
8
12
 
13
+ /** Row state of a plain field: `filled` reads fine, `check` marks a value the extractor is unsure about. */
9
14
  export type ExtractFieldStatus = "filled" | "check";
10
15
 
16
+ /** One extracted scalar value: one row in the Fields view and one entry in the JSON view. */
11
17
  export type ExtractField = {
18
+ /** Stable identity; confirming a row records this id and drives the footer counters. */
12
19
  id: string;
20
+ /** Row label on the left, 100px wide; also the key in the JSON view. */
13
21
  label: string;
22
+ /** The read value, shown truncated in a button-sized pill. */
14
23
  value: string;
15
24
  /** Field type shown at the row end, for example `Text` or `Date`. */
16
25
  type?: string;
26
+ /** `check` marks the value for confirmation: the row takes the danger border and a "Check" caption. */
17
27
  status?: ExtractFieldStatus;
18
28
  };
19
29
 
30
+ /** One extracted table: a labelled block with a header row and body rows instead of a single value. */
20
31
  export type ExtractTable = {
32
+ /** Stable identity, counted like a field in the footer. */
21
33
  id: string;
34
+ /** Row label above the table. */
22
35
  label: string;
36
+ /** Field type shown at the row end, such as `Table`. */
23
37
  type?: string;
24
- columns: { label: string; align?: "start" | "end"; width?: number }[];
38
+ /** Column definitions, in order; a cell reads its column by index. */
39
+ columns: {
40
+ /** Column heading, `caption` secondary text. */
41
+ label: string;
42
+ /** Cell alignment; `end` right-aligns numbers. */
43
+ align?: "start" | "end";
44
+ /** Fixed column width in px; a column without it shares the free width. */
45
+ width?: number;
46
+ }[];
47
+ /** Body rows; each inner array is one row, matched to `columns` by index. */
25
48
  rows: string[][];
26
49
  };
27
50
 
51
+ /** A labelled section of the Fields view, grouped under a dot and a heading. */
28
52
  export type ExtractGroup = {
53
+ /** Stable identity for the React key. */
29
54
  id: string;
55
+ /** Section heading, `label-md` primary text. */
30
56
  label: string;
57
+ /** Fields and tables of this section, in display order. */
31
58
  fields: (ExtractField | ExtractTable)[];
32
59
  };
33
60
 
61
+ /** Props for {@link DocumentExtractPanel}: the sheet to view, the extracted groups and the footer actions. */
34
62
  export type DocumentExtractPanelProps = WithClass<Omit<ComponentProps<"section">, "title">> & {
63
+ /** Document name in the viewer toolbar; `label-md` primary text, truncated. */
35
64
  fileName: string;
65
+ /** Page indicator at the right of the toolbar. @default "1 / 1" */
36
66
  pageLabel?: string;
37
67
  /** The paper sheet content. Wrap fields in `ExtractHighlight` to outline what was read. */
38
68
  sheet: ReactNode;
69
+ /** Extracted sections, in display order; the same data feeds the Fields, JSON and Schema views. */
39
70
  groups: ExtractGroup[];
71
+ /** Starting zoom the toolbar shows; 70 draws the sheet at its design size (scale 1) and the range is 30–150. @default 70 */
40
72
  defaultZoom?: number;
41
- bot?: { initial: string; name: string; elapsed?: string };
73
+ /** Bot credited in the footer; without it the footer shows no attribution. */
74
+ bot?: {
75
+ /** One-letter mark on the 20px brand square. */
76
+ initial: string;
77
+ /** Bot name, `caption` secondary text. */
78
+ name: string;
79
+ /** Optional run time appended as "· 12s". */
80
+ elapsed?: string;
81
+ };
82
+ /** Fires when the Extract button is pressed; the panel also bumps its run counter, which remounts the active view and replays its entry animation. */
42
83
  onExtract?: () => void;
84
+ /** Fires after Accept all has queued the confirmation of every row, so the confirmed marks land on the next render. */
43
85
  onAcceptAll?: () => void;
44
86
  /** Makes the file name actionable; without it the name is a plain label. */
45
87
  onOpenFile?: () => void;
46
88
  };
47
89
 
90
+ /** Lowest and highest zoom the toolbar allows, and the step of one press. */
48
91
  const ZOOM_MIN = 30;
49
92
  const ZOOM_MAX = 150;
50
93
  const ZOOM_STEP = 10;
94
+ /** The fit zoom: 100% of the sheet box is drawn at this percent, so the default 70 shows it at its design size. */
51
95
  const ZOOM_FIT = 70;
52
96
 
97
+ /** Outline colours for `ExtractHighlight`: brand-family tints for reads, neutral and ink for notes and marks. */
53
98
  const highlightTone = {
54
99
  sky: "border-accent-sky-500-90 bg-accent-sky-500-12",
55
100
  brand: "border-accent-sky-700-90 bg-accent-sky-700-12",
@@ -62,7 +107,10 @@ export function ExtractHighlight({
62
107
  tone = "sky",
63
108
  className,
64
109
  ...props
65
- }: WithClass<ComponentProps<"div">> & { tone?: keyof typeof highlightTone }) {
110
+ }: WithClass<ComponentProps<"div">> & {
111
+ /** Outline and fill pair: `sky` for a normal read, `brand` for a highlighted one, `neutral` and `ink` for notes. @default "sky" */
112
+ tone?: keyof typeof highlightTone;
113
+ }) {
66
114
  return (
67
115
  <div
68
116
  className={cn("flex flex-col gap-px rounded-sm border-[1.2px] px-1 py-0.5", highlightTone[tone], className)}
@@ -79,7 +127,12 @@ function isChecked(field: ExtractField, resolved: Set<string>) {
79
127
  return field.status === "check" && !resolved.has(field.id);
80
128
  }
81
129
 
82
- function Divider({ vertical }: { vertical?: boolean }) {
130
+ /** One hairline between the panel's regions; `vertical` draws it as a column rule for the split layout. */
131
+ // prettier-ignore
132
+ function Divider({ vertical }: {
133
+ /** Draws a column rule instead of a row rule. */
134
+ vertical?: boolean;
135
+ }) {
83
136
  return (
84
137
  <div
85
138
  role="separator"
@@ -95,6 +148,31 @@ function stepClass() {
95
148
  /**
96
149
  * Document extract panel (Paper `YUT-1` / `GWP-0`): a viewer with zoom controls on the left and the extracted
97
150
  * fields on the right, with Fields / JSON / Schema views, a per-row confirmation check and an Accept all footer.
151
+ *
152
+ * Laid out with container queries, not the viewport: the two columns stack below the 4xl container width and
153
+ * split above it, with the viewer column fixed at 540px. Zoom runs 30–150 in steps of 10, "Fit" returns to 70
154
+ * (the sheet's design size), and the sheet is scaled from its top edge, so it never reflows. A field with
155
+ * `status: "check"` renders as a button that confirms that one row; Accept all confirms every row at once.
156
+ * The footer counts filled and unresolved rows, keeps a dot per field and credits the bot when `bot` is set.
157
+ *
158
+ * @example
159
+ * ```tsx
160
+ * <DocumentExtractPanel
161
+ * fileName="invoice-0424.pdf"
162
+ * groups={[
163
+ * {
164
+ * id: "vendor",
165
+ * label: "Vendor",
166
+ * fields: [
167
+ * { id: "name", label: "Name", value: "Northwind Supply", type: "Text" },
168
+ * { id: "due", label: "Due", value: "Oct 24, 2026", type: "Date", status: "check" },
169
+ * ],
170
+ * },
171
+ * ]}
172
+ * sheet={<h1 className="text-title-md">Northwind Supply</h1>}
173
+ * onAcceptAll={() => saveExtraction()}
174
+ * />
175
+ * ```
98
176
  */
99
177
  export function DocumentExtractPanel({
100
178
  fileName,
@@ -2,17 +2,36 @@ import type { ReactNode } from "react";
2
2
 
3
3
  import { cn } from "../lib/cn";
4
4
 
5
+ /** Props for {@link EmptyState}: an optional illustration, the copy and one action. */
5
6
  export type EmptyStateProps = {
6
- /** 140px illustration, such as a mascot pose. */
7
+ /** 140px illustration, such as a mascot pose. The slot is square and the child is stretched to fill it. */
7
8
  illustration?: ReactNode;
9
+ /** Heading, rendered as a `title-md` balanced paragraph. */
8
10
  title: ReactNode;
11
+ /** Supporting copy under the heading, `body-md` secondary text across the full column width. */
9
12
  description?: ReactNode;
10
13
  /** Action under the text, usually a `Button`. */
11
14
  action?: ReactNode;
15
+ /** Extra classes on the column, merged after the width, spacing and entry animation. */
12
16
  className?: string;
13
17
  };
14
18
 
15
- /** Figma `Empty state` (64:485): centered 140px mascot, title-md heading, body-md text, action. */
19
+ /**
20
+ * Figma `Empty state` (64:485): centered 140px mascot, title-md heading, body-md text, action.
21
+ *
22
+ * A 360px-max column with a rise-in animation and no border or fill, so it drops into a card, a panel body
23
+ * or an empty table area without drawing its own surface.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <EmptyState
28
+ * illustration={<img src="/mascots/horon-mascot-welcome.webp" alt="" />}
29
+ * title="Meet your first bot"
30
+ * description="Give it a role and a task. It keeps working after you close the laptop."
31
+ * action={<Button>Create a bot</Button>}
32
+ * />
33
+ * ```
34
+ */
16
35
  export function EmptyState({ illustration, title, description, action, className }: EmptyStateProps) {
17
36
  return (
18
37
  <div
@@ -4,8 +4,12 @@ import { createContext, useContext, type ComponentProps } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import type { ControlSize } from "../lib/size";
6
6
 
7
- type WithClass<T> = Omit<T, "className"> & { className?: string };
7
+ type WithClass<T> = Omit<T, "className"> & {
8
+ /** Extra classes appended after the part's own classes. */
9
+ className?: string;
10
+ };
8
11
 
12
+ /** `md` outside a `Field`, so a part rendered on its own keeps the default rung rather than throwing. */
9
13
  const FieldSizeContext = createContext<ControlSize>("md");
10
14
 
11
15
  const gapClass: Record<ControlSize, string> = {
@@ -26,12 +30,38 @@ const metaClass: Record<ControlSize, string> = {
26
30
  lg: "text-body-sm",
27
31
  };
28
32
 
29
- /** Figma `Field examples` (58:163): label, control and help text, 6/8/10px apart. */
33
+ /**
34
+ * Figma `Field examples` (58:163): label, control and help text, 6/8/10px apart.
35
+ *
36
+ * Wraps Base UI's `Field.Root`, a vertical stack (`max-w-full`, left aligned) whose gaps and text
37
+ * rungs follow `size` through context, so `FieldLabel`, `FieldDescription` and `FieldError` match the
38
+ * control without repeating the size. `invalid` (or Base UI's own validation) marks the control inside
39
+ * the field invalid, and the control's box paints its danger border from that.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * import { Field, FieldDescription, FieldError, FieldLabel, Input } from "@horon-ai/react";
44
+ *
45
+ * function EmailField() {
46
+ * return (
47
+ * <Field size="md">
48
+ * <FieldLabel htmlFor="email">Email</FieldLabel>
49
+ * <Input id="email" type="email" placeholder="you@example.com" />
50
+ * <FieldDescription>We only use it to sign you in.</FieldDescription>
51
+ * <FieldError>Enter a valid address.</FieldError>
52
+ * </Field>
53
+ * );
54
+ * }
55
+ * ```
56
+ */
30
57
  export function Field({
31
58
  size = "md",
32
59
  className,
33
60
  ...props
34
- }: WithClass<Omit<ComponentProps<typeof BaseField.Root>, "size">> & { size?: ControlSize }) {
61
+ }: WithClass<Omit<ComponentProps<typeof BaseField.Root>, "size">> & {
62
+ /** Control rung shared by every part; sets the gap (6/8/10px) and the label and meta type scales. @default "md" */
63
+ size?: ControlSize;
64
+ }) {
35
65
  return (
36
66
  <FieldSizeContext.Provider value={size}>
37
67
  <BaseField.Root
@@ -42,6 +72,11 @@ export function Field({
42
72
  );
43
73
  }
44
74
 
75
+ /**
76
+ * Part of {@link Field}. The control's label (`Field.Label`): `label-sm/md/lg` from the field's size,
77
+ * primary text, and the disabled colour when its control is disabled. Give it `htmlFor` matching the
78
+ * control's `id`, which is also what makes clicking the text focus the control.
79
+ */
45
80
  export function FieldLabel({ className, ...props }: WithClass<ComponentProps<typeof BaseField.Label>>) {
46
81
  const size = useContext(FieldSizeContext);
47
82
  return (
@@ -52,11 +87,22 @@ export function FieldLabel({ className, ...props }: WithClass<ComponentProps<typ
52
87
  );
53
88
  }
54
89
 
90
+ /**
91
+ * Part of {@link Field}. The help line under the control (`Field.Description`) in secondary text:
92
+ * `caption` at sm and md, `body-sm` at lg. Always full width so the copy wraps inside the field, and
93
+ * Base UI wires it to the control as the description for assistive tech.
94
+ */
55
95
  export function FieldDescription({ className, ...props }: WithClass<ComponentProps<typeof BaseField.Description>>) {
56
96
  const size = useContext(FieldSizeContext);
57
97
  return <BaseField.Description className={cn(metaClass[size], "w-full text-text-secondary", className)} {...props} />;
58
98
  }
59
99
 
100
+ /**
101
+ * Part of {@link Field}. The validation message (`Field.Error`) in `text-text-danger` at the field's
102
+ * meta rung. It stays mounted only while it should show: Base UI renders it when the enclosing field is
103
+ * invalid, or when `match` is set, and keeps it mounted through its exit — the row collapses from `1fr`
104
+ * to `0fr` as it fades, so the parts below glide up instead of jumping.
105
+ */
60
106
  export function FieldError({ className, children, ...props }: WithClass<ComponentProps<typeof BaseField.Error>>) {
61
107
  const size = useContext(FieldSizeContext);
62
108
  // Base UI keeps the message mounted through its exit (`data-ending-style`), so the row can close
@@ -4,26 +4,61 @@ import { type ComponentProps, type DragEvent, type ReactNode, useId, useState }
4
4
  import { cn } from "../lib/cn";
5
5
  import { Spinner } from "./spinner";
6
6
 
7
+ /**
8
+ * How the drop target is painted: `solid` fills it with the raised surface (`bg-bg-surface`), `glass`
9
+ * leaves it transparent on the veil so imagery behind shows through.
10
+ */
7
11
  export type FileDropSurface = "solid" | "glass";
8
12
 
13
+ /** Props of {@link FileDrop}: the native file input attributes plus the target's own state. */
9
14
  export type FileDropProps = Omit<ComponentProps<"input">, "className" | "type" | "children" | "onDrop"> & {
10
- /** Label next to the icon. */
15
+ /** Label next to the icon. @default "Drop files here or click to upload" */
11
16
  label?: ReactNode;
12
17
  /** Shows a spinner in place of the icon while files upload. */
13
18
  uploading?: boolean;
14
19
  /** Danger rim for a rejected file. */
15
20
  rejected?: boolean;
16
- /** Called with the dropped files. */
21
+ /** Called with the dropped or picked files, in picker order; fires on both drop and native file input change. */
17
22
  onFiles?: (files: File[]) => void;
18
- /** `solid` (default) fills the target with the raised surface; `glass` puts it on the veil over imagery. */
23
+ /** `solid` (default) fills the target with the raised surface; `glass` puts it on the veil over imagery. @default "solid" */
19
24
  surface?: FileDropSurface;
25
+ /** Extra classes for the label that forms the target. */
20
26
  className?: string;
21
27
  };
22
28
 
23
29
  /**
24
30
  * Paper `File drop` (1VWI-0): a 1.5px dashed r10 target, icon 16 and a 13/18 500 label centred with gap 8.
25
- * Dragging swaps the dashes for a 1px brand rim; no shake or bounce. Solid fills with the raised surface,
31
+ * Dragging keeps the dashed edge and repaints it in the brand border, adds a 1px inset brand rim and, on
32
+ * `solid`, shifts the fill to `bg-bg-subtle`; no shake or bounce. Solid fills with the raised surface,
26
33
  * glass sits on the veil (`veil-drop`) and keeps the dashed border as its only outline.
34
+ *
35
+ * The target is a `<label>` around a screen-reader-only `<input type="file">`, so a click opens the native
36
+ * picker and the control stays keyboard reachable; the focus ring is drawn on the label through
37
+ * `has-[:focus-visible]`. Drops land in `onFiles` with `DataTransfer.files`, and the input's change event
38
+ * also calls `onFiles` before forwarding `onChange`. While `uploading`, the icon becomes a spinner, the
39
+ * input is disabled and drops are ignored; `rejected` paints a danger rim. The target is capped at 550px
40
+ * and never wider than its parent.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * import { useState } from "react";
45
+ * import { FileDrop } from "@horon-ai/react";
46
+ *
47
+ * function Example() {
48
+ * const [uploading, setUploading] = useState(false);
49
+ * return (
50
+ * <FileDrop
51
+ * accept=".pdf,.png"
52
+ * label="Drop contracts here or click to upload"
53
+ * uploading={uploading}
54
+ * onFiles={(files) => {
55
+ * setUploading(true);
56
+ * void upload(files).finally(() => setUploading(false));
57
+ * }}
58
+ * />
59
+ * );
60
+ * }
61
+ * ```
27
62
  */
28
63
  export function FileDrop({
29
64
  label = "Drop files here or click to upload",
@@ -3,18 +3,27 @@ import { type ComponentProps, type ReactNode, useRef, useState } from "react";
3
3
  import { cn } from "../lib/cn";
4
4
  import { useActiveIndicator } from "../hooks/use-active-indicator";
5
5
 
6
+ /** One option in a `FilterBar`. */
6
7
  export type FilterBarItem = {
8
+ /** Stable value reported through `onValueChange` and compared with `value`. */
7
9
  value: string;
10
+ /** Visible chip text; normally a short noun such as `All` or `Agents`. */
8
11
  label: ReactNode;
9
- /** Count shown after the label, such as 4. */
12
+ /** Count shown after the label, such as 4. Omit to hide the slot. */
10
13
  count?: number;
14
+ /** Disables just this option: 40% opacity, not clickable. */
11
15
  disabled?: boolean;
12
16
  };
13
17
 
18
+ /** Props of {@link FilterBar}: the options and the selection, controlled or uncontrolled. */
14
19
  export type FilterBarProps = Omit<ComponentProps<"div">, "onChange" | "defaultValue"> & {
20
+ /** Options in display order; the first one is the initial selection when `defaultValue` is omitted. */
15
21
  items: FilterBarItem[];
22
+ /** Controlled selection. Pass `onValueChange` to hear about clicks. */
16
23
  value?: string;
24
+ /** Initial selection when uncontrolled. @default items[0].value */
17
25
  defaultValue?: string;
26
+ /** Called with the clicked option's `value`; clicks on the already-selected option still fire. */
18
27
  onValueChange?: (value: string) => void;
19
28
  };
20
29
 
@@ -22,6 +31,34 @@ export type FilterBarProps = Omit<ComponentProps<"div">, "onChange" | "defaultVa
22
31
  * Paper `Filter bar` (Agents N2): a 5% ink pill track with 30px options; the selected option sits on a sliding 10% ink pill.
23
32
  * The pills never wrap, so a screen too narrow for all of them scrolls the track sideways instead of widening the page.
24
33
  * The track keeps its intrinsic width (and so stays centred by its parent) unless that width exceeds the box.
34
+ *
35
+ * Selection is controlled through `value` or uncontrolled from `defaultValue`, which falls back to the first
36
+ * item. The group is a `radiogroup` labelled by `aria-label` (`Filter` when omitted), each option a
37
+ * `<button role="radio" aria-checked>`; the background pill is measured by the active option and hides when
38
+ * none matches, and it lands without animating on the first paint. There is no roving tab index: every enabled
39
+ * option is a tab stop, and Space or Enter selects it.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * import { useState } from "react";
44
+ * import { FilterBar } from "@horon-ai/react";
45
+ *
46
+ * function Example() {
47
+ * const [scope, setScope] = useState("all");
48
+ * return (
49
+ * <FilterBar
50
+ * aria-label="Scope"
51
+ * value={scope}
52
+ * onValueChange={setScope}
53
+ * items={[
54
+ * { value: "all", label: "All", count: 12 },
55
+ * { value: "agents", label: "Agents", count: 4 },
56
+ * { value: "archived", label: "Archived", disabled: true },
57
+ * ]}
58
+ * />
59
+ * );
60
+ * }
61
+ * ```
25
62
  */
26
63
  export function FilterBar({
27
64
  items,