@horon-ai/react 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -2
  3. package/src/components/agent-activity.tsx +75 -7
  4. package/src/components/agent-lane.tsx +32 -3
  5. package/src/components/agent-lanes-timeline.tsx +85 -7
  6. package/src/components/agent-panel.tsx +46 -4
  7. package/src/components/agent-steering-panel.tsx +53 -3
  8. package/src/components/approval-card.tsx +34 -2
  9. package/src/components/avatar.tsx +20 -4
  10. package/src/components/badge.tsx +24 -3
  11. package/src/components/banner.tsx +19 -3
  12. package/src/components/bot-instructions-panel.tsx +86 -7
  13. package/src/components/bot-list-row.tsx +13 -1
  14. package/src/components/bot-memory-panel.tsx +58 -3
  15. package/src/components/bottom-sheet.tsx +61 -7
  16. package/src/components/breadcrumb-header.tsx +32 -2
  17. package/src/components/breadcrumb.tsx +31 -1
  18. package/src/components/button.tsx +46 -1
  19. package/src/components/calendar.tsx +76 -6
  20. package/src/components/card.tsx +30 -1
  21. package/src/components/chat-bubble.tsx +32 -3
  22. package/src/components/chat-controls.tsx +60 -7
  23. package/src/components/checkbox.tsx +22 -1
  24. package/src/components/chip.tsx +29 -2
  25. package/src/components/choice-card.tsx +25 -1
  26. package/src/components/code-panel.tsx +28 -3
  27. package/src/components/combobox.tsx +52 -2
  28. package/src/components/command-palette.tsx +65 -1
  29. package/src/components/composer.tsx +28 -1
  30. package/src/components/data-table.tsx +198 -16
  31. package/src/components/date-field.tsx +31 -2
  32. package/src/components/dialog.tsx +45 -3
  33. package/src/components/directory-card.tsx +23 -2
  34. package/src/components/document-extract-panel.tsx +83 -5
  35. package/src/components/empty-state.tsx +21 -2
  36. package/src/components/field.tsx +49 -3
  37. package/src/components/file-drop.tsx +39 -4
  38. package/src/components/filter-bar.tsx +38 -1
  39. package/src/components/flue-background.tsx +88 -4
  40. package/src/components/horon-icon.tsx +50 -3
  41. package/src/components/horon-wordmark.tsx +26 -1
  42. package/src/components/icon-button.tsx +38 -1
  43. package/src/components/input-group.tsx +57 -6
  44. package/src/components/input.tsx +48 -2
  45. package/src/components/integration-row.tsx +43 -2
  46. package/src/components/kbd.tsx +28 -2
  47. package/src/components/knowledge-base-panel.tsx +40 -2
  48. package/src/components/liquid-glass.tsx +16 -0
  49. package/src/components/list-detail.tsx +89 -7
  50. package/src/components/marketplace-card.tsx +40 -4
  51. package/src/components/mcp-server-row.tsx +49 -3
  52. package/src/components/menu.tsx +83 -3
  53. package/src/components/message-extras.tsx +50 -4
  54. package/src/components/modal.tsx +71 -4
  55. package/src/components/orchestrator-panel.tsx +48 -4
  56. package/src/components/page-header.tsx +26 -3
  57. package/src/components/plan-card.tsx +28 -1
  58. package/src/components/plan-step.tsx +41 -1
  59. package/src/components/plugin-row.tsx +61 -3
  60. package/src/components/progress.tsx +32 -2
  61. package/src/components/radio-group.tsx +55 -2
  62. package/src/components/research-run-panel.tsx +73 -3
  63. package/src/components/schedule.tsx +163 -12
  64. package/src/components/segmented-control.tsx +57 -2
  65. package/src/components/select.tsx +60 -0
  66. package/src/components/settings-list.tsx +66 -8
  67. package/src/components/sidebar.tsx +214 -23
  68. package/src/components/skeleton.tsx +61 -4
  69. package/src/components/slider.tsx +30 -0
  70. package/src/components/source-row.tsx +43 -4
  71. package/src/components/spinner.tsx +29 -2
  72. package/src/components/split-button.tsx +37 -3
  73. package/src/components/switch.tsx +31 -1
  74. package/src/components/tab-bar.tsx +47 -2
  75. package/src/components/tabs.tsx +59 -5
  76. package/src/components/task-row.tsx +49 -3
  77. package/src/components/textarea-field.tsx +36 -2
  78. package/src/components/textarea.tsx +35 -2
  79. package/src/components/toast.tsx +52 -3
  80. package/src/components/tool-call.tsx +87 -4
  81. package/src/components/tool-calls-header.tsx +32 -1
  82. package/src/components/tool-permissions.tsx +50 -2
  83. package/src/components/tooltip.tsx +33 -1
  84. package/src/components/top-bar.tsx +35 -2
  85. package/src/components/usage-meter.tsx +28 -2
  86. package/src/components/workspace-menu.tsx +46 -1
  87. package/src/hooks/use-active-indicator.ts +44 -1
  88. package/src/hooks/use-color-scheme.ts +24 -1
  89. package/src/hooks/use-copy-to-clipboard.ts +23 -0
  90. package/src/hooks/use-device.ts +29 -1
  91. package/src/hooks/use-hotkey.ts +21 -0
  92. package/src/hooks/use-media-query.ts +18 -0
  93. package/src/hooks/use-platform.ts +30 -1
  94. package/src/hooks/use-reduced-motion.ts +18 -1
  95. package/src/index.ts +31 -0
  96. package/src/lib/cn.ts +13 -1
  97. package/src/lib/cursor-ghost.ts +16 -6
  98. package/src/lib/platform.ts +71 -4
@@ -81,12 +81,33 @@ import {
81
81
  } from "@phosphor-icons/react";
82
82
  import type { ComponentProps } from "react";
83
83
 
84
- export type HoronIconProps = Omit<ComponentProps<"svg">, "children"> & { size?: number | string };
84
+ /** Props for the Horon mark: any svg attribute plus a size like a Phosphor icon. */
85
+ export type HoronIconProps = Omit<ComponentProps<"svg">, "children"> & {
86
+ /** Width and height together: a px number or a CSS length. @default "1em" */
87
+ size?: number | string;
88
+ };
85
89
 
86
90
  /**
87
91
  * Figma `icon/horon` (256:9116): the line falcon eye that stands in for Horon in
88
92
  * navigation. Phosphor has no equivalent, so the path is copied from the Tab bar
89
93
  * (63:441) export. It takes `currentColor` and sizes like a Phosphor icon.
94
+ *
95
+ * The svg is `aria-hidden` by default and its 22-unit viewBox scales with `size`, so it drops into any place
96
+ * a Phosphor glyph fits. The default `1em` makes it follow the font size, which is what a row of mixed icons
97
+ * needs.
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * import { HoronIcon } from "@horon-ai/react";
102
+ *
103
+ * function BrandMark() {
104
+ * return (
105
+ * <span className="flex size-10 items-center justify-center rounded-full bg-bg-subtle text-text-primary">
106
+ * <HoronIcon size={22} />
107
+ * </span>
108
+ * );
109
+ * }
110
+ * ```
90
111
  */
91
112
  export function HoronIcon({ size = "1em", ...props }: HoronIconProps) {
92
113
  return (
@@ -100,6 +121,9 @@ export function HoronIcon({ size = "1em", ...props }: HoronIconProps) {
100
121
  * Paper's icon contract: every icon is a component named `icon/<name>`. `iconNames` is that list in
101
122
  * Paper order. Paper draws three sizes (16 in small controls and captions, 20 in buttons and rows,
102
123
  * 24 in bars and empty states); 12, 14, 18 and 22 elsewhere in the code stay as they are.
124
+ *
125
+ * The tuple is the single source of the registry: `HoronIconName` derives from it and `phosphorGlyphs`
126
+ * must map every entry except `horon`, so a name added here fails to type-check until it has a glyph.
103
127
  */
104
128
  export const iconNames = [
105
129
  "plus",
@@ -183,6 +207,7 @@ export const iconNames = [
183
207
  "plus-circle",
184
208
  ] as const;
185
209
 
210
+ /** Every registered icon name: a literal union derived from `iconNames`. */
186
211
  export type HoronIconName = (typeof iconNames)[number];
187
212
 
188
213
  const phosphorGlyphs: Record<Exclude<HoronIconName, "horon">, Icon> = {
@@ -266,13 +291,35 @@ const phosphorGlyphs: Record<Exclude<HoronIconName, "horon">, Icon> = {
266
291
  "plus-circle": PlusCircle,
267
292
  };
268
293
 
294
+ /** Props for `HoronGlyph`: the registry name plus the same svg attributes a Phosphor icon takes. */
269
295
  export type HoronGlyphProps = Omit<ComponentProps<"svg">, "children" | "ref"> & {
296
+ /** Which registered glyph to draw; `horon` resolves to the Horon mark instead of a Phosphor icon. */
270
297
  name: HoronIconName;
271
- /** Paper sizes are 16, 20 and 24. */
298
+ /** Paper sizes are 16, 20 and 24. @default 20 */
272
299
  size?: number | string;
273
300
  };
274
301
 
275
- /** Figma `icon/<name>`: resolves a registry name to its glyph. Takes `currentColor`, so the parent's text color drives it. */
302
+ /**
303
+ * Figma `icon/<name>`: resolves a registry name to its glyph. Takes `currentColor`, so the parent's text color drives it.
304
+ *
305
+ * The runtime lookup behind an icon name stored as data (a tool's icon, a settings row): `horon` returns
306
+ * `HoronIcon`, everything else the matching Phosphor component. Like Phosphor, the svg is `aria-hidden`;
307
+ * put the meaning on the surrounding button or label. Outside data-driven cases, prefer importing the
308
+ * Phosphor icon directly so the bundler can tree-shake it.
309
+ *
310
+ * @example
311
+ * ```tsx
312
+ * import { HoronGlyph, type HoronIconName } from "@horon-ai/react";
313
+ *
314
+ * function ToolIcon({ icon }: { icon: HoronIconName }) {
315
+ * return (
316
+ * <span className="text-text-secondary">
317
+ * <HoronGlyph name={icon} size={20} />
318
+ * </span>
319
+ * );
320
+ * }
321
+ * ```
322
+ */
276
323
  export function HoronGlyph({ name, size = 20, ...props }: HoronGlyphProps) {
277
324
  if (name === "horon") return <HoronIcon size={size} {...props} />;
278
325
  const Glyph = phosphorGlyphs[name];
@@ -1,6 +1,14 @@
1
1
  import type { ComponentProps } from "react";
2
2
 
3
- export type HoronWordmarkProps = Omit<ComponentProps<"svg">, "children"> & { title?: string };
3
+ /** Props of {@link HoronWordmark}: any svg attribute plus the accessible name. */
4
+ export type HoronWordmarkProps = Omit<ComponentProps<"svg">, "children"> & {
5
+ /**
6
+ * Accessible name of the wordmark. It becomes the `aria-label` on the `role="img"` svg, so screen readers
7
+ * announce it as one image rather than six paths.
8
+ * @default "Horon"
9
+ */
10
+ title?: string;
11
+ };
4
12
 
5
13
  const PATHS = [
6
14
  "M0.0230414 11.2212H1.91244L1.92626 8.60366L1.93548 5.99076H8.47923L8.48845 8.60366L8.50228 11.2212H10.3917L10.4193 8.06449C10.447 5.32717 10.5207 2.68663 10.6267 0.585252L10.6543 0H8.47923V4.2857H1.93548V0H0L0.0230414 11.2212Z",
@@ -14,6 +22,23 @@ const PATHS = [
14
22
  /**
15
23
  * The UI-size Horon wordmark from the Figma Sidebar (88:808): a 48.7 x 11.5
16
24
  * vector in `currentColor`. Scale it with `height`; the width follows.
25
+ *
26
+ * Six filled paths trace the letter outlines as one drawing, painted in the current text color, so color it
27
+ * with the surrounding `text-*` class rather than a `fill` prop. The paths carry no individual names; only
28
+ * `title` gives the svg its accessible name.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * import { HoronWordmark } from "@horon-ai/react";
33
+ *
34
+ * function SidebarBrand() {
35
+ * return (
36
+ * <div className="flex items-center gap-2 text-text-primary">
37
+ * <HoronWordmark height={12} />
38
+ * </div>
39
+ * );
40
+ * }
41
+ * ```
17
42
  */
18
43
  export function HoronWordmark({ title = "Horon", height = 11.5, ...props }: HoronWordmarkProps) {
19
44
  return (
@@ -5,15 +5,31 @@ import { cn } from "../lib/cn";
5
5
  import type { ControlSize } from "../lib/size";
6
6
  import { LiquidGlass } from "./liquid-glass";
7
7
 
8
+ /**
9
+ * Fill and text colour of an icon button:
10
+ *
11
+ * - `primary`: solid brand fill with the inverse icon, for the one main action in a cluster.
12
+ * - `ghost` (default): no fill until hover, primary icon; the common toolbar and row action.
13
+ * - `glass`: the clear glass disc with a `LiquidGlass` layer, for floating chrome over imagery.
14
+ * - `solid`: bordered raised surface, for a button that sits on the page background.
15
+ * - `quiet`: no fill and no liquid layer, secondary icon that turns primary on hover; for dark shells
16
+ * and title bars where a glass disc would read as a grey bubble.
17
+ */
8
18
  export type IconButtonVariant = "primary" | "ghost" | "glass" | "solid" | "quiet";
19
+ /** Square footprint: `sm` 32px, `md` 40px, `lg` 48px. */
9
20
  export type IconButtonSize = ControlSize;
10
21
 
22
+ /** Props of {@link IconButton}: the treatment, the footprint and the glyph. */
11
23
  export type IconButtonProps = Omit<ComponentProps<typeof BaseButton>, "className" | "children"> & {
24
+ /** Treatment, from the list on `IconButtonVariant`. @default "ghost" */
12
25
  variant?: IconButtonVariant;
26
+ /** Square footprint: `sm` 32px, `md` 40px, `lg` 48px, with the icon at 16/20/24px. @default "md" */
13
27
  size?: IconButtonSize;
28
+ /** The glyph. It is wrapped in a positioning span; size it through the button rather than by hand. */
14
29
  icon: ReactNode;
15
30
  /** Accessible name; the button has no visible text. */
16
31
  label: string;
32
+ /** Extra classes for the button. */
17
33
  className?: string;
18
34
  };
19
35
 
@@ -37,7 +53,28 @@ const variantClass: Record<IconButtonVariant, string> = {
37
53
  "text-text-secondary hover:not-data-disabled:bg-overlay-ink-6 hover:not-data-disabled:text-text-primary active:not-data-disabled:bg-overlay-ink-10",
38
54
  };
39
55
 
40
- /** Figma `Icon button` (57:167): Primary, Ghost, Glass and Solid in three sizes, plus a fill-free `quiet` variant. */
56
+ /**
57
+ * Figma `Icon button` (57:167): Primary, Ghost, Glass and Solid in three sizes, plus a fill-free `quiet` variant.
58
+ *
59
+ * A round, icon-only Base UI button. `label` becomes `aria-label`, since nothing is visible; the visible
60
+ * disc is 32/40/48px while an invisible pseudo-element keeps a 44px touch target around it. It renders
61
+ * `LiquidGlass` inside the `glass` variant only, and disabled state is 40% opacity with `cursor-not-allowed`.
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * import { Gear, Plus } from "@phosphor-icons/react";
66
+ * import { IconButton } from "@horon-ai/react";
67
+ *
68
+ * function Actions() {
69
+ * return (
70
+ * <div className="flex items-center gap-1">
71
+ * <IconButton icon={<Plus />} label="New agent" variant="primary" />
72
+ * <IconButton icon={<Gear />} label="Settings" />
73
+ * </div>
74
+ * );
75
+ * }
76
+ * ```
77
+ */
41
78
  export function IconButton({ variant = "ghost", size = "md", icon, label, className, ...props }: IconButtonProps) {
42
79
  return (
43
80
  <BaseButton
@@ -5,8 +5,10 @@ import { cn } from "../lib/cn";
5
5
  import type { ControlSize } from "../lib/size";
6
6
  import { fieldBoxClass, fieldTextClass, type InputSurface } from "./input";
7
7
 
8
+ /** Height rung of the group and its segments: `sm` 32px, `md` 40px, `lg` 48px. */
8
9
  export type InputGroupSize = ControlSize;
9
10
 
11
+ /** Props of {@link InputGroup}: the input's own props plus the segments around it. */
10
12
  export type InputGroupProps = Omit<ComponentProps<typeof BaseInput>, "className" | "prefix" | "size"> & {
11
13
  /**
12
14
  * Segment before the input: `<Select variant="segment" />` or `<Combobox variant="segment" />`
@@ -15,19 +17,35 @@ export type InputGroupProps = Omit<ComponentProps<typeof BaseInput>, "className"
15
17
  prefix?: ReactNode;
16
18
  /** Segment after the input, such as a currency select. */
17
19
  suffix?: ReactNode;
20
+ /** Height and text rung: 32/40/48px, matching a standalone `Input`. @default "md" */
18
21
  size?: InputGroupSize;
22
+ /** Paints the danger border on the whole group and marks the input `aria-invalid`. */
19
23
  invalid?: boolean;
20
- /** Solid (default) or glass, as on `Input`. */
24
+ /** Solid (default) or glass, as on `Input`. @default "solid" */
21
25
  surface?: InputSurface;
26
+ /** Extra classes for the box that wraps the segments and the input. */
22
27
  className?: string;
23
28
  };
24
29
 
25
- type InputGroupContextValue = { size: InputGroupSize; disabled: boolean; surface: InputSurface };
30
+ /** What a segment reads from the group it sits in. */
31
+ type InputGroupContextValue = {
32
+ /** Height rung the group was given. */
33
+ size: InputGroupSize;
34
+ /** Whether the group is disabled, so segments can dim and drop their own interactivity. */
35
+ disabled: boolean;
36
+ /** Surface the group uses, so a segment and the box stay the same material. */
37
+ surface: InputSurface;
38
+ };
26
39
 
27
40
  /** Lets a segment (select, combobox or fixed text) take the group's size, disabled state and surface. */
28
41
  const InputGroupContext = createContext<InputGroupContextValue | null>(null);
29
42
 
30
- /** The group a segment sits in, or null outside an `InputGroup`. */
43
+ /**
44
+ * The group a segment sits in, or null outside an `InputGroup`. `Select` and `Combobox` call it in their
45
+ * `segment` variant to pick the right height, padding and disabled look without a prop of their own.
46
+ *
47
+ * @returns The `{ size, disabled, surface }` of the enclosing group, or `null` when there is none.
48
+ */
31
49
  export function useInputGroup() {
32
50
  return useContext(InputGroupContext);
33
51
  }
@@ -55,7 +73,30 @@ export const inputGroupSegmentClass: Record<InputGroupSize, string> = {
55
73
  const divider =
56
74
  "relative h-full w-px shrink-0 bg-border-default group-data-invalid:bg-border-danger group-data-disabled:bg-border-default/60";
57
75
 
58
- /** Figma `Input group` (60:429): a glass or solid field split into a select segment and an input segment, in three sizes. */
76
+ /**
77
+ * Figma `Input group` (60:429): a glass or solid field split into a select segment and an input segment, in three sizes.
78
+ *
79
+ * One box drawn from `fieldBoxClass` holds the optional `prefix`, the input and the optional `suffix`, with a
80
+ * 1px hairline between them; the hairlines turn danger-red with `invalid` and dim with the group's `disabled`.
81
+ * The group renders its own Base UI input, so pass `placeholder`/`value`/`onChange` as props rather than
82
+ * children. Size, disabled state and surface go to the segments through `InputGroupContext`, which is why a
83
+ * `Select` or `Combobox` with `variant="segment"` matches without extra props.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * import { InputGroup, InputGroupSegment } from "@horon-ai/react";
88
+ *
89
+ * function Website() {
90
+ * return (
91
+ * <InputGroup
92
+ * prefix={<InputGroupSegment>https://</InputGroupSegment>}
93
+ * placeholder="horon.ai"
94
+ * size="md"
95
+ * />
96
+ * );
97
+ * }
98
+ * ```
99
+ */
59
100
  export function InputGroup({
60
101
  prefix,
61
102
  suffix,
@@ -99,8 +140,18 @@ export function InputGroup({
99
140
  );
100
141
  }
101
142
 
102
- /** A fixed-text segment, such as a unit or a `https://` prefix. Takes the group's size. */
103
- export function InputGroupSegment({ className, children }: { className?: string; children: ReactNode }) {
143
+ /**
144
+ * Part of {@link InputGroup}. A fixed-text segment, such as a unit or a `https://` prefix. Takes the group's
145
+ * size (falling back to `md` outside a group), sits on the 4% ink tint, and dims its text when the group is
146
+ * disabled.
147
+ */
148
+ // prettier-ignore
149
+ export function InputGroupSegment({ className, children }: {
150
+ /** Extra classes for the segment. */
151
+ className?: string;
152
+ /** Segment content: short text, or text plus an icon sized by the segment's rung. */
153
+ children: ReactNode;
154
+ }) {
104
155
  const group = useInputGroup();
105
156
  return (
106
157
  <div
@@ -4,17 +4,29 @@ import type { ComponentProps, ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import type { ControlSize } from "../lib/size";
6
6
 
7
+ /** Height rung shared with every sized control: `sm` 32px, `md` 40px, `lg` 48px. */
7
8
  export type InputSize = ControlSize;
9
+
10
+ /**
11
+ * Where the input sits:
12
+ *
13
+ * - `glass`: on imagery or a glass panel; uses the veil field (`.veil-field`) with no liquid layer inside.
14
+ * - `solid`: on a raised surface (`bg-bg-surface`), the default.
15
+ * - `solid-on-page`: directly on the page background (`bg-bg-default`), so the box reads against it.
16
+ */
8
17
  export type InputSurface = "glass" | "solid" | "solid-on-page";
9
18
 
19
+ /** Props of {@link Input}: the native input attributes plus the box's size, surface and decorations. */
10
20
  export type InputProps = Omit<ComponentProps<typeof BaseInput>, "className" | "size"> & {
21
+ /** Height and text rung: 32/40/48px with `body-sm/md/lg` text. @default "md" */
11
22
  size?: InputSize;
12
- /** `glass` over imagery and glass panels, `solid` on raised surfaces, `solid-on-page` directly on the page background. */
23
+ /** `glass` over imagery and glass panels, `solid` on raised surfaces, `solid-on-page` directly on the page background. @default "solid" */
13
24
  surface?: InputSurface;
14
25
  /** Leading icon, 16px (sm, md) or 20px (lg), in the secondary text color. */
15
26
  icon?: ReactNode;
16
27
  /** Shows the error border. Inside a `Field`, an invalid field does the same. */
17
28
  invalid?: boolean;
29
+ /** Extra classes for the box that wraps the input. */
18
30
  className?: string;
19
31
  /** Classes for the native `<input>`. */
20
32
  inputClassName?: string;
@@ -34,19 +46,53 @@ export const fieldTextClass: Record<ControlSize, string> = {
34
46
  lg: "text-body-lg",
35
47
  };
36
48
 
49
+ /**
50
+ * The class set a field-shaped control composes: the box (border, radius, focus ring), the surface fill and
51
+ * the inner text/input. Shared by `Input` and `InputGroup` so every field-style control stays one material.
52
+ */
37
53
  export const fieldBoxClass = {
38
54
  /** The box is the focus target's container, so the ring is drawn on `:focus-within`, not on the inner input. */
39
55
  base: "motion-colors relative flex min-w-0 max-w-full items-center overflow-clip rounded-md border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-text-brand",
40
56
  /** The veil field (globals.css `.veil-field`): no liquid layers, so hosts render no `LiquidGlass`. */
41
57
  glass: "veil-field has-[[data-disabled]]:opacity-60 has-[:disabled]:opacity-60",
58
+ /** Raised surface fill with a default border; disabled or invalid states repaint both fill and border. */
42
59
  solid:
43
60
  "border-border-default bg-bg-surface has-[[data-disabled]]:border-border-default has-[:disabled]:border-border-default has-[[data-disabled]]:bg-bg-subtle has-[:disabled]:bg-bg-subtle has-[[data-invalid]]:border-border-danger data-invalid:border-border-danger",
61
+ /** The solid fill on the page background instead of a raised surface; the same disabled and invalid states. */
44
62
  "solid-on-page":
45
63
  "border-border-default bg-bg-default has-[[data-disabled]]:border-border-default has-[:disabled]:border-border-default has-[[data-disabled]]:bg-bg-subtle has-[:disabled]:bg-bg-subtle has-[[data-invalid]]:border-border-danger data-invalid:border-border-danger",
64
+ /** The inner control: transparent, fills the remaining width, primary text, placeholder and disabled colours. */
46
65
  text: "min-w-0 flex-1 bg-transparent text-text-primary outline-none placeholder:text-text-placeholder disabled:cursor-not-allowed disabled:text-text-disabled",
47
66
  } as const;
48
67
 
49
- /** Figma `Input` (58:126): SM, MD and LG, on Glass or Solid surface. */
68
+ /**
69
+ * Figma `Input` (58:126): SM, MD and LG, on Glass or Solid surface.
70
+ *
71
+ * A bordered box around Base UI's input: render the box, then the optional leading icon, then the native
72
+ * `<input>`, which takes the remaining width. Focus is drawn on the box (`:focus-within`), so the ring never
73
+ * detaches from the field. `invalid` marks both the box (`data-invalid`) and the input (`aria-invalid`); a
74
+ * `Field` that reports invalid does the same through Base UI. Every other prop (`value`, `onChange`,
75
+ * `placeholder`, `disabled`, `type`, …) goes to the native input, while `className`/`inputClassName` split
76
+ * between the box and the input itself.
77
+ *
78
+ * @example
79
+ * ```tsx
80
+ * import { MagnifyingGlass } from "@phosphor-icons/react";
81
+ * import { Input } from "@horon-ai/react";
82
+ *
83
+ * function Search() {
84
+ * return (
85
+ * <Input
86
+ * type="search"
87
+ * size="md"
88
+ * icon={<MagnifyingGlass />}
89
+ * placeholder="Search agents"
90
+ * aria-label="Search agents"
91
+ * />
92
+ * );
93
+ * }
94
+ * ```
95
+ */
50
96
  export function Input({
51
97
  size = "md",
52
98
  surface = "solid",
@@ -4,21 +4,36 @@ import type { ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { LiquidGlass } from "./liquid-glass";
6
6
 
7
+ /**
8
+ * Connection state of an integration:
9
+ *
10
+ * - `connected`: sky tile, a live-dot "Connected" pill and no button.
11
+ * - `reconnect`: dark-sky tile, a red outline on the row, danger text in `detail` and a danger action button.
12
+ * - `not-connected`: subtle tile, secondary text and the primary "Connect" button.
13
+ */
7
14
  export type IntegrationStatus = "connected" | "reconnect" | "not-connected";
8
15
 
16
+ /** Props of {@link IntegrationRow}: the service, its connection state and the one action. */
9
17
  export type IntegrationRowProps = {
10
18
  /** 20px icon shown in inverse on the 40px tile. */
11
19
  icon: ReactNode;
20
+ /** Service name on the first line, truncated with an ellipsis. */
12
21
  name: ReactNode;
13
22
  /** Account and scopes, or the reason a reconnect is needed. */
14
23
  detail?: ReactNode;
24
+ /** Drives the tile fill, the pill or button on the right and the row's danger border. */
15
25
  status: IntegrationStatus;
26
+ /** Called when the button is pressed; connected rows have no button, so it never fires then. */
16
27
  onAction?: () => void;
28
+ /** Pill text for `connected`. @default "Connected" */
17
29
  connectedLabel?: string;
30
+ /** Button text for `reconnect`. @default "Reconnect" */
18
31
  reconnectLabel?: string;
32
+ /** Button text for `not-connected`. @default "Connect" */
19
33
  connectLabel?: string;
20
- /** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass row. */
34
+ /** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass row. @default "solid" */
21
35
  surface?: "solid" | "glass";
36
+ /** Extra classes for the row. */
22
37
  className?: string;
23
38
  };
24
39
 
@@ -28,7 +43,33 @@ const tileClass: Record<IntegrationStatus, string> = {
28
43
  "not-connected": "bg-bg-subtle text-text-secondary",
29
44
  };
30
45
 
31
- /** Figma `Integration row` (Integrations 110:883): 40px tile, status pill on the right, solid or thick glass. */
46
+ /**
47
+ * Figma `Integration row` (Integrations 110:883): 40px tile, status pill on the right, solid or thick glass.
48
+ *
49
+ * A one-line service row: 40px rounded tile with the icon, a name and an optional detail line that share a
50
+ * `basis-40` column so the row wraps on narrow widths, and one action slot on the right. The pill and the
51
+ * button are both mounted and swapped with `motion-swap`; the inactive one is `aria-hidden` and untabbable,
52
+ * so the row never exposes a dead control. `glass` adds a `LiquidGlass` layer and moves the reconnect outline
53
+ * to the glass edge.
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * import { SlackLogo } from "@phosphor-icons/react";
58
+ * import { IntegrationRow } from "@horon-ai/react";
59
+ *
60
+ * function Slack() {
61
+ * return (
62
+ * <IntegrationRow
63
+ * icon={<SlackLogo />}
64
+ * name="Slack"
65
+ * detail="horon.slack.com · 4 scopes"
66
+ * status="reconnect"
67
+ * onAction={() => reconnect("slack")}
68
+ * />
69
+ * );
70
+ * }
71
+ * ```
72
+ */
32
73
  export function IntegrationRow({
33
74
  icon,
34
75
  name,
@@ -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" />;