@horon-ai/react 0.1.0 → 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 (98) hide show
  1. package/package.json +2 -2
  2. package/src/components/agent-activity.tsx +81 -11
  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 +53 -7
  6. package/src/components/agent-steering-panel.tsx +73 -21
  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 +92 -9
  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 +89 -10
  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 +39 -2
  38. package/src/components/flue-background.tsx +91 -21
  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 +37 -4
  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 +54 -18
  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 +58 -3
  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 +48 -3
  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 +91 -17
  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/index.ts +8 -0
  87. package/src/{lib → hooks}/use-active-indicator.ts +44 -1
  88. package/src/hooks/use-color-scheme.ts +50 -0
  89. package/src/hooks/use-copy-to-clipboard.ts +55 -0
  90. package/src/hooks/use-device.ts +69 -0
  91. package/src/hooks/use-hotkey.ts +56 -0
  92. package/src/hooks/use-media-query.ts +43 -0
  93. package/src/hooks/use-platform.ts +68 -0
  94. package/src/hooks/use-reduced-motion.ts +25 -0
  95. package/src/index.ts +44 -0
  96. package/src/lib/cn.ts +13 -1
  97. package/src/lib/cursor-ghost.ts +16 -6
  98. package/src/lib/platform.ts +236 -0
@@ -5,7 +5,14 @@ import type { ReactNode } from "react";
5
5
  import { cn } from "../lib/cn";
6
6
  import type { ControlSize } from "../lib/size";
7
7
 
8
+ /**
9
+ * Toast severity, which picks the leading icon and its color: `neutral` (info icon, secondary
10
+ * text), `success` (check, brand color) or `danger` (warning, danger color). Any other Base UI
11
+ * toast type falls back to `neutral`.
12
+ */
8
13
  export type ToastTone = "neutral" | "success" | "danger";
14
+
15
+ /** Toast rungs: `sm` 320px, `md` 360px and `lg` 400px wide cards. */
9
16
  export type ToastSize = ControlSize;
10
17
 
11
18
  const toneIcon: Record<ToastTone, ReactNode> = {
@@ -54,7 +61,14 @@ function isTone(value: string | undefined): value is ToastTone {
54
61
  return value === "neutral" || value === "success" || value === "danger";
55
62
  }
56
63
 
57
- function ToastList({ size, surface }: { size: ToastSize; surface: "solid" | "glass" }) {
64
+ /** Renders the live toast queue; each toast is a Base UI root positioned by the provider's viewport. */
65
+ // prettier-ignore
66
+ function ToastList({ size, surface }: {
67
+ /** Card rung applied to every toast in the stack. */
68
+ size: ToastSize;
69
+ /** `glass` paints each card on the veil; `solid` gives it a surface fill, a border and the popover shadow. */
70
+ surface: "solid" | "glass";
71
+ }) {
58
72
  const { toasts } = BaseToast.useToastManager();
59
73
  return toasts.map((toast) => {
60
74
  const tone = isTone(toast.type) ? toast.type : "neutral";
@@ -89,21 +103,51 @@ function ToastList({ size, surface }: { size: ToastSize; surface: "solid" | "gla
89
103
  });
90
104
  }
91
105
 
106
+ /** Props of {@link Toaster}: the app subtree it wraps plus the size and surface every toast shares. */
92
107
  export type ToasterProps = {
108
+ /** The app subtree; toasts portal to the viewport on top of it. */
93
109
  children?: ReactNode;
94
- /** Toast size: `sm` 320px, `md` 360px (default), `lg` 400px; applies to every toast this provider shows. */
110
+ /** Toast size: `sm` 320px, `md` 360px (default), `lg` 400px; applies to every toast this provider shows. @default "md" */
95
111
  size?: ToastSize;
96
112
  /**
97
113
  * `solid` (default) is an opaque surface card with a default border; `glass` puts it on the veil (`veil-overlay`).
98
- * Applies to every toast this provider shows.
114
+ * Applies to every toast this provider shows. @default "solid"
99
115
  */
100
116
  surface?: "solid" | "glass";
117
+ /** Extra classes on the fixed viewport, which sits 24px from the bottom, right and left edges. */
101
118
  className?: string;
102
119
  };
103
120
 
104
121
  /**
105
122
  * Figma `Toast` (62:307): a 360px card, solid or the veil. Wrap the app once; call
106
123
  * `useToast().add({ title, description, type: "success" })` to show one.
124
+ *
125
+ * Toasts portal to a fixed bottom-right viewport that stacks upward; each card rises 16px into
126
+ * place in 240ms, shrinks from 95% as it fades in, and leaves to the right while its height
127
+ * collapses. The close button clears it, and the whole height/margin tween runs on
128
+ * `--toast-height`.
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * import { Toaster, useToast } from "@horon-ai/react";
133
+ *
134
+ * function App() {
135
+ * return (
136
+ * <Toaster size="md" surface="solid">
137
+ * <SaveButton />
138
+ * </Toaster>
139
+ * );
140
+ * }
141
+ *
142
+ * function SaveButton() {
143
+ * const toast = useToast();
144
+ * return (
145
+ * <button onClick={() => toast.add({ title: "Saved", description: "All changes stored.", type: "success" })}>
146
+ * Save
147
+ * </button>
148
+ * );
149
+ * }
150
+ * ```
107
151
  */
108
152
  export function Toaster({ children, size = "md", surface = "solid", className }: ToasterProps) {
109
153
  return (
@@ -125,4 +169,9 @@ export function Toaster({ children, size = "md", surface = "solid", className }:
125
169
  );
126
170
  }
127
171
 
172
+ /**
173
+ * The toast manager from the nearest {@link Toaster}: `add({ title, description, type })` shows a
174
+ * toast and returns its id, `close(id)` dismisses one, and `toasts` is the live list the viewport
175
+ * renders. Calling it without a provider throws.
176
+ */
128
177
  export const useToast = BaseToast.useToastManager;
@@ -1,15 +1,28 @@
1
1
  import { Collapsible } from "@base-ui/react/collapsible";
2
2
  import { ArrowClockwise, CaretDown, Check, CheckCircle, Copy, WarningCircle } from "@phosphor-icons/react";
3
- import { useEffect, useState, type ReactNode } from "react";
3
+ import type { ReactNode } from "react";
4
4
 
5
+ import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard";
5
6
  import { cn } from "../lib/cn";
6
7
  import { Button } from "./button";
7
8
  import { Spinner } from "./spinner";
8
9
 
10
+ /**
11
+ * Any JSON-shaped value: a string, number, boolean, `null`, or an array or object of the same.
12
+ * `ToolCall` renders `args` and `result` through {@link JsonCode}, so plain objects and arrays are
13
+ * enough; class instances and functions are not supported.
14
+ */
9
15
  type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
10
16
 
11
17
  const punctuation = (text: string) => <span className="text-syntax-punctuation">{text}</span>;
12
18
 
19
+ /**
20
+ * Walks a JSON value and returns syntax-highlighted React nodes, nesting one indentation step per level.
21
+ *
22
+ * @param value - The JSON value to render.
23
+ * @param indent - Current nesting depth; 0 for a root value, one extra level per object or array.
24
+ * @returns A flat list of span nodes for the key, string, number and punctuation syntax colors.
25
+ */
13
26
  function renderJson(value: Json, indent: number): ReactNode[] {
14
27
  const pad = " ".repeat(indent);
15
28
  if (value === null || typeof value === "number" || typeof value === "boolean") {
@@ -49,8 +62,20 @@ function renderJson(value: Json, indent: number): ReactNode[] {
49
62
  ];
50
63
  }
51
64
 
52
- /** JSON in Geist Mono code style with the Figma syntax colors (key, string, number, punctuation). */
53
- export function JsonCode({ value, className }: { value: Json; className?: string }) {
65
+ /**
66
+ * JSON in Geist Mono code style with the Figma syntax colors (key, string, number, punctuation).
67
+ *
68
+ * Renders a `pre` in the code type token that wraps lines so long values stay inside a narrow card;
69
+ * it scrolls horizontally only when a single unbreakable token overflows. Used by {@link ToolCall}
70
+ * for tool arguments and results, and usable on its own.
71
+ */
72
+ // prettier-ignore
73
+ export function JsonCode({ value, className }: {
74
+ /** The JSON value to print; see `Json` for the accepted shapes. */
75
+ value: Json;
76
+ /** Extra classes for the `pre`, for example a maximum height or `text-code` override. */
77
+ className?: string;
78
+ }) {
54
79
  return (
55
80
  <pre
56
81
  className={cn("max-w-full overflow-x-auto font-mono text-code whitespace-pre-wrap text-syntax-string", className)}
@@ -60,43 +85,70 @@ export function JsonCode({ value, className }: { value: Json; className?: string
60
85
  );
61
86
  }
62
87
 
88
+ /**
89
+ * Lifecycle state of a tool call.
90
+ *
91
+ * - `running`: a spinner replaces the status icon and the result section shows "Waiting for result…".
92
+ * - `done`: a live-green check marks the row and the result is printed.
93
+ * - `failed`: the card border turns danger, an error notice (with an optional Retry button) replaces the result.
94
+ */
63
95
  export type ToolCallStatus = "running" | "done" | "failed";
64
96
 
97
+ /**
98
+ * Props for {@link ToolCall}.
99
+ */
65
100
  export type ToolCallProps = {
101
+ /** Current lifecycle state; drives the status icon, border and which sections appear. */
66
102
  status: ToolCallStatus;
67
103
  /** Plain tool name, such as "Web search", with its 16px icon. */
68
- tool: { icon: ReactNode; name: ReactNode };
104
+ tool: {
105
+ /** A 16px Phosphor icon; it inherits the secondary text color inside the name pill. */
106
+ icon: ReactNode;
107
+ /** The visible tool name; truncates when the header runs out of room. */
108
+ name: ReactNode;
109
+ };
110
+ /** One-line description of the call, such as the query that was sent. */
69
111
  summary: ReactNode;
112
+ /** Elapsed time, such as "1.2s"; hidden when omitted. */
70
113
  elapsed?: ReactNode;
114
+ /** Arguments object printed as JSON in an `Arguments` section when defined. */
71
115
  args?: Json;
116
+ /** Result value printed as JSON in a `Result` section while the call is `running` (as a waiting message) or `done`; a `failed` call omits the section in favour of `error`. */
72
117
  result?: Json;
118
+ /** Failure message shown instead of the result section when `status` is `failed`. */
73
119
  error?: ReactNode;
120
+ /** Renders a Retry button on the failure notice; omit it to show the error without a retry. */
74
121
  onRetry?: () => void;
75
122
  /** The result is cut off; shows a "Show all" link under it. */
76
123
  truncated?: boolean;
124
+ /** Called when the "Show all" link is pressed; only rendered when `truncated` is set and a result exists. */
77
125
  onShowAll?: () => void;
126
+ /** Starts the card expanded; the card manages its own open state afterwards. */
78
127
  defaultOpen?: boolean;
128
+ /** Extra classes for the card root. */
79
129
  className?: string;
80
130
  };
81
131
 
132
+ /**
133
+ * One labelled block inside the expanded card: an uppercase label row with an optional copy button,
134
+ * a subtle rounded body, and an optional footer (used for the "Show all" link).
135
+ */
82
136
  function Section({
83
137
  label,
84
138
  children,
85
139
  copyText,
86
140
  footer,
87
141
  }: {
142
+ /** Uppercase heading, such as "Arguments"; also names the copy button for screen readers. */
88
143
  label: string;
144
+ /** Body content, normally a {@link JsonCode} block or the waiting message. */
89
145
  children: ReactNode;
90
146
  /** Plain text of the section body; adds a real copy button when present. */
91
147
  copyText?: string;
148
+ /** Content under the body, such as the truncated "Show all" link. */
92
149
  footer?: ReactNode;
93
150
  }) {
94
- const [copied, setCopied] = useState(false);
95
- useEffect(() => {
96
- if (!copied) return undefined;
97
- const timer = setTimeout(() => setCopied(false), 1500);
98
- return () => clearTimeout(timer);
99
- }, [copied]);
151
+ const { copied, copy } = useCopyToClipboard(1500);
100
152
  return (
101
153
  <div className="flex min-w-0 flex-col gap-1.5">
102
154
  <div className="flex items-center gap-2 text-label-sm text-text-secondary">
@@ -105,12 +157,7 @@ function Section({
105
157
  <button
106
158
  type="button"
107
159
  aria-label={copied ? "Copied" : `Copy ${label.toLowerCase()}`}
108
- onClick={() => {
109
- navigator.clipboard?.writeText(copyText).then(
110
- () => setCopied(true),
111
- () => undefined,
112
- );
113
- }}
160
+ onClick={() => copy(copyText)}
114
161
  className="flex motion-press cursor-pointer items-center justify-center rounded-sm hover:text-text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand"
115
162
  >
116
163
  {copied ? (
@@ -127,7 +174,34 @@ function Section({
127
174
  );
128
175
  }
129
176
 
130
- /** Figma `Tool call` (Tool call card 186:7811): a solid 16px-radius card that expands to arguments and result. */
177
+ /**
178
+ * Figma `Tool call` (Tool call card 186:7811): a solid 16px-radius card that expands to arguments and result.
179
+ *
180
+ * The header trigger is a Base UI collapsible: it shows the tool pill, the summary, a status icon
181
+ * (spinner, check or warning) and a caret that rotates while open. The expanded body adds the
182
+ * failure notice with Retry, the JSON `Arguments` section and the `Result` section with copy buttons.
183
+ * The card is uncontrolled apart from `defaultOpen`; use {@link JsonCode} directly when only the
184
+ * highlighted JSON is needed.
185
+ *
186
+ * @example
187
+ * ```tsx
188
+ * import { ToolCall } from "@horon-ai/react";
189
+ * import { Globe } from "@phosphor-icons/react";
190
+ *
191
+ * function Example() {
192
+ * return (
193
+ * <ToolCall
194
+ * status="done"
195
+ * tool={{ icon: <Globe />, name: "Web search" }}
196
+ * summary="Fetched 8 results for Horon tokens"
197
+ * elapsed="1.2s"
198
+ * args={{ query: "Horon design tokens" }}
199
+ * result={{ results: 8 }}
200
+ * />
201
+ * );
202
+ * }
203
+ * ```
204
+ */
131
205
  export function ToolCall({
132
206
  status,
133
207
  tool,
@@ -4,21 +4,52 @@ import { Children, type ReactNode } from "react";
4
4
 
5
5
  import { cn } from "../lib/cn";
6
6
 
7
+ /**
8
+ * Props for {@link ToolCallsHeader}.
9
+ */
7
10
  export type ToolCallsHeaderProps = {
8
11
  /** Up to three 14px tool icons, drawn as overlapping 28px chips. */
9
12
  icons?: ReactNode;
13
+ /** Header text; defaults to "Using tools…". @default "Using tools…" */
10
14
  title?: ReactNode;
11
15
  /** Elapsed time, such as "4.0s". */
12
16
  elapsed?: ReactNode;
13
17
  /** `ToolCall` cards, stacked 8px apart under the header. */
14
18
  children?: ReactNode;
19
+ /** Starts with the cards shown; the group manages its own open state afterwards. @default true */
15
20
  defaultOpen?: boolean;
21
+ /** Controlled open state; pass `onOpenChange` too when set. */
16
22
  open?: boolean;
23
+ /** Called with the next open state whenever the trigger is pressed. */
17
24
  onOpenChange?: (open: boolean) => void;
25
+ /** Extra classes for the outer card. */
18
26
  className?: string;
19
27
  };
20
28
 
21
- /** Figma `Tool-calls header` (Agents N11): overlapping tool chips, "Using tools…" and a caret that fold the ToolCall cards below. */
29
+ /**
30
+ * Figma `Tool-calls header` (Agents N11): overlapping tool chips, "Using tools…" and a caret that fold the ToolCall cards below.
31
+ *
32
+ * Only the first three icons are drawn, each as a 28px chip. The trigger is a Base UI collapsible
33
+ * button that rotates the caret and reveals the children in a `motion-collapse` panel. Pass `open`
34
+ * and `onOpenChange` to control it, or leave them out and start from `defaultOpen`.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * import { ToolCall, ToolCallsHeader } from "@horon-ai/react";
39
+ * import { Globe, Terminal } from "@phosphor-icons/react";
40
+ *
41
+ * function Example() {
42
+ * return (
43
+ * <ToolCallsHeader
44
+ * icons={[<Globe key="globe" aria-hidden />, <Terminal key="terminal" aria-hidden />]}
45
+ * elapsed="4.0s"
46
+ * >
47
+ * <ToolCall status="done" tool={{ icon: <Globe />, name: "Web search" }} summary="Searched the docs" />
48
+ * </ToolCallsHeader>
49
+ * );
50
+ * }
51
+ * ```
52
+ */
22
53
  export function ToolCallsHeader({
23
54
  icons,
24
55
  title = "Using tools…",
@@ -3,27 +3,54 @@ import { type ComponentProps, type ReactNode, useState } from "react";
3
3
  import { cn } from "../lib/cn";
4
4
  import { SegmentedControl } from "./segmented-control";
5
5
 
6
+ /**
7
+ * Permission a tool can be given.
8
+ *
9
+ * - `allow`: runs without asking.
10
+ * - `ask`: asks for confirmation before each call; the fallback when a `ToolPermissionItem` has no `defaultValue`.
11
+ * - `off`: the tool is disabled.
12
+ */
6
13
  export type ToolPermission = "allow" | "ask" | "off";
7
14
 
15
+ /**
16
+ * One tool row in {@link ToolPermissions}.
17
+ */
8
18
  export type ToolPermissionItem = {
19
+ /** Tool name, shown in mono type; also the key used in `values`. */
9
20
  name: string;
21
+ /** Caption under the name, such as what the tool reads or writes. */
10
22
  description?: ReactNode;
11
23
  /** Starting choice for the uncontrolled component; defaults to `ask`. */
12
24
  defaultValue?: ToolPermission;
13
25
  };
14
26
 
27
+ /**
28
+ * Props for {@link ToolPermissions}.
29
+ */
15
30
  export type ToolPermissionsProps = Omit<ComponentProps<"div">, "className" | "title" | "onChange"> & {
31
+ /** Header title, such as "Tool access". */
16
32
  title: ReactNode;
33
+ /** One row per tool, in display order; the row count also fills the header pill. */
17
34
  tools: ToolPermissionItem[];
18
35
  /** Controlled choices keyed by tool name. */
19
36
  values?: Record<string, ToolPermission>;
37
+ /** Called with the complete next map whenever one row changes or "Allow all" is pressed. */
20
38
  onValuesChange?: (values: Record<string, ToolPermission>) => void;
21
- /** Label of the text button that sets every tool to Allow. */
39
+ /** Label of the text button that sets every tool to Allow. @default "Allow all" */
22
40
  allowAllLabel?: ReactNode;
41
+ /** Extra classes for the card root. */
23
42
  className?: string;
24
43
  };
25
44
 
26
- const options: { value: ToolPermission; label: string }[] = [
45
+ /**
46
+ * Segmented control options in display order; each row renders the same Allow / Ask / Off triple.
47
+ */
48
+ const options: {
49
+ /** Permission stored for the tool when this segment is picked. */
50
+ value: ToolPermission;
51
+ /** Segment label shown to the user. */
52
+ label: string;
53
+ }[] = [
27
54
  { value: "allow", label: "Allow" },
28
55
  { value: "ask", label: "Ask" },
29
56
  { value: "off", label: "Off" },
@@ -34,6 +61,27 @@ const options: { value: ToolPermission; label: string }[] = [
34
61
  * carries the group title, a count pill and an "Allow all" text button; below it one divided row per tool
35
62
  * with a mono name, a description and a small solid Allow / Ask / Off segmented control. 16px side padding
36
63
  * on every row so the names, the header title and the controls line up.
64
+ *
65
+ * The component is uncontrolled by default, seeding each row from `ToolPermissionItem.defaultValue`
66
+ * (or `ask`). Pass `values` and `onValuesChange` to control every row at once; the controlled map wins
67
+ * over the internal state, and the internal copy is still updated on each change.
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * import { ToolPermissions } from "@horon-ai/react";
72
+ *
73
+ * function Example() {
74
+ * return (
75
+ * <ToolPermissions
76
+ * title="Tool access"
77
+ * tools={[
78
+ * { name: "web_search", description: "Read public pages", defaultValue: "allow" },
79
+ * { name: "shell", description: "Run local commands" },
80
+ * ]}
81
+ * />
82
+ * );
83
+ * }
84
+ * ```
37
85
  */
38
86
  export function ToolPermissions({
39
87
  title,
@@ -1,15 +1,43 @@
1
1
  import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
2
2
  import type { ReactElement, ReactNode } from "react";
3
3
 
4
+ /**
5
+ * Props for {@link Tooltip}.
6
+ */
4
7
  export type TooltipProps = {
5
8
  /** The element that shows the tooltip on hover and focus. */
6
9
  children: ReactElement;
10
+ /** Bubble content; keep it to a short phrase because the bubble caps at 256px. */
7
11
  label: ReactNode;
12
+ /** Preferred side of the trigger; Base UI flips it when there is no room. @default "top" */
8
13
  side?: "top" | "bottom" | "left" | "right";
14
+ /** Hover delay in milliseconds before the bubble opens; Base UI's default when omitted. */
9
15
  delay?: number;
10
16
  };
11
17
 
12
- /** Figma `Tooltip` (62:282): an ink bubble, 10/6px padding, 8px radius, with a 10x6 arrow. */
18
+ /**
19
+ * Figma `Tooltip` (62:282): an ink bubble, 10/6px padding, 8px radius, with a 10x6 arrow.
20
+ *
21
+ * The trigger is the child element itself (Base UI `render`), so no wrapper node is added. The popup
22
+ * is portaled to the body at `z-50`, 6px from the trigger with an arrow that follows the chosen side,
23
+ * and it opens on hover and on keyboard focus.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { Tooltip } from "@horon-ai/react";
28
+ * import { Copy } from "@phosphor-icons/react";
29
+ *
30
+ * function Example() {
31
+ * return (
32
+ * <Tooltip label="Copy link" side="top">
33
+ * <button type="button" aria-label="Copy link">
34
+ * <Copy />
35
+ * </button>
36
+ * </Tooltip>
37
+ * );
38
+ * }
39
+ * ```
40
+ */
13
41
  export function Tooltip({ children, label, side = "top", delay }: TooltipProps) {
14
42
  return (
15
43
  <BaseTooltip.Root>
@@ -30,4 +58,8 @@ export function Tooltip({ children, label, side = "top", delay }: TooltipProps)
30
58
  );
31
59
  }
32
60
 
61
+ /**
62
+ * Base UI tooltip provider, re-exported unchanged. Wrap a group of tooltips in it to share hover
63
+ * timing, so moving between adjacent triggers does not restart each delay.
64
+ */
33
65
  export const TooltipProvider = BaseTooltip.Provider;
@@ -6,21 +6,54 @@ import { Avatar } from "./avatar";
6
6
  import { IconButton } from "./icon-button";
7
7
  import { LiquidGlass } from "./liquid-glass";
8
8
 
9
+ /**
10
+ * Props for {@link TopBar}.
11
+ */
9
12
  export type TopBarProps = Omit<ComponentProps<"header">, "className" | "title"> & {
13
+ /** Bar title; truncates to one line and re-runs the fade-in when the text changes. */
10
14
  title: ReactNode;
15
+ /** Second line under the title, muted and truncated. */
11
16
  subtitle?: ReactNode;
12
17
  /** Shows the live status dot on the avatar. */
13
18
  live?: boolean;
19
+ /** Renders the back IconButton on the left and calls this on press; hidden when omitted. */
14
20
  onBack?: () => void;
21
+ /** Renders the overflow IconButton on the right and calls this on press; hidden when omitted. */
15
22
  onMore?: () => void;
23
+ /** Accessible label of the back button. @default "Back" */
16
24
  backLabel?: string;
25
+ /** Accessible label of the overflow button. @default "More" */
17
26
  moreLabel?: string;
18
- /** `solid` (default) is an opaque surface bar with a default border; `glass` is the regular glass bar. */
27
+ /** `solid` (default) is an opaque surface bar with a default border; `glass` is the regular glass bar. @default "solid" */
19
28
  surface?: "solid" | "glass";
29
+ /** Extra classes for the `header` element. */
20
30
  className?: string;
21
31
  };
22
32
 
23
- /** Paper `Top bar`: back button, bot avatar with a flush live dot, title and subtitle, on solid surface or regular glass. */
33
+ /**
34
+ * Paper `Top bar`: back button, bot avatar with a flush live dot, title and subtitle, on solid surface or regular glass.
35
+ *
36
+ * Renders a rounded `header` with `rounded-full` corners; the glass variant mounts {@link LiquidGlass}
37
+ * as its first child, so keep the bar inside a positioned parent. Back and overflow buttons only appear
38
+ * when `onBack` / `onMore` are passed, which keeps the bare title layout for read-only screens.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * import { TopBar } from "@horon-ai/react";
43
+ *
44
+ * function Example() {
45
+ * return (
46
+ * <TopBar
47
+ * title="Research bot"
48
+ * subtitle="12 tasks today"
49
+ * live
50
+ * onBack={() => history.back()}
51
+ * onMore={() => console.log("more")}
52
+ * />
53
+ * );
54
+ * }
55
+ * ```
56
+ */
24
57
  export function TopBar({
25
58
  title,
26
59
  subtitle,
@@ -4,7 +4,11 @@ import type { ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { LiquidGlass } from "./liquid-glass";
6
6
 
7
+ /**
8
+ * Props for {@link UsageMeter}.
9
+ */
7
10
  export type UsageMeterProps = {
11
+ /** What the meter measures, such as "Tasks used"; sits left of the percentage. */
8
12
  label: ReactNode;
9
13
  /** 0-100. */
10
14
  value: number;
@@ -12,12 +16,34 @@ export type UsageMeterProps = {
12
16
  detail?: ReactNode;
13
17
  /** Right caption, such as "Resets Nov 1". */
14
18
  note?: ReactNode;
15
- /** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. */
19
+ /** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. @default "solid" */
16
20
  surface?: "solid" | "glass";
21
+ /** Extra classes for the meter root. */
17
22
  className?: string;
18
23
  };
19
24
 
20
- /** Figma `Usage meter` (in Loading 109:733): a card with a 6px sky fill, solid or thick glass. */
25
+ /**
26
+ * Figma `Usage meter` (in Loading 109:733): a card with a 6px sky fill, solid or thick glass.
27
+ *
28
+ * Built on Base UI `Meter`, so the percentage is exposed to assistive technology and the fill
29
+ * animates its width over 400ms. The fill is the single `sky-500` accent; the card caps at 300px wide.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * import { UsageMeter } from "@horon-ai/react";
34
+ *
35
+ * function Example() {
36
+ * return (
37
+ * <UsageMeter
38
+ * label="Tasks"
39
+ * value={64}
40
+ * detail="6,420 of 10,000 tasks"
41
+ * note="Resets Nov 1"
42
+ * />
43
+ * );
44
+ * }
45
+ * ```
46
+ */
21
47
  export function UsageMeter({ label, value, detail, note, surface = "solid", className }: UsageMeterProps) {
22
48
  return (
23
49
  <Meter.Root
@@ -4,8 +4,13 @@ import type { ComponentProps, ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { Menu, MenuContent, MenuItem, MenuLabel, MenuSeparator, MenuTile, MenuTrigger } from "./menu";
6
6
 
7
+ /**
8
+ * One workspace listed in {@link WorkspaceMenu}.
9
+ */
7
10
  export type WorkspaceMenuItem = {
11
+ /** Stable identifier returned by `onValueChange` and compared with `value`. */
8
12
  id: string;
13
+ /** Workspace name shown in the row. */
9
14
  name: string;
10
15
  /** Caption under the name, such as "12 members". */
11
16
  meta?: ReactNode;
@@ -13,21 +18,61 @@ export type WorkspaceMenuItem = {
13
18
  initial?: string;
14
19
  };
15
20
 
21
+ /**
22
+ * Props for {@link WorkspaceMenu}.
23
+ */
16
24
  export type WorkspaceMenuProps = {
25
+ /** Workspaces in menu order; the current one gets a check. */
17
26
  workspaces: WorkspaceMenuItem[];
27
+ /** Id of the current workspace. */
18
28
  value: string;
29
+ /** Called with the picked workspace id; pressing the current one calls it too. */
19
30
  onValueChange?: (id: string) => void;
31
+ /** Called by the "New workspace" row; the row stays visible when omitted. */
20
32
  onCreate?: () => void;
33
+ /** Called by the "Workspace settings" row; the row stays visible when omitted. */
21
34
  onSettings?: () => void;
22
35
  /** The element that opens the menu, such as the sidebar workspace button. */
23
36
  trigger: ComponentProps<typeof MenuTrigger>["render"];
37
+ /** Content of the trigger when `trigger` renders a plain element rather than taking children. */
24
38
  triggerChildren?: ReactNode;
39
+ /** Extra classes for the trigger element. */
25
40
  triggerClassName?: string;
41
+ /** Menu heading above the workspace rows. @default "Workspaces" */
26
42
  label?: ReactNode;
43
+ /** Extra classes for the menu popup. */
27
44
  className?: string;
28
45
  };
29
46
 
30
- /** Paper `Workspace menu`: solid 256px menu listing workspaces with a check on the current one. */
47
+ /**
48
+ * Paper `Workspace menu`: solid 256px menu listing workspaces with a check on the current one.
49
+ *
50
+ * Wraps the package {@link Menu} in a solid surface so the popup stays readable over any background.
51
+ * Each row shows a tile with the workspace initial and fades a check in on the selected id; a separator
52
+ * then offers "New workspace" and "Workspace settings". The menu is anchored to `trigger` and opens
53
+ * below it, aligned to the start edge.
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * import { WorkspaceMenu } from "@horon-ai/react";
58
+ * import { useState } from "react";
59
+ *
60
+ * function Example() {
61
+ * const [current, setCurrent] = useState("acme");
62
+ * return (
63
+ * <WorkspaceMenu
64
+ * workspaces={[
65
+ * { id: "acme", name: "Acme" },
66
+ * { id: "labs", name: "Labs", meta: "4 members" },
67
+ * ]}
68
+ * value={current}
69
+ * onValueChange={setCurrent}
70
+ * trigger={<button type="button">Acme</button>}
71
+ * />
72
+ * );
73
+ * }
74
+ * ```
75
+ */
31
76
  export function WorkspaceMenu({
32
77
  workspaces,
33
78
  value,
@@ -0,0 +1,8 @@
1
+ export * from "./use-active-indicator";
2
+ export * from "./use-color-scheme";
3
+ export * from "./use-copy-to-clipboard";
4
+ export * from "./use-device";
5
+ export * from "./use-hotkey";
6
+ export * from "./use-media-query";
7
+ export * from "./use-platform";
8
+ export * from "./use-reduced-motion";