@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
@@ -7,7 +7,13 @@ import { IconButton } from "./icon-button";
7
7
  import { LiquidGlass } from "./liquid-glass";
8
8
 
9
9
  /** Figma `Date separator` (Message extras 109:667): caption between two 10% ink lines. */
10
- export function DateSeparator({ children, className }: { children: ReactNode; className?: string }) {
10
+ // prettier-ignore
11
+ export function DateSeparator({ children, className }: {
12
+ /** Caption text between the rules, such as "Today" or "Mar 4". */
13
+ children: ReactNode;
14
+ /** Extra classes on the row, merged after the built-in ones. */
15
+ className?: string;
16
+ }) {
11
17
  return (
12
18
  <div role="separator" className={cn("flex items-center gap-3", className)}>
13
19
  <span className="h-px flex-1 bg-overlay-ink-10" />
@@ -17,17 +23,43 @@ export function DateSeparator({ children, className }: { children: ReactNode; cl
17
23
  );
18
24
  }
19
25
 
20
- export type MessageAction = { label: string; icon: ReactNode; onClick?: () => void };
26
+ /** One button of a {@link MessageActions} pill. */
27
+ export type MessageAction = {
28
+ /** Accessible label of the button; also its React key, so keep labels unique within a pill. */
29
+ label: string;
30
+ /** 16px glyph; sized by the pill's `[&>svg]:size-4`. */
31
+ icon: ReactNode;
32
+ /** Fires on click. */
33
+ onClick?: () => void;
34
+ };
21
35
 
22
- /** Figma `Message actions` (109:667): a pill of 30px icon buttons with 16px icons, solid or regular glass. */
36
+ /**
37
+ * Figma `Message actions` (109:667): a pill of 30px icon buttons with 16px icons, solid or regular glass.
38
+ *
39
+ * Hovering a button fills a circle behind the glyph; each button carries its
40
+ * label as `aria-label` and the pill exposes `role="toolbar"`. With
41
+ * `surface="glass"` it paints a `LiquidGlass` layer and the buttons sit above it.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * <MessageActions
46
+ * actions={[
47
+ * { label: "Copy", icon: <Copy />, onClick: () => copy(text) },
48
+ * { label: "Regenerate", icon: <ArrowsClockwise />, onClick: regenerate },
49
+ * ]}
50
+ * />
51
+ * ```
52
+ */
23
53
  export function MessageActions({
24
54
  actions,
25
55
  surface = "solid",
26
56
  className,
27
57
  }: {
58
+ /** Buttons of the pill, rendered in order; each needs a unique label. */
28
59
  actions: MessageAction[];
29
- /** `solid` (default) is an opaque surface pill with a default border; `glass` is the regular glass pill. */
60
+ /** `solid` (default) is an opaque surface pill with a default border; `glass` is the regular glass pill. @default "solid" */
30
61
  surface?: "solid" | "glass";
62
+ /** Extra classes on the pill, merged after the built-in ones. */
31
63
  className?: string;
32
64
  }) {
33
65
  return (
@@ -55,14 +87,20 @@ export function MessageActions({
55
87
  );
56
88
  }
57
89
 
90
+ /** Props for {@link Attachment}: what the file is, how it looks and whether it can be removed. */
58
91
  export type AttachmentProps = {
92
+ /** File name, truncated to one line. */
59
93
  name: ReactNode;
60
94
  /** Caption, such as "PDF · 2.4 MB". */
61
95
  detail?: ReactNode;
62
96
  /** Image thumbnail URL; files show a document icon on a sky tile. */
63
97
  thumbnail?: string;
98
+ /** Renders the 32px remove button when set. */
64
99
  onRemove?: () => void;
100
+ /** Accessible label of the remove button.
101
+ * @default "Remove" */
65
102
  removeLabel?: string;
103
+ /** Extra classes on the card, merged after the built-in ones. */
66
104
  className?: string;
67
105
  };
68
106
 
@@ -103,11 +141,19 @@ export function Attachment({ name, detail, thumbnail, onRemove, removeLabel = "R
103
141
  );
104
142
  }
105
143
 
144
+ /** Props for {@link CodeBlock}: the code text, its language caption and the copy-button labels. */
106
145
  export type CodeBlockProps = Omit<ComponentProps<"div">, "className" | "children"> & {
146
+ /** Source text, rendered verbatim inside a `<code>` in a horizontal scroller. */
107
147
  code: string;
148
+ /** Caption in the header bar, such as "tsx"; the space is kept even when empty. */
108
149
  language?: string;
150
+ /** Copy-button text before the copy succeeds.
151
+ * @default "Copy" */
109
152
  copyLabel?: string;
153
+ /** Copy-button text for 1500ms after a successful copy.
154
+ * @default "Copied" */
110
155
  copiedLabel?: string;
156
+ /** Extra classes on the outer `div`, merged after the built-in ones. */
111
157
  className?: string;
112
158
  };
113
159
 
@@ -4,28 +4,71 @@ import type { ComponentProps, ReactNode } from "react";
4
4
 
5
5
  import { cn } from "../lib/cn";
6
6
 
7
+ /**
8
+ * Root of a modal; owns open state. Re-exported from Base UI's `Dialog.Root`, so
9
+ * it accepts `open`, `defaultOpen`, `onOpenChange` and `modal`.
10
+ */
7
11
  export const Modal = BaseDialog.Root;
12
+
13
+ /**
14
+ * The element that opens the modal. Base UI gives it the expanded state and the
15
+ * `aria-controls` link to the popup; use `render` to make an existing button the
16
+ * trigger.
17
+ *
18
+ * Part of {@link ModalContent}.
19
+ */
8
20
  export const ModalTrigger = BaseDialog.Trigger;
21
+
22
+ /**
23
+ * A button that closes the modal when clicked. Any `Button`-like element can be
24
+ * its child; it also returns focus to the trigger.
25
+ *
26
+ * Part of {@link ModalContent}.
27
+ */
9
28
  export const ModalClose = BaseDialog.Close;
10
29
 
30
+ /**
31
+ * Width step of the modal popup.
32
+ *
33
+ * - `sm` — 400px, for a short form.
34
+ * - `md` — 560px, the default reading width.
35
+ * - `lg` — 720px, for a wide editor or table.
36
+ */
11
37
  export type ModalSize = "sm" | "md" | "lg";
38
+
39
+ /**
40
+ * Surface of the modal popup.
41
+ *
42
+ * - `solid` — the opaque page fill with a `0 24 64` shadow.
43
+ * - `glass` — the `veil-overlay` material, for a modal that floats over content.
44
+ */
12
45
  export type ModalSurface = "solid" | "glass";
13
46
 
14
- type DivProps = Omit<ComponentProps<"div">, "className"> & { className?: string };
47
+ type DivProps = Omit<ComponentProps<"div">, "className"> & {
48
+ /** Extra classes on the element, merged after the built-in ones. */
49
+ className?: string;
50
+ };
15
51
 
52
+ /** Props for {@link ModalContent}: title and description, the size and surface, and an optional footer. */
16
53
  export type ModalContentProps = Omit<ComponentProps<typeof BaseDialog.Popup>, "className" | "title"> & {
54
+ /** Modal heading, rendered as the `Dialog.Title` that labels the popup. */
17
55
  title: ReactNode;
56
+ /** Supporting line under the title, rendered as the `Dialog.Description`. */
18
57
  description?: ReactNode;
19
- /** Width: `sm` 400px, `md` 560px (default), `lg` 720px. Never wider than the viewport minus 32px. */
58
+ /** Width: `sm` 400px, `md` 560px (default), `lg` 720px. Never wider than the viewport minus 32px.
59
+ * @default "md" */
20
60
  size?: ModalSize;
21
- /** `solid` (default) is the opaque page fill with a 0 24 64 shadow; `glass` is the veil (`veil-overlay`). */
61
+ /** `solid` (default) is the opaque page fill with a 0 24 64 shadow; `glass` is the veil (`veil-overlay`).
62
+ * @default "solid" */
22
63
  surface?: ModalSurface;
23
64
  /** Tile or icon before the title. */
24
65
  leading?: ReactNode;
25
- /** Label for the close button; hides it when `null`. */
66
+ /** Label for the close button; hides it when `null`.
67
+ * @default "Close" */
26
68
  closeLabel?: string | null;
27
69
  /** `ModalFooter`, kept below the scrolling body. */
28
70
  footer?: ReactNode;
71
+ /** Extra classes on the popup, merged after the built-in ones. */
29
72
  className?: string;
30
73
  };
31
74
 
@@ -39,6 +82,30 @@ const widthClass: Record<ModalSize, string> = {
39
82
  * A modal for work that needs a form or a longer read, where `Dialog` is a short confirmation. It has three
40
83
  * fixed parts: a header (title, description and close), a body that scrolls on its own once the modal reaches
41
84
  * the viewport height, and a footer whose actions sit at the right. 24px padding, 20px radius, 32% scrim.
85
+ *
86
+ * The popup portals to `document.body` and renders its own backdrop and centred
87
+ * viewport, so it is unaffected by the caller's stacking context; Base UI traps
88
+ * focus, closes on Escape and on backdrop press, and restores focus to the
89
+ * trigger. Only the body scrolls: the header and footer stay put.
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * <Modal>
94
+ * <ModalTrigger render={<Button variant="solid" />}>Rename workspace</ModalTrigger>
95
+ * <ModalContent
96
+ * title="Rename workspace"
97
+ * description="This changes the name for every member."
98
+ * footer={
99
+ * <ModalFooter>
100
+ * <ModalClose render={<Button variant="solid" />}>Cancel</ModalClose>
101
+ * <Button onClick={save}>Save</Button>
102
+ * </ModalFooter>
103
+ * }
104
+ * >
105
+ * <Input aria-label="Workspace name" defaultValue="Horon" />
106
+ * </ModalContent>
107
+ * </Modal>
108
+ * ```
42
109
  */
43
110
  export function ModalContent({
44
111
  title,
@@ -5,20 +5,26 @@ import { cn } from "../lib/cn";
5
5
  import { SegmentedControl } from "./segmented-control";
6
6
  import { Spinner } from "./spinner";
7
7
 
8
+ /** The lead bot row at the top of {@link OrchestratorPanel}. */
8
9
  export type OrchestratorLead = {
9
10
  /** One letter shown in the 22px ink tile. */
10
11
  initial: string;
12
+ /** Bot name, next to the tile. */
11
13
  name: ReactNode;
12
- /** Role caption next to the name, such as "Lead". */
14
+ /** Role caption next to the name, such as "Lead". Defaults to "Lead" when unset.
15
+ * @default "Lead" */
13
16
  role?: ReactNode;
14
17
  /** Status text, such as "1 working". */
15
18
  status?: ReactNode;
16
19
  /** Shows the 13px spinner while the lead waits on its lanes. */
17
20
  running?: boolean;
21
+ /** Timing label at the right, in caption colour. */
18
22
  elapsed?: ReactNode;
19
23
  };
20
24
 
25
+ /** One step of the lead's plan, listed under its row. */
21
26
  export type OrchestratorPlanItem = {
27
+ /** Step text; also the React key, so keep steps unique. */
22
28
  label: string;
23
29
  /** Dot color of the lane that runs this step, such as "bg-sky-600". Defaults to the live blue. */
24
30
  dotClassName?: string;
@@ -26,35 +32,73 @@ export type OrchestratorPlanItem = {
26
32
  running?: boolean;
27
33
  };
28
34
 
35
+ /** The closing summary card of {@link OrchestratorPanel}. */
29
36
  export type OrchestratorSummary = {
37
+ /** Title of the card.
38
+ * @default "Summary" */
30
39
  title?: ReactNode;
31
40
  /** Right-aligned count, such as "1 of 3 returned". */
32
41
  count?: ReactNode;
42
+ /** Body text of the summary. */
33
43
  body: ReactNode;
34
44
  };
35
45
 
46
+ /** Props for {@link OrchestratorPanel}: the header controls, the lead and its plan, the lanes and the summary. */
36
47
  export type OrchestratorPanelProps = {
48
+ /** Panel heading, rendered as the `h3`. */
37
49
  title: ReactNode;
38
- /** Label before the segmented picker, such as "Bots in parallel". */
50
+ /** Label before the segmented picker, such as "Bots in parallel". @default "Bots in parallel" */
39
51
  parallelLabel?: ReactNode;
40
- /** Choices of the parallel picker. */
52
+ /** Choices of the parallel picker. The picker only renders when this is set. */
41
53
  parallelOptions?: number[];
54
+ /** Controlled picked value; the picker is uncontrolled-looking when unset. */
42
55
  parallelValue?: number;
56
+ /** Fires with the picked number, converted from the segmented control's string value. */
43
57
  onParallelChange?: (value: number) => void;
58
+ /** Text of the Stop button.
59
+ * @default "Stop" */
44
60
  stopLabel?: ReactNode;
45
61
  /** Renders the Stop button when set. */
46
62
  onStop?: () => void;
63
+ /** The lead row: tile, name, role, status and elapsed. */
47
64
  lead: OrchestratorLead;
65
+ /** Steps under the lead; each row rises in with a 30ms stagger. Hides the list when empty. */
48
66
  plan?: OrchestratorPlanItem[];
49
67
  /** `AgentLane` cards; the branch connector draws one elbow per lane. */
50
68
  children?: ReactNode;
69
+ /** Closing summary card; the reveal wrapper collapses when unset. */
51
70
  summary?: OrchestratorSummary;
71
+ /** Extra classes on the panel `section`, merged after the built-in ones. */
52
72
  className?: string;
53
73
  };
54
74
 
55
75
  const stagger = (index: number) => ({ "--i": index, animationDelay: `calc(var(--i) * 30ms)` }) as CSSProperties;
56
76
 
57
- /** Figma `Orchestrator` (Agents N1): a 24px-radius solid panel, header with a parallel picker and Stop, a lead row, its plan, branching lanes and a summary. */
77
+ /**
78
+ * Figma `Orchestrator` (Agents N1): a 24px-radius solid panel, header with a parallel picker and Stop, a lead row, its plan, branching lanes and a summary.
79
+ *
80
+ * Renders a lead bot that fans work out to lane cards: the connector draws one
81
+ * elbow per child and a vertical trunk between them, and the plan rows and lane
82
+ * cards rise in with a 30ms stagger. The summary does not rise; it is revealed by
83
+ * the `motion-reveal` wrapper around it. Every part is optional except `title` and
84
+ * `lead`, so the panel works as a bare lead view too.
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * <OrchestratorPanel
89
+ * title="Orchestrator"
90
+ * parallelOptions={[1, 2, 3]}
91
+ * parallelValue={2}
92
+ * onParallelChange={setParallel}
93
+ * onStop={stop}
94
+ * lead={{ initial: "H", name: "Horon", status: "2 working", running: true }}
95
+ * plan={[{ label: "Collect sources" }, { label: "Draft the summary", running: true }]}
96
+ * summary={{ count: "0 of 2 returned", body: "Lanes are still running." }}
97
+ * >
98
+ * <AgentLane status="running" bot={{ initial: "R", name: "Researcher" }} task="Collect sources" />
99
+ * </OrchestratorPanel>
100
+ * ```
101
+ */
58
102
  export function OrchestratorPanel({
59
103
  title,
60
104
  parallelLabel = "Bots in parallel",
@@ -5,20 +5,43 @@ 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 PageHeader}: the title row, an optional back button, trailing actions and a filter row. */
8
9
  export type PageHeaderProps = {
10
+ /** Page title, rendered as the `h1`; truncated with an ellipsis when it does not fit. */
9
11
  title: ReactNode;
12
+ /** Renders a leading 40px back `IconButton` when set; the header drops the title's left padding in exchange. */
10
13
  onBack?: () => void;
14
+ /** Accessible label of the back button; `IconButton` sets it as `aria-label` and shows no visible tooltip.
15
+ * @default "Back" */
11
16
  backLabel?: string;
12
17
  /** Trailing 40px icon buttons, such as search. */
13
18
  actions?: ReactNode;
14
- /** Content under the top row, such as a `SegmentedControl` filter. */
19
+ /** Content under the top row, such as a `SegmentedControl` filter. Animates in with `swap-move` when it appears. */
15
20
  children?: ReactNode;
16
- /** `solid` (default) is an opaque surface header with a default border; `glass` is the regular glass header. */
21
+ /** `solid` (default) is an opaque surface header with a default border; `glass` is the regular glass header. @default "solid" */
17
22
  surface?: "solid" | "glass";
23
+ /** Extra classes on the `header` element, merged after the built-in ones. */
18
24
  className?: string;
19
25
  };
20
26
 
21
- /** Figma `Header with filter` (Segmented filters 88:723): a 28px-radius header, 8px padding, solid or regular glass. */
27
+ /**
28
+ * Figma `Header with filter` (Segmented filters 88:723): a 28px-radius header, 8px padding, solid or regular glass.
29
+ *
30
+ * A single row of back button, title and actions, plus an optional filter row
31
+ * below it. The `glass` surface paints a `LiquidGlass` layer behind the content
32
+ * and keeps the header transparent.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * <PageHeader
37
+ * title="Sources"
38
+ * onBack={() => history.back()}
39
+ * actions={<IconButton label="Search" icon={<MagnifyingGlass />} />}
40
+ * >
41
+ * <SegmentedControl value="all" onValueChange={() => {}} items={[{ value: "all", label: "All" }]} />
42
+ * </PageHeader>
43
+ * ```
44
+ */
22
45
  export function PageHeader({
23
46
  title,
24
47
  onBack,
@@ -5,11 +5,20 @@ import type { ReactNode } from "react";
5
5
  import { cn } from "../lib/cn";
6
6
  import { Spinner } from "./spinner";
7
7
 
8
+ /**
9
+ * Footer state of a `PlanCard`: at most one at a time, and setting it expands the
10
+ * footer.
11
+ *
12
+ * - `running` shows a 14px spinner and keeps the footer text in the primary colour.
13
+ * - `failed` shows a warning icon and tints the footer text with `text-text-danger`.
14
+ */
8
15
  export type PlanCardStatus = "running" | "failed";
9
16
 
17
+ /** Props for {@link PlanCard}: a label, title and step list, plus the optional status footer. */
10
18
  export type PlanCardProps = {
11
19
  /** Label line next to the 16px list icon, such as "Plan · 5 steps". */
12
20
  label?: ReactNode;
21
+ /** Plan title, rendered as the `h3` under the label. */
13
22
  title: ReactNode;
14
23
  /** `PlanStep`s, stacked without dividers. */
15
24
  children?: ReactNode;
@@ -17,11 +26,29 @@ export type PlanCardProps = {
17
26
  status?: PlanCardStatus;
18
27
  /** Footer text, such as "Stopped · step 4 failed" or "Running · 1 of 5". */
19
28
  statusLabel?: ReactNode;
29
+ /** Right-aligned footer timestamp or duration, kept in caption colour. */
20
30
  elapsed?: ReactNode;
31
+ /** Extra classes on the `section` element, merged after the built-in ones. */
21
32
  className?: string;
22
33
  };
23
34
 
24
- /** Figma `Plan card` (Agents N3): a 20px-radius solid card with a label, title, the steps and a status footer. */
35
+ /**
36
+ * Figma `Plan card` (Agents N3): a 20px-radius solid card with a label, title, the steps and a status footer.
37
+ *
38
+ * The footer is a `Collapsible` panel that stays mounted, so passing and clearing
39
+ * `status` plays the collapse animation instead of mounting and unmounting. Only
40
+ * the footer text fades in again, and only when `status` changes; the label line
41
+ * above the title does not animate.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * <PlanCard label="Plan · 3 steps" title="Ship the parser" status="running" statusLabel="Running · 1 of 3">
46
+ * <PlanStep status="done" index={1} title="Read the spec" />
47
+ * <PlanStep status="running" index={2} title="Write the parser" />
48
+ * <PlanStep status="pending" index={3} title="Publish" last />
49
+ * </PlanCard>
50
+ * ```
51
+ */
25
52
  export function PlanCard({ label, title, children, status, statusLabel, elapsed, className }: PlanCardProps) {
26
53
  return (
27
54
  <section
@@ -4,25 +4,65 @@ import type { ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { Spinner } from "./spinner";
6
6
 
7
+ /**
8
+ * State of one step in a plan.
9
+ *
10
+ * - `done` — finished: sky marker with a check and a sky rail running to the next step.
11
+ * - `running` — in progress: spinner on a subtle fill.
12
+ * - `failed` — stopped with an error: danger marker with `!` and the error line expanded.
13
+ * - `pending` — not started: outlined marker that shows the step number.
14
+ */
7
15
  export type PlanStepStatus = "done" | "running" | "failed" | "pending";
8
16
 
17
+ /**
18
+ * Props for {@link PlanStep}. Only `status`, `index` and `title` are required; everything else is
19
+ * optional decoration around the marker and the title line.
20
+ */
9
21
  export type PlanStepProps = {
22
+ /** Current step state; drives the marker glyph, the marker fill and the rail color. */
10
23
  status: PlanStepStatus;
11
24
  /** Step number, shown on pending steps. */
12
25
  index: number;
26
+ /** Step title; primary while done, running or failed, secondary while pending. */
13
27
  title: ReactNode;
28
+ /** Secondary line under the title, next to the tool tag. */
14
29
  description?: ReactNode;
15
30
  /** Tool id in mono, such as "stripe.query". */
16
31
  tool?: ReactNode;
32
+ /** Timing label at the right of the title; rendered only while `done` or `running`. */
17
33
  elapsed?: ReactNode;
34
+ /** Error line under the title, revealed only when `status` is `failed`. */
18
35
  error?: ReactNode;
36
+ /** Adds a "Retry from here" button in the expanded error block; it is inert unless `status` is `failed`. */
19
37
  onRetry?: () => void;
20
38
  /** Hides the rail line under the last step. */
21
39
  last?: boolean;
40
+ /** Classes merged onto the outer row. */
22
41
  className?: string;
23
42
  };
24
43
 
25
- /** Figma `Plan step` (Agent plan 191:8296): a 24px marker on a 2px rail, sky when done. */
44
+ /**
45
+ * Figma `Plan step` (Agent plan 191:8296): a 24px marker on a 2px rail, sky when done.
46
+ *
47
+ * Stack the steps of an agent plan vertically and pass `last` to the final one so the rail stops
48
+ * there. The marker cross-fades on every status change, and the error block collapses unless the
49
+ * step failed.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * import { PlanStep } from "@horon-ai/react";
54
+ *
55
+ * function Example() {
56
+ * return (
57
+ * <div>
58
+ * <PlanStep status="done" index={1} title="Read the brief" elapsed="2s" />
59
+ * <PlanStep status="running" index={2} title="Search the web" tool="web.search" />
60
+ * <PlanStep status="pending" index={3} title="Write the summary" last />
61
+ * </div>
62
+ * );
63
+ * }
64
+ * ```
65
+ */
26
66
  export function PlanStep({
27
67
  status,
28
68
  index,
@@ -7,29 +7,87 @@ import { IconButton } from "./icon-button";
7
7
  import { Menu, MenuContent, MenuTrigger } from "./menu";
8
8
  import { Switch } from "./switch";
9
9
 
10
+ /**
11
+ * Props for {@link PluginRow}. The row also forwards every other `div` prop (except `children`)
12
+ * to the outer element, so `id`, `data-*` and DOM handlers work as usual.
13
+ */
10
14
  export type PluginRowProps = Omit<ComponentProps<"div">, "children"> & {
11
15
  /** Two-letter initials shown on the 40px tile. */
12
16
  initials: string;
17
+ /** Plugin name; truncated when the row gets narrow. */
13
18
  name: ReactNode;
19
+ /** Version string drawn in mono next to the name. */
14
20
  version?: ReactNode;
15
21
  /** Text of the update pill next to the version; omit when up to date. */
16
22
  updateBadge?: ReactNode;
23
+ /** Author line under the name. */
17
24
  author?: ReactNode;
25
+ /** Adds the sky "Verified" check after the author; only shown when `author` is set. */
18
26
  verified?: boolean;
19
27
  /** Capability chips, each with an optional 11px icon. */
20
- chips?: { id: string; label: ReactNode; icon?: ReactNode }[];
28
+ chips?: {
29
+ /** Stable key for the chip. */
30
+ id: string;
31
+ /** Chip text. */
32
+ label: ReactNode;
33
+ /** Optional leading glyph, sized 11px. */
34
+ icon?: ReactNode;
35
+ }[];
36
+ /** Switch state for the plugin. Pass it with `onEnabledChange` to control the toggle. */
21
37
  enabled?: boolean;
38
+ /** Fires with the next switch state when the plugin is enabled or disabled. */
22
39
  onEnabledChange?: (enabled: boolean) => void;
23
- /** Shows the Update button when set. */
40
+ /** Shows the Update button when set; without it the button stays collapsed. */
24
41
  onUpdate?: () => void;
42
+ /**
43
+ * Label of the Update button.
44
+ * @default "Update"
45
+ */
25
46
  updateLabel?: string;
47
+ /**
48
+ * Accessible name of the enable/disable switch.
49
+ * @default "Enabled"
50
+ */
26
51
  toggleLabel?: string;
52
+ /**
53
+ * Accessible name of the overflow menu button.
54
+ * @default "More"
55
+ */
27
56
  overflowLabel?: string;
28
57
  /** Items of the overflow menu; the menu button is hidden without them. */
29
58
  menu?: ReactNode;
30
59
  };
31
60
 
32
- /** Paper `Plugin row` (Settings 1VKS-0): tile, name with version and update pill, verified author, chips, and Update, Switch and overflow actions. */
61
+ /**
62
+ * Paper `Plugin row` (Settings 1VKS-0): tile, name with version and update pill, verified author, chips, and Update, Switch and overflow actions.
63
+ *
64
+ * Rows stack directly in a list: each one draws its own bottom divider and drops it on the last
65
+ * child. The Update button collapses when `onUpdate` is missing, and the overflow menu only renders
66
+ * when `menu` has content.
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * import { MenuItem, PluginRow } from "@horon-ai/react";
71
+ *
72
+ * function Example() {
73
+ * return (
74
+ * <PluginRow
75
+ * initials="ST"
76
+ * name="Stripe"
77
+ * version="2.4.1"
78
+ * updateBadge="Update ready"
79
+ * author="Stripe"
80
+ * verified
81
+ * chips={[{ id: "payments", label: "Payments" }]}
82
+ * enabled
83
+ * onEnabledChange={() => {}}
84
+ * onUpdate={() => {}}
85
+ * menu={<MenuItem>Uninstall</MenuItem>}
86
+ * />
87
+ * );
88
+ * }
89
+ * ```
90
+ */
33
91
  export function PluginRow({
34
92
  initials,
35
93
  name,
@@ -3,13 +3,43 @@ import type { ComponentProps } from "react";
3
3
 
4
4
  import { cn } from "../lib/cn";
5
5
 
6
+ /**
7
+ * Props for {@link Progress}. The remaining Base UI `Progress.Root` props (`min`, `max`, `format`,
8
+ * `locale`, `getAriaValueText`, `aria-valuetext`) pass through; the label is a separate
9
+ * `Progress.Label` part, which this wrapper does not render.
10
+ */
6
11
  export type ProgressProps = Omit<ComponentProps<typeof BaseProgress.Root>, "className" | "value"> & {
12
+ /** Classes merged onto the 240px root. */
7
13
  className?: string;
8
- /** 0-100. Omit it (or pass `null`) for the indeterminate bar. */
14
+ /**
15
+ * Determinate progress in percent (0-100). Omit it (or pass `null`) for the indeterminate bar,
16
+ * which animates a third-width sweep inside the track.
17
+ * @default null
18
+ */
9
19
  value?: number | null;
10
20
  };
11
21
 
12
- /** Figma `Progress bar` (in Loading 109:733): a 6px 8% ink track with an ink fill. */
22
+ /**
23
+ * Figma `Progress bar` (in Loading 109:733): a 6px 8% ink track with an ink fill.
24
+ *
25
+ * The width animates on a 400ms quint curve when `value` changes, so a value fed from state reads
26
+ * as movement rather than a jump. Base UI supplies the `progressbar` role and the `aria-valuenow`
27
+ * wiring.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * import { Progress } from "@horon-ai/react";
32
+ *
33
+ * function Example() {
34
+ * return (
35
+ * <div>
36
+ * <Progress value={64} aria-label="Upload" />
37
+ * <Progress aria-label="Loading" />
38
+ * </div>
39
+ * );
40
+ * }
41
+ * ```
42
+ */
13
43
  export function Progress({ className, value = null, ...props }: ProgressProps) {
14
44
  return (
15
45
  <BaseProgress.Root value={value} className={cn("w-60", className)} {...props}>