@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
@@ -5,13 +5,16 @@ import { cn } from "../lib/cn";
5
5
  import { formatShortcut } from "../lib/platform";
6
6
  import type { ControlSize } from "../lib/size";
7
7
 
8
+ /** Height rung of the chip, shared with the other sized controls: `sm`, `md`, `lg`. */
8
9
  export type KbdSize = ControlSize;
9
10
 
11
+ /** Props of {@link Kbd}: the native `kbd` attributes plus the chip size. */
10
12
  export type KbdProps = Omit<ComponentProps<"kbd">, "className"> & {
11
- /** Chip size: `sm` tighter caption, `md` the Command palette chip (default), `lg` label-sm with more padding. */
13
+ /** Chip size: `sm` tighter caption, `md` the Command palette chip (default), `lg` label-sm with more padding. @default "md" */
12
14
  size?: KbdSize;
13
15
  /** Shortcut such as `Mod+Enter`, drawn with the OS's key labels (`⌘↵` / `Ctrl+Enter`) instead of children. */
14
16
  keys?: string;
17
+ /** Extra classes for the `kbd` element. */
15
18
  className?: string;
16
19
  };
17
20
 
@@ -22,7 +25,30 @@ const sizeClass: Record<KbdSize, string> = {
22
25
  lg: "h-6 gap-1 px-2 text-label-sm [&_svg]:size-3.5",
23
26
  };
24
27
 
25
- /** Figma `Kbd` (Command palette 110:676): a 6% ink chip with caption text. */
28
+ /**
29
+ * Figma `Kbd` (Command palette 110:676): a 6% ink chip with caption text.
30
+ *
31
+ * A `<kbd>` for a keyboard shortcut: inline-flex, `rounded-xs`, secondary text, centred on a trimmed
32
+ * line box so the glyph sits optically centred in the chip. Pass `keys` as a `+`-joined shortcut such as
33
+ * `Mod+Enter` and the chip draws the OS's labels — `⌘↵` on macOS and iOS, `Ctrl+Enter` on Windows, Linux
34
+ * and Android, and the Mac symbols whenever the OS is still unknown (server render, first hydration pass).
35
+ * Without `keys` it renders `children`, so several chips can be composed by hand
36
+ * (`<Kbd>⌘</Kbd><Kbd>K</Kbd>`). Sizes are 16/20/24px tall, and any icon inside is resized with the chip
37
+ * (`2.5`/`3`/`3.5`), so pass a bare Phosphor icon or glyph.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * import { Kbd } from "@horon-ai/react";
42
+ *
43
+ * function Hint() {
44
+ * return (
45
+ * <span className="flex items-center gap-1 text-caption">
46
+ * Search <Kbd keys="Mod+K" />
47
+ * </span>
48
+ * );
49
+ * }
50
+ * ```
51
+ */
26
52
  export function Kbd({ size = "md", keys, className, children, ...props }: KbdProps) {
27
53
  const { os } = usePlatform();
28
54
  return (
@@ -5,32 +5,51 @@ import { cn } from "../lib/cn";
5
5
  import { PanelPillTabs } from "./bot-instructions-panel";
6
6
  import { Spinner } from "./spinner";
7
7
 
8
+ /** One pill tab of {@link KnowledgeBasePanel}, such as "All" or "Documents". */
8
9
  export type KnowledgeBaseTab = {
10
+ /** Stable identifier; it is the tab's value and React key. */
9
11
  id: string;
12
+ /** Tab text. */
10
13
  label: ReactNode;
14
+ /** Optional count drawn in caption colour right of the label. */
11
15
  count?: number;
12
16
  };
13
17
 
18
+ /** Props for {@link KnowledgeBasePanel}: header, tabs, indexing state and the dropzone handler. */
14
19
  export type KnowledgeBasePanelProps = Omit<ComponentProps<"section">, "className" | "title" | "onChange"> & {
20
+ /** Panel heading, rendered as the `h3`. */
15
21
  title: ReactNode;
16
22
  /** The 12/18 line under the title: sources, chunks, bots. */
17
23
  meta?: ReactNode;
24
+ /** Pills of the tab tray, in display order. */
18
25
  tabs: KnowledgeBaseTab[];
26
+ /** Controlled selected tab id. Leave unset to let the component hold the selection itself. */
19
27
  value?: string;
28
+ /** Initial selected tab when uncontrolled. Falls back to the first tab. */
20
29
  defaultValue?: string;
30
+ /** Fires with the new tab id when a pill is clicked. */
21
31
  onValueChange?: (id: string) => void;
22
- /** Shows the spinner and "N indexing" at the right of the tabs when above zero. */
32
+ /** Shows the spinner and "N indexing" at the right of the tabs when above zero.
33
+ * @default 0 */
23
34
  indexing?: number;
35
+ /** Fires when the round settings button is clicked; the button always renders. */
24
36
  onSettings?: () => void;
37
+ /** Fires when "Add source" is clicked; the button always renders. */
25
38
  onAddSource?: () => void;
26
39
  /** Called with the files dropped on the dropzone or picked with Browse. Omit to hide the dropzone. */
27
40
  onFiles?: (files: File[]) => void;
41
+ /** Headline inside the dropzone.
42
+ * @default "Drop files here, or browse" */
28
43
  dropTitle?: ReactNode;
44
+ /** Supporting line under the dropzone headline.
45
+ * @default "PDF, DOCX, Markdown or TXT · Notion, Google Drive and websites from Add source" */
29
46
  dropHint?: ReactNode;
30
- /** Pipeline column labels shown above the list; hidden on narrow widths. */
47
+ /** Pipeline column labels shown above the list; hidden on narrow widths.
48
+ * @default ["Parse", "Chunk", "Embed", "Index"] */
31
49
  stages?: string[];
32
50
  /** The list rows, usually `SourceRow variant="list"`. */
33
51
  children?: ReactNode;
52
+ /** Extra classes on the panel `section`, merged after the built-in ones. */
34
53
  className?: string;
35
54
  };
36
55
 
@@ -40,6 +59,25 @@ const defaultStages = ["Parse", "Chunk", "Embed", "Index"];
40
59
  * Paper `Knowledge base` (XVM-1): a 24px-radius surface panel with a header, a pill tab tray, a table head, a
41
60
  * flat divided source list and a dashed dropzone. The selected tab pill travels in 280ms, the dropzone border and
42
61
  * fill change in 150ms while a file is over it, and indexing progress fills are animated by the rows.
62
+ *
63
+ * The dropzone is a `<label>` wrapping a visually hidden multi-file input, so
64
+ * clicking it opens the file picker as well as accepting a drag; the input is
65
+ * cleared after each pick so the same file can be chosen twice. Passing `onFiles`
66
+ * is what makes the dropzone appear.
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * <KnowledgeBasePanel
71
+ * title="Knowledge base"
72
+ * meta="3 sources · 128 chunks"
73
+ * tabs={[{ id: "all", label: "All", count: 3 }]}
74
+ * indexing={2}
75
+ * onAddSource={openSourcePicker}
76
+ * onFiles={upload}
77
+ * >
78
+ * <SourceRow variant="list" status="ready" icon={<FileText />} name="README.md" detail="24 chunks" />
79
+ * </KnowledgeBasePanel>
80
+ * ```
43
81
  */
44
82
  export function KnowledgeBasePanel({
45
83
  title,
@@ -18,6 +18,22 @@
18
18
  *
19
19
  * Without backdrop-filter support, or with reduced transparency requested, the
20
20
  * fill turns to `glass-fill-frost` and the rim stays (see `globals.css`).
21
+ *
22
+ * It takes no props and renders a single `aria-hidden` span; every visual
23
+ * decision comes from the host's class list, so the same element serves buttons,
24
+ * headers, cards and sheets.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * function GlassCard() {
29
+ * return (
30
+ * <div className="relative isolate overflow-clip rounded-xl bg-glass-regular p-4">
31
+ * <LiquidGlass />
32
+ * <span className="relative">Floating chrome over the backdrop</span>
33
+ * </div>
34
+ * );
35
+ * }
36
+ * ```
21
37
  */
22
38
  export function LiquidGlass() {
23
39
  return <span aria-hidden className="liquid-layers" />;
@@ -6,33 +6,73 @@ import { Button } from "./button";
6
6
  import { CodePanel } from "./code-panel";
7
7
  import { LiquidGlass } from "./liquid-glass";
8
8
 
9
+ /** One selectable row of a {@link ListDetail} list. */
9
10
  export type ListDetailItem = {
11
+ /** Stable identifier; it is the value the selection reports and the React key. */
10
12
  id: string;
13
+ /** Row title, drawn in mono and truncated to one line. */
11
14
  title: ReactNode;
15
+ /** Secondary line under the title, in secondary colour. */
12
16
  meta?: ReactNode;
13
17
  };
14
18
 
19
+ /** A captioned group of {@link ListDetailItem} rows. */
15
20
  export type ListDetailGroup = {
21
+ /** Stable identifier used as the group's React key. */
16
22
  id: string;
23
+ /** Caption above the rows, in 11px semibold secondary text. */
17
24
  label: ReactNode;
25
+ /** Rows of this group, in display order. */
18
26
  items: ListDetailItem[];
19
27
  };
20
28
 
29
+ /** Props for {@link ListDetail}: the groups and one render function for the selected item. */
21
30
  export type ListDetailProps = Omit<ComponentProps<"div">, "className" | "children" | "onChange"> & {
31
+ /** Groups rendered into the left list; the flat order also drives the selection fallback. */
22
32
  groups: ListDetailGroup[];
33
+ /** Controlled selected id. Leave unset to let the component hold the selection itself. */
23
34
  value?: string;
35
+ /** Initial selection when uncontrolled. Falls back to the first item of the first group. */
24
36
  defaultValue?: string;
37
+ /** Fires with the new id whenever a row is clicked, controlled or not. */
25
38
  onValueChange?: (id: string) => void;
26
39
  /** Renders the detail pane for the selected item. */
27
40
  children: (item: ListDetailItem) => ReactNode;
28
- /** `solid` (default) is an opaque surface shell with a default border; `glass` is the thick glass shell. */
41
+ /** `solid` (default) is an opaque surface shell with a default border; `glass` is the thick glass shell. @default "solid" */
29
42
  surface?: "solid" | "glass";
43
+ /** Extra classes on the shell, merged after the built-in ones. */
30
44
  className?: string;
31
45
  };
32
46
 
33
47
  /**
34
48
  * Paper `List and detail` (1W6P-0): a 16px-radius shell, solid or thick glass, with a 250px list on an inset
35
49
  * surface and a detail pane. Selecting a row fills it in 150ms and swaps the detail with a short rise.
50
+ *
51
+ * Selection is controlled or uncontrolled like a native input: pass `value` to
52
+ * own it, or `defaultValue` for the initial row. Below `sm` the two panes stack
53
+ * instead of shrinking the list. The list is a `role="listbox"` whose rows are
54
+ * `role="option"` buttons with `aria-selected`; they respond to clicks and the
55
+ * keyboard's normal button activation.
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * <ListDetail
60
+ * groups={[
61
+ * {
62
+ * id: "sources",
63
+ * label: "Sources",
64
+ * items: [{ id: "readme", title: "README.md", meta: "2.1 KB" }],
65
+ * },
66
+ * ]}
67
+ * >
68
+ * {(item) => (
69
+ * <>
70
+ * <ListDetailHeader title={item.title} description="Indexed 4 minutes ago" />
71
+ * <ListDetailMeta items={[{ id: "chunks", label: "Chunks", value: 42 }]} />
72
+ * </>
73
+ * )}
74
+ * </ListDetail>
75
+ * ```
36
76
  */
37
77
  export function ListDetail({
38
78
  groups,
@@ -103,16 +143,24 @@ export function ListDetail({
103
143
  );
104
144
  }
105
145
 
146
+ /** Props for {@link ListDetailHeader}: a tile icon, title, description and a right-hand action cluster. */
106
147
  export type ListDetailHeaderProps = Omit<ComponentProps<"div">, "className" | "title"> & {
148
+ /** Glyph on the 40px sky tile; the tile only renders when this is set. */
107
149
  icon?: ReactNode;
150
+ /** Detail title, drawn in mono at `label-lg` semibold. */
108
151
  title: ReactNode;
152
+ /** One line under the title, in body-sm secondary colour. */
109
153
  description?: ReactNode;
110
154
  /** Right-hand cluster: a state label, a `Switch`, an icon button. */
111
155
  actions?: ReactNode;
156
+ /** Extra classes on the row, merged after the built-in ones. */
112
157
  className?: string;
113
158
  };
114
159
 
115
- /** Detail header: a 40px sky tile with an 18px icon, title and description, actions on the right. */
160
+ /** Detail header: a 40px sky tile with a 20px icon, title and description, actions on the right.
161
+ *
162
+ * Part of {@link ListDetail}.
163
+ */
116
164
  export function ListDetailHeader({ icon, title, description, actions, className, ...props }: ListDetailHeaderProps) {
117
165
  return (
118
166
  <div className={cn("flex flex-wrap items-center gap-3", className)} {...props}>
@@ -130,12 +178,25 @@ export function ListDetailHeader({ icon, title, description, actions, className,
130
178
  );
131
179
  }
132
180
 
181
+ /** Props for {@link ListDetailMeta}: the label/value pairs of the meta box. */
133
182
  export type ListDetailMetaProps = Omit<ComponentProps<"dl">, "className"> & {
134
- items: { id: string; label: ReactNode; value: ReactNode }[];
183
+ /** Pairs rendered in a two-column grid, label over value. */
184
+ items: {
185
+ /** Stable identifier of the pair; its React key. */
186
+ id: string;
187
+ /** Term above the value, in `label-sm` secondary text. */
188
+ label: ReactNode;
189
+ /** Definition under the label, truncated to one line. */
190
+ value: ReactNode;
191
+ }[];
192
+ /** Extra classes on the `dl`, merged after the built-in ones. */
135
193
  className?: string;
136
194
  };
137
195
 
138
- /** Meta box: a subtle-fill 12px-radius box with equal columns of a label over a 14/20 value. */
196
+ /** Meta box: a subtle-fill 12px-radius box with equal columns of a label over a 14/20 value.
197
+ *
198
+ * Part of {@link ListDetail}.
199
+ */
139
200
  export function ListDetailMeta({ items, className, ...props }: ListDetailMetaProps) {
140
201
  return (
141
202
  <dl
@@ -152,13 +213,21 @@ export function ListDetailMeta({ items, className, ...props }: ListDetailMetaPro
152
213
  );
153
214
  }
154
215
 
216
+ /** Props for {@link ListDetailInstructions}: the block heading, the text and the edit callback. */
155
217
  export type ListDetailInstructionsProps = Omit<ComponentProps<"div">, "className" | "onChange"> & {
218
+ /** Heading above the panel.
219
+ * @default "Instructions" */
156
220
  title?: ReactNode;
157
221
  /** Small chip beside the title, such as the file name. */
158
222
  tag?: ReactNode;
223
+ /** Controlled text. Leave unset to let the component hold the draft itself. */
159
224
  value?: string;
225
+ /** Initial text when uncontrolled.
226
+ * @default "" */
160
227
  defaultValue?: string;
228
+ /** Fires with the full textarea value on every keystroke in edit mode. */
161
229
  onValueChange?: (value: string) => void;
230
+ /** Extra classes on the wrapper, merged after the built-in ones. */
162
231
  className?: string;
163
232
  };
164
233
 
@@ -244,14 +313,21 @@ export function ListDetailInstructions({
244
313
  );
245
314
  }
246
315
 
316
+ /** Props for {@link ListDetailFiles}: the row label and the chips it wraps. */
247
317
  export type ListDetailFilesProps = Omit<ComponentProps<"div">, "className"> & {
318
+ /** Heading above the chips.
319
+ * @default "Files" */
248
320
  label?: ReactNode;
249
321
  /** Chips, usually a small `Button` with an icon. */
250
322
  children: ReactNode;
323
+ /** Extra classes on the wrapper, merged after the built-in ones. */
251
324
  className?: string;
252
325
  };
253
326
 
254
- /** Files row: a 13/18 label over a wrap of 32px glass chips, 6px apart. */
327
+ /** Files row: a 13/18 label over a wrap of 32px glass chips, 6px apart.
328
+ *
329
+ * Part of {@link ListDetail}.
330
+ */
255
331
  export function ListDetailFiles({ label = "Files", children, className, ...props }: ListDetailFilesProps) {
256
332
  return (
257
333
  <div className={cn("flex flex-col gap-2", className)} {...props}>
@@ -261,11 +337,17 @@ export function ListDetailFiles({ label = "Files", children, className, ...props
261
337
  );
262
338
  }
263
339
 
264
- /** Footer: a 1px top rule and 8px-spaced glass buttons; use `<span className="flex-1" />` as the spacer. */
340
+ /** Footer: a 1px top rule and 8px-spaced glass buttons; use `<span className="flex-1" />` as the spacer.
341
+ *
342
+ * Part of {@link ListDetail}.
343
+ */
265
344
  export function ListDetailFooter({
266
345
  className,
267
346
  ...props
268
- }: Omit<ComponentProps<"div">, "className"> & { className?: string }) {
347
+ }: Omit<ComponentProps<"div">, "className"> & {
348
+ /** Extra classes on the row, merged after the built-in ones. */
349
+ className?: string;
350
+ }) {
269
351
  return (
270
352
  <div
271
353
  className={cn("flex flex-wrap items-center gap-2 border-t border-border-default pt-3.5", className)}
@@ -5,23 +5,59 @@ import { cn } from "../lib/cn";
5
5
  import { Button } from "./button";
6
6
  import { LiquidGlass } from "./liquid-glass";
7
7
 
8
+ /** Props for {@link MarketplaceCard}: identity, copy, capability chips and the install action. */
8
9
  export type MarketplaceCardProps = Omit<ComponentProps<"div">, "children"> & {
9
10
  /** Two-letter initials shown on the 40px ink tile. */
10
11
  initials: string;
12
+ /** Card title on the row beside the tile; truncated in one line. */
11
13
  name: ReactNode;
14
+ /** Publisher line under the name, in caption colour. */
12
15
  author?: ReactNode;
16
+ /** Body copy under the header; any length, wrapped by the paragraph. */
13
17
  description?: ReactNode;
14
- /** Capability chips, each with an optional 11px icon. */
15
- chips?: { id: string; label: ReactNode; icon?: ReactNode }[];
18
+ /** Capability chips, each with an optional 12px icon. */
19
+ chips?: {
20
+ /** Stable key for this chip; also its React key. */
21
+ id: string;
22
+ /** Chip text. */
23
+ label: ReactNode;
24
+ /** Optional leading icon, rendered at 12px by the chip's `[&_svg]:size-3`. */
25
+ icon?: ReactNode;
26
+ }[];
27
+ /** Rating value shown after a filled star, such as "4.8". Hides the star when unset. */
16
28
  score?: ReactNode;
29
+ /** Install-count caption, separated from the score by a middle dot when both are set. */
17
30
  installs?: ReactNode;
31
+ /** Renders the Install button in the footer when set; receives the native click event. */
18
32
  onInstall?: () => void;
33
+ /** Text of the Install button.
34
+ * @default "Install" */
19
35
  installLabel?: string;
20
- /** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. */
36
+ /** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. @default "solid" */
21
37
  surface?: "solid" | "glass";
22
38
  };
23
39
 
24
- /** Paper `Marketplace card` (Settings 1VMY-0): solid or thick glass column with tile header, description, chips and a footer with rating and Install. */
40
+ /**
41
+ * Paper `Marketplace card` (Settings 1VMY-0): solid or thick glass column with tile header, description, chips and a footer with rating and Install.
42
+ *
43
+ * The card is a plain `div`, so any extra native attribute (`data-*`, `id`,
44
+ * `onClick`) passes through. With `surface="glass"` it paints a `LiquidGlass`
45
+ * layer and every child is positioned to stay above it.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <MarketplaceCard
50
+ * initials="GH"
51
+ * name="GitHub"
52
+ * author="by Horon"
53
+ * description="Read repositories and open pull requests."
54
+ * chips={[{ id: "official", label: "Official" }]}
55
+ * score="4.8"
56
+ * installs="12k installs"
57
+ * onInstall={() => install("github")}
58
+ * />
59
+ * ```
60
+ */
25
61
  export function MarketplaceCard({
26
62
  initials,
27
63
  name,
@@ -8,9 +8,24 @@ import { Menu, MenuContent, MenuTrigger } from "./menu";
8
8
 
9
9
  const pillClass = "inline-flex h-5.5 items-center rounded-full px-2 text-label-sm";
10
10
 
11
+ /**
12
+ * Colour of an MCP server's status pill.
13
+ *
14
+ * - `connected` — muted pill with a live dot, the healthy default.
15
+ * - `warning` — danger-tinted pill with a danger dot, for a server that needs attention.
16
+ * - `neutral` — muted pill with a placeholder-coloured dot, for an idle or unknown state.
17
+ */
11
18
  export type McpServerStatusTone = "connected" | "warning" | "neutral";
12
19
 
13
- const statusToneClass: Record<McpServerStatusTone, { pill: string; dot: string }> = {
20
+ const statusToneClass: Record<
21
+ McpServerStatusTone,
22
+ {
23
+ /** Classes of the pill itself: background tint and text colour. */
24
+ pill: string;
25
+ /** Classes of the 6px dot inside the pill: its fill colour. */
26
+ dot: string;
27
+ }
28
+ > = {
14
29
  connected: { pill: "bg-overlay-ink-6 text-text-primary", dot: "bg-status-live" },
15
30
  warning: { pill: "bg-accent-red-500-14 text-text-danger", dot: "bg-text-danger" },
16
31
  neutral: { pill: "bg-overlay-ink-6 text-text-secondary", dot: "bg-text-placeholder" },
@@ -26,29 +41,60 @@ const withKeys = (values: string[] | undefined) => {
26
41
  });
27
42
  };
28
43
 
44
+ /** Props for {@link McpServerRow}: the server identity, its tags and status, and the two actions. */
29
45
  export type McpServerRowProps = Omit<ComponentProps<"div">, "className" | "title"> & {
30
46
  /** Server name; its first two letters fill the 40px tile unless `initials` is set. */
31
47
  name: string;
48
+ /** Overrides the two-letter tile text derived from `name`; up to two characters read best. */
32
49
  initials?: string;
33
50
  /** Tags next to the name, such as "Official". */
34
51
  tags?: string[];
35
52
  /** Tags tinted brand, such as "All devices". */
36
53
  brandTags?: string[];
54
+ /** Body line under the tags, in body-sm secondary colour. */
37
55
  description?: ReactNode;
38
56
  /** Meta line, joined by "·". */
39
57
  meta?: string[];
58
+ /** Text inside the status pill.
59
+ * @default "Connected" */
40
60
  statusLabel?: ReactNode;
41
- /** Colour of the status pill: a live dot by default, a danger tint for a warning, a muted dot for a neutral state. */
61
+ /** Colour of the status pill: a live dot by default, a danger tint for a warning, a muted dot for a neutral state.
62
+ * @default "connected" */
42
63
  statusTone?: McpServerStatusTone;
64
+ /** Text of the Configure button.
65
+ * @default "Configure" */
43
66
  configureLabel?: ReactNode;
67
+ /** Fires when the Configure button is clicked; the button always renders, so pass this to make it do anything. */
44
68
  onConfigure?: () => void;
45
69
  /** Overflow menu items (`MenuItem`). Hides the overflow button when absent. */
46
70
  menu?: ReactNode;
71
+ /** Accessible label of the overflow button.
72
+ * @default "More" */
47
73
  menuLabel?: string;
74
+ /** Extra classes on the row `div`, merged after the built-in ones. */
48
75
  className?: string;
49
76
  };
50
77
 
51
- /** Paper `MCP server row` (1VHS-0): a divided row with a 40px tile, tags, a status pill and two actions. */
78
+ /**
79
+ * Paper `MCP server row` (1VHS-0): a divided row with a 40px tile, tags, a status pill and two actions.
80
+ *
81
+ * A row of a settings list rather than a card: it draws only a bottom border and
82
+ * drops it on the last child. The main cluster wraps (`basis-40`) so the row stays
83
+ * usable in a narrow column; its overflow menu opens from the row and portals to
84
+ * `document.body`.
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * <McpServerRow
89
+ * name="Linear"
90
+ * tags={["Official"]}
91
+ * description="Read issues and projects."
92
+ * meta={["12 tools", "OAuth"]}
93
+ * onConfigure={() => openConfig("linear")}
94
+ * menu={<MenuItem>Reconnect</MenuItem>}
95
+ * />
96
+ * ```
97
+ */
52
98
  export function McpServerRow({
53
99
  name,
54
100
  initials,
@@ -3,15 +3,38 @@ import { createContext, useContext, type ComponentProps, type ReactNode } from "
3
3
 
4
4
  import { cn } from "../lib/cn";
5
5
 
6
- type WithClass<T> = Omit<T, "className"> & { className?: string };
6
+ type WithClass<T> = Omit<T, "className"> & {
7
+ /** Extra classes merged onto the element the part renders. */
8
+ className?: string;
9
+ };
7
10
 
11
+ /**
12
+ * Visual treatment of the popup.
13
+ *
14
+ * - `glass` — the Figma `Menu` on the veil: 20px radius, 40px rows, 220px wide.
15
+ * - `solid` — the Account / Workspace menu that opens over glass chrome: opaque fill, 18px radius, `shadow-popover`, 36px rows, 256px wide.
16
+ * - `paper` — the Paper workspace switcher: default fill, 1px default border, 14px radius, `0 12 32` shadow, 304px wide, 36px rows.
17
+ */
8
18
  export type MenuSurface = "glass" | "solid" | "paper";
9
19
 
10
20
  const SurfaceContext = createContext<MenuSurface>("glass");
11
21
 
22
+ /**
23
+ * Root of a menu; owns open state and the trigger/popup pair. Re-exported from
24
+ * Base UI's `Menu.Root`, so it accepts `open`, `defaultOpen`, `onOpenChange` and
25
+ * `modal`.
26
+ */
12
27
  export const Menu = BaseMenu.Root;
28
+
29
+ /**
30
+ * The element that opens the menu. Base UI gives it `aria-haspopup` and the
31
+ * expanded state; use `render` to make an existing button the trigger.
32
+ *
33
+ * Part of {@link MenuContent}.
34
+ */
13
35
  export const MenuTrigger = BaseMenu.Trigger;
14
36
 
37
+ /** Props for {@link MenuContent}: the surface variant and the Base UI positioner offsets. */
15
38
  export type MenuContentProps = WithClass<ComponentProps<typeof BaseMenu.Popup>> & {
16
39
  /**
17
40
  * `glass` is the Figma `Menu` on the veil (20px radius, 40px rows).
@@ -19,21 +42,58 @@ export type MenuContentProps = WithClass<ComponentProps<typeof BaseMenu.Popup>>
19
42
  * (opaque fill, 18px radius, soft shadow, 36px rows).
20
43
  * `paper` is the Paper workspace switcher: default fill, 1px default border, 14px radius,
21
44
  * 0 12 32 shadow, 304px wide, 36px rows.
45
+ * @default "solid"
22
46
  */
23
47
  surface?: MenuSurface;
48
+ /** Preferred side of the anchor the popup opens on; flips when it does not fit. */
24
49
  side?: ComponentProps<typeof BaseMenu.Positioner>["side"];
50
+ /** Alignment of the popup against the anchor.
51
+ * @default "start" */
25
52
  align?: ComponentProps<typeof BaseMenu.Positioner>["align"];
53
+ /** Gap in px between the anchor and the popup.
54
+ * @default 8 */
26
55
  sideOffset?: number;
56
+ /** Offset in px along the alignment axis. */
27
57
  alignOffset?: number;
28
58
  };
29
59
 
60
+ /**
61
+ * Radius, border, fill and shadow of the popup per {@link MenuSurface}. Width is
62
+ * not part of this map; `MenuContent` applies it directly (`w-55` glass, `w-76`
63
+ * paper, `w-64` solid).
64
+ */
30
65
  export const menuPopupClass = {
31
66
  glass: "rounded-xl veil-overlay",
32
67
  solid: "rounded-[18px] bg-glass-fill-solid shadow-popover",
33
68
  paper: "rounded-[14px] border border-border-default bg-bg-default shadow-[0_12px_32px_#0A142424]",
34
69
  } as const;
35
70
 
36
- /** Figma `Menus and popovers` (88:495): `Menu`, `Account menu` and `Workspace menu`. */
71
+ /**
72
+ * Figma `Menus and popovers` (88:495): `Menu`, `Account menu` and `Workspace menu`.
73
+ *
74
+ * The popup portals to `document.body` through `BaseMenu.Portal`, so it escapes
75
+ * any `overflow: hidden` ancestor and always paints above the page at `z-50`.
76
+ * Base UI handles keyboard navigation, focus return to the trigger and dismissal;
77
+ * the surface prop only changes the look. Its width is capped at the viewport
78
+ * minus 32px so the 304px `paper` menu still fits a 320px screen.
79
+ *
80
+ * @example
81
+ * ```tsx
82
+ * <Menu>
83
+ * <MenuTrigger render={<Button variant="solid" />}>Account</MenuTrigger>
84
+ * <MenuContent align="end">
85
+ * <MenuLabel>Workspaces</MenuLabel>
86
+ * <MenuItem icon={<GearSix />} trailing="⌘," onClick={openSettings}>
87
+ * Settings
88
+ * </MenuItem>
89
+ * <MenuSeparator />
90
+ * <MenuItem icon={<Trash />} danger onClick={remove}>
91
+ * Delete
92
+ * </MenuItem>
93
+ * </MenuContent>
94
+ * </Menu>
95
+ * ```
96
+ */
37
97
  export function MenuContent({
38
98
  surface = "solid",
39
99
  side,
@@ -72,18 +132,27 @@ export function MenuContent({
72
132
  );
73
133
  }
74
134
 
135
+ /** Props for {@link MenuItem}: optional leading icon, description line and trailing shortcut. */
75
136
  export type MenuItemProps = WithClass<ComponentProps<typeof BaseMenu.Item>> & {
76
137
  /** 18px leading icon, or a 28px workspace tile. */
77
138
  icon?: ReactNode;
78
- /** Caption under the label, such as "3 members". */
139
+ /** Caption under the label, such as "3 members". Grows the row to 48px. */
79
140
  description?: ReactNode;
80
141
  /** Right side: a shortcut such as "⌘C", or a check. */
81
142
  trailing?: ReactNode;
143
+ /** Paints the label and icon in `text-text-danger`; pair it with `onClick` or `disabled` for a destructive command. */
82
144
  danger?: boolean;
83
145
  /** Current choice drawn on the brand tint (Paper workspace switcher). */
84
146
  selected?: boolean;
85
147
  };
86
148
 
149
+ /**
150
+ * One row of a {@link MenuContent}: icon, label, optional description and trailing
151
+ * shortcut. It is a Base UI `Menu.Item`, so it closes the menu on click, is
152
+ * disabled through `disabled` and highlighted with the arrow keys.
153
+ *
154
+ * Part of {@link MenuContent}.
155
+ */
87
156
  export function MenuItem({
88
157
  icon,
89
158
  description,
@@ -131,6 +200,17 @@ export function MenuItem({
131
200
  );
132
201
  }
133
202
 
203
+ /**
204
+ * A 1px rule between groups of items, drawn full width inside the popup's own
205
+ * `p-1.5` inset. Its wrapper adds `px-1.5` on glass and `px-2.5` on solid and
206
+ * paper, so the line starts 12px from the popup edge on glass and 16px on the
207
+ * others: on solid and paper it lines up with the row padding (`px-2.5`), while
208
+ * the glass rows are inset further (`px-3`) and the rule runs wider than they do.
209
+ *
210
+ * It renders as a Base UI `Menu.Separator`, so it is hidden from the keyboard.
211
+ *
212
+ * Part of {@link MenuContent}.
213
+ */
134
214
  export function MenuSeparator({ className, ...props }: WithClass<ComponentProps<typeof BaseMenu.Separator>>) {
135
215
  const surface = useContext(SurfaceContext);
136
216
  return (