@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
@@ -3,26 +3,41 @@ import type { ReactNode } from "react";
3
3
 
4
4
  import { cn } from "../lib/cn";
5
5
 
6
+ /**
7
+ * Overall state of an indexed source: `ready` when every stage finished, `indexing` while the
8
+ * pipeline runs, and `failed` when a stage broke and Retry is offered.
9
+ */
6
10
  export type SourceStatus = "ready" | "indexing" | "failed";
7
11
 
12
+ /** Props of {@link SourceRow}: the source's identity, its pipeline state and the Retry action. */
8
13
  export type SourceRowProps = {
14
+ /** Drives the default stage states, the detail color and which trailing control appears. */
9
15
  status: SourceStatus;
10
16
  /** 16px source icon on the 32px tile. */
11
17
  icon: ReactNode;
18
+ /** Source name, one truncated line at `label-md`. */
12
19
  name: ReactNode;
13
20
  /** Pages and chunks, progress, or the error. */
14
21
  detail: ReactNode;
15
- /** 0-100 while indexing. */
22
+ /** 0-100 while indexing. @default 0 */
16
23
  progress?: number;
24
+ /** Called by the Retry button, which only appears when `status` is `failed`. */
17
25
  onRetry?: () => void;
18
- /** `card` is a standalone subtle row; `list` is transparent with a 1px divider, for stacking inside a list. */
26
+ /** `card` is a standalone subtle row; `list` is transparent with a 1px divider, for stacking inside a list. @default "card" */
19
27
  variant?: "card" | "list";
20
- /** Pipeline stage states; defaults to Parse, Chunk, Embed, Index derived from `status`. */
28
+ /** Pipeline stage states; defaults to Parse, Chunk, Embed, Index derived from `status`. @default `defaultStages[status]`: `ready` is all `done`, `indexing` is `done, done, active, todo`, `failed` is `done, done, failed, todo` */
21
29
  stages?: SourceStages;
30
+ /** Extra classes on the row, which owns the radius, border or divider. */
22
31
  className?: string;
23
32
  };
24
33
 
34
+ /**
35
+ * One stage of the parse → chunk → embed → index pipeline: `done` is a 35% ink bar, `active`
36
+ * shows the live progress fill, `todo` is 8% ink and `failed` takes the red accent.
37
+ */
25
38
  export type SourceStage = "done" | "active" | "todo" | "failed";
39
+
40
+ /** The four pipeline stages in display order: parse, chunk, embed, index. */
26
41
  export type SourceStages = [SourceStage, SourceStage, SourceStage, SourceStage];
27
42
  const stageNames = ["parse", "chunk", "embed", "index"] as const;
28
43
 
@@ -36,7 +51,31 @@ const stageFill = { done: "bg-overlay-ink-35", todo: "bg-overlay-ink-8", failed:
36
51
  const stage = (state: keyof typeof stageFill) =>
37
52
  cn("h-[3px] w-6 overflow-clip rounded-full transition-colors duration-150 sm:w-11", stageFill[state]);
38
53
 
39
- /** Figma `Source row` (Knowledge base): a flat row with a four-stage pipeline (parse, chunk, embed, index). */
54
+ /**
55
+ * Figma `Source row` (Knowledge base): a flat row with a four-stage pipeline (parse, chunk, embed, index).
56
+ *
57
+ * The four 24px stage bars (44px from `sm` up) show the pipeline; the `active` bar fills to
58
+ * `progress`. The trailing slot is state-specific: a check when ready, a live percentage while
59
+ * indexing, and a Retry button when failed, where the detail line turns danger red with a warning icon.
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * import { FilePdf } from "@phosphor-icons/react";
64
+ * import { SourceRow } from "@horon-ai/react";
65
+ *
66
+ * function IndexingSource() {
67
+ * return (
68
+ * <SourceRow
69
+ * status="indexing"
70
+ * icon={<FilePdf />}
71
+ * name="Q4 launch brief.pdf"
72
+ * detail="42 pages · embedding"
73
+ * progress={68}
74
+ * />
75
+ * );
76
+ * }
77
+ * ```
78
+ */
40
79
  export function SourceRow({
41
80
  status,
42
81
  icon,
@@ -1,17 +1,44 @@
1
1
  import { cn } from "../lib/cn";
2
2
 
3
+ /**
4
+ * Named spinner rungs, in 14/16/24/32px (`xs`, `sm`, `md`, `lg`); a raw number is also
5
+ * accepted for one-off sizes.
6
+ */
3
7
  export type SpinnerSize = "xs" | "sm" | "md" | "lg";
4
8
 
9
+ /** Props of {@link Spinner}: a rung or pixel size, the accessible label, and extra classes. */
5
10
  export type SpinnerProps = {
6
- /** Named size, or a pixel size for the one-off spinners in agent rows (12, 15, 20). */
11
+ /** Named size, or a pixel size for the one-off spinners in agent rows (12, 15, 20). @default "sm" */
7
12
  size?: SpinnerSize | number;
13
+ /** Accessible name announced while spinning (`role="status"`). @default "Loading" */
8
14
  label?: string;
15
+ /** Extra classes on the `<svg>`; `animate-spin` and `shrink-0` are always applied. */
9
16
  className?: string;
10
17
  };
11
18
 
12
19
  const sizeClass: Record<SpinnerSize, string> = { xs: "size-3.5", sm: "size-4", md: "size-6", lg: "size-8" };
13
20
 
14
- /** Figma `Spinner` (Loading 109:733): a 10% ink ring with a quarter ink arc, 11% stroke. */
21
+ /**
22
+ * Figma `Spinner` (Loading 109:733): a 10% ink ring with a quarter ink arc, 11% stroke.
23
+ *
24
+ * Sizes are fixed pixel rungs (`xs` 14px, `sm` 16px, `md` 24px, `lg` 32px) or a raw number;
25
+ * it never shrinks, spins for as long as it is mounted, and announces `label` through
26
+ * `role="status"`.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * import { Spinner } from "@horon-ai/react";
31
+ *
32
+ * function Saving() {
33
+ * return (
34
+ * <span className="flex items-center gap-2 text-text-secondary">
35
+ * <Spinner size="sm" label="Saving" />
36
+ * Saving
37
+ * </span>
38
+ * );
39
+ * }
40
+ * ```
41
+ */
15
42
  export function Spinner({ size = "sm", label = "Loading", className }: SpinnerProps) {
16
43
  return (
17
44
  <svg
@@ -5,21 +5,55 @@ import type { ComponentProps, ReactNode } from "react";
5
5
  import { cn } from "../lib/cn";
6
6
  import { Menu, MenuContent, MenuTrigger } from "./menu";
7
7
 
8
+ /**
9
+ * Props of {@link SplitButton}: the Base UI button props (`onClick`, `disabled`, `type`, plus
10
+ * aria attributes) with the label, icon and menu the split pill adds.
11
+ */
8
12
  export type SplitButtonProps = Omit<ComponentProps<typeof BaseButton>, "className" | "children"> & {
9
- /** Primary action label. */
13
+ /** Primary action label, rendered at `label-md` inside the left half. */
10
14
  children: ReactNode;
11
15
  /** 16px leading icon. */
12
16
  icon?: ReactNode;
13
- /** Menu items (`MenuItem`, `MenuSeparator`) opened by the caret. */
17
+ /** Menu items (`MenuItem`, `MenuSeparator`) opened by the caret. Without it the pill is a single full-round button. */
14
18
  menu?: ReactNode;
19
+ /** Accessible name of the caret trigger. @default "More actions" */
15
20
  menuLabel?: string;
21
+ /** Extra classes on the pill container, which owns the fill, the press scale and the disabled dim. */
16
22
  className?: string;
17
23
  };
18
24
 
19
25
  const halfClass =
20
26
  "relative inline-flex h-full cursor-pointer items-center select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand data-disabled:cursor-not-allowed";
21
27
 
22
- /** Paper `Split button` (1VGK-0): one primary pill on `Button`'s 32px rung, two hit areas split by a 1x18 bar. */
28
+ /**
29
+ * Paper `Split button` (1VGK-0): one primary pill on `Button`'s 32px rung, two hit areas split by a 1x18 bar.
30
+ *
31
+ * The container owns the primary fill, the 0.97 press scale and the 40% disabled dim, so both halves
32
+ * stay one pill; the caret opens the menu aligned to the end and rotates while the popup is open.
33
+ * Without `menu` there is no divider or caret and the button keeps a full round shape.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { MenuItem, SplitButton } from "@horon-ai/react";
38
+ *
39
+ * function Publish() {
40
+ * return (
41
+ * <SplitButton
42
+ * menuLabel="Publish options"
43
+ * onClick={() => publishNow()}
44
+ * menu={
45
+ * <>
46
+ * <MenuItem onClick={() => schedulePublish()}>Schedule</MenuItem>
47
+ * <MenuItem onClick={() => saveDraft()}>Save as draft</MenuItem>
48
+ * </>
49
+ * }
50
+ * >
51
+ * Publish
52
+ * </SplitButton>
53
+ * );
54
+ * }
55
+ * ```
56
+ */
23
57
  export function SplitButton({
24
58
  children,
25
59
  icon,
@@ -4,12 +4,19 @@ import type { ComponentProps, ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import type { ControlSize } from "../lib/size";
6
6
 
7
- /** `compact` is the 40x24 Paper bot-memory switch and keeps its own rung. */
7
+ /** Shared control rungs plus `compact`, the 40x24 Paper bot-memory switch that keeps its own rung. */
8
8
  export type SwitchSize = ControlSize | "compact";
9
9
 
10
+ /**
11
+ * Props of {@link Switch}: Base UI's switch root props (`checked`, `defaultChecked`,
12
+ * `onCheckedChange`, `disabled`, `name`) plus the rung and the optional inline label.
13
+ */
10
14
  export type SwitchProps = Omit<ComponentProps<typeof BaseSwitch.Root>, "className"> & {
15
+ /** Track and thumb rung: `sm` 32x20, `md` 44x26, `lg` 52x30, `compact` 40x24. @default "md" */
11
16
  size?: SwitchSize;
17
+ /** When set, the switch sits at the end of a label row and the whole row toggles it. */
12
18
  label?: ReactNode;
19
+ /** Extra classes, applied to the label row when `label` is set and to the switch itself otherwise. */
13
20
  className?: string;
14
21
  };
15
22
 
@@ -38,6 +45,29 @@ const labelClass: Record<SwitchSize, string> = {
38
45
  * Switch: MD 44x26 and SM 32x20 tracks with a white thumb 2px in; LG is 52x30; COMPACT 40x24 with an
39
46
  * 18px thumb 3px in (Paper bot memory). The thumb stays white on the grey track in both themes and
40
47
  * takes the inverse of the ink fill when checked, so the checked state reads the same in dark.
48
+ *
49
+ * Renders Base UI's switch button (`role="switch"`, `aria-checked`) and is controlled or
50
+ * uncontrolled the same way; the thumb springs across in 220ms and the track repaints on hover,
51
+ * checked and disabled states. With `label` the control becomes a `<label>` row whose text
52
+ * toggles it, so the switch does not need its own accessible name.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * import { useState } from "react";
57
+ * import { Switch } from "@horon-ai/react";
58
+ *
59
+ * function MemoryToggle() {
60
+ * const [remember, setRemember] = useState(true);
61
+ * return (
62
+ * <Switch
63
+ * label="Remember this bot's memory"
64
+ * size="compact"
65
+ * checked={remember}
66
+ * onCheckedChange={setRemember}
67
+ * />
68
+ * );
69
+ * }
70
+ * ```
41
71
  */
42
72
  export function Switch({ size = "md", label, className, ...props }: SwitchProps) {
43
73
  const control = (
@@ -5,17 +5,34 @@ import type { ControlSize } from "../lib/size";
5
5
  import { useActiveIndicator } from "../hooks/use-active-indicator";
6
6
  import { LiquidGlass } from "./liquid-glass";
7
7
 
8
- export type TabBarItem = { value: string; label: string; icon: ReactNode };
8
+ /** One destination in a {@link TabBar}: its stable value, the accessible name, and the icon shown in the tab. */
9
+ export type TabBarItem = {
10
+ /** Identifies the tab; compared against the bar's `value`. */
11
+ value: string;
12
+ /** Accessible name of the icon-only button. */
13
+ label: string;
14
+ /** The tab icon; sized by the bar (20/22/24px per rung). */
15
+ icon: ReactNode;
16
+ };
17
+
18
+ /** Tab bar rungs: `sm` 56x40, `md` 64x48 (the Figma size) and `lg` 72x56. */
9
19
  export type TabBarSize = ControlSize;
10
20
 
21
+ /** Props of {@link TabBar}: the destinations, the selected value, and the track rung and surface. */
11
22
  export type TabBarProps = {
23
+ /** Destinations in display order; each keeps its fixed tap size and scrolls if the row overflows. */
12
24
  items: TabBarItem[];
25
+ /** Currently selected item's `value`; the control is fully controlled. */
13
26
  value: string;
27
+ /** Called with the clicked item's `value`; the bar itself does not change `value`. */
14
28
  onValueChange?: (value: string) => void;
15
- /** `md` is the 64x48 Figma tab; `sm` is 56x40 and `lg` 72x56. */
29
+ /** `md` is the 64x48 Figma tab; `sm` is 56x40 and `lg` 72x56. @default "md" */
16
30
  size?: TabBarSize;
31
+ /** `glass` (default) adds the liquid glass layer inside the pill; `flat` keeps the glass fill but omits that layer. @default "glass" */
17
32
  surface?: "glass" | "flat";
33
+ /** Accessible name of the `<nav>` landmark. @default "Main" */
18
34
  "aria-label"?: string;
35
+ /** Extra classes on the scrolling track. */
19
36
  className?: string;
20
37
  };
21
38
 
@@ -35,6 +52,34 @@ const tabClass: Record<TabBarSize, string> = {
35
52
  * Figma `Tab bar` (63:441): the mobile bottom navigation, a regular-glass pill
36
53
  * with icon-only 64x48 tabs and 22px icons; the current tab is a 10% ink pill
37
54
  * that slides to the tab you pick.
55
+ *
56
+ * The bar is controlled: it reports clicks through `onValueChange` and marks the matching
57
+ * item with `aria-current="page"`. Tabs keep their fixed tap size, so an over-wide row scrolls
58
+ * horizontally instead of shrinking; the moving pill is measured from the current tab and
59
+ * honours `prefers-reduced-motion`.
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * import { useState } from "react";
64
+ * import { House, MagnifyingGlass, User } from "@phosphor-icons/react";
65
+ * import { TabBar } from "@horon-ai/react";
66
+ *
67
+ * function MobileNav() {
68
+ * const [tab, setTab] = useState("home");
69
+ * return (
70
+ * <TabBar
71
+ * aria-label="Sections"
72
+ * value={tab}
73
+ * onValueChange={setTab}
74
+ * items={[
75
+ * { value: "home", label: "Home", icon: <House /> },
76
+ * { value: "search", label: "Search", icon: <MagnifyingGlass /> },
77
+ * { value: "profile", label: "Profile", icon: <User /> },
78
+ * ]}
79
+ * />
80
+ * );
81
+ * }
82
+ * ```
38
83
  */
39
84
  export function TabBar({
40
85
  items,
@@ -5,23 +5,61 @@ import { cn } from "../lib/cn";
5
5
  import type { ControlSize } from "../lib/size";
6
6
  import { LiquidGlass } from "./liquid-glass";
7
7
 
8
+ /** Tab rungs, shared with `ControlSize`: `sm` 26px, `md` 30px and `lg` 36px tab height. */
8
9
  export type TabsSize = ControlSize;
9
10
 
10
11
  /** `Tabs` owns the size; every `TabsTab` inside reads it so one list stays on one rung. */
11
12
  const TabsSizeContext = createContext<ControlSize>("md");
12
13
 
14
+ /**
15
+ * Shape of a tab list: `pill` is the tray of rounded pill tabs, `underline` is a bare
16
+ * text row with a 2px bar that slides under the active tab.
17
+ */
13
18
  export type TabsListVariant = "pill" | "underline";
14
19
 
15
20
  /** `TabsList` owns the variant; its `TabsTab`s read it to drop the pill shape on the underline list. */
16
21
  const TabsVariantContext = createContext<TabsListVariant>("pill");
17
22
 
18
- type WithClass<T> = Omit<T, "className"> & { className?: string };
23
+ /** Drops the native `className` slot and re-adds it as optional, so every part takes extra classes. */
24
+ type WithClass<T> = Omit<T, "className"> & {
25
+ /** Extra classes appended after the part's own classes, so conflicting utilities win. */
26
+ className?: string;
27
+ };
19
28
 
29
+ /**
30
+ * The root of a tab set (Figma `Tabs` example 62:353): it stacks the list and panels in a
31
+ * 16px-gap column, caps at its container, and provides the size rung every `TabsTab` inside
32
+ * reads. It is controlled or uncontrolled like Base UI's tabs (`value`/`defaultValue` plus
33
+ * `onValueChange`), and panels are matched to tabs by `value`.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { Tabs, TabsList, TabsPanel, TabsTab } from "@horon-ai/react";
38
+ *
39
+ * function SettingsTabs() {
40
+ * return (
41
+ * <Tabs defaultValue="general">
42
+ * <TabsList variant="underline">
43
+ * <TabsTab value="general">General</TabsTab>
44
+ * <TabsTab value="billing" count={3}>
45
+ * Billing
46
+ * </TabsTab>
47
+ * </TabsList>
48
+ * <TabsPanel value="general">General settings</TabsPanel>
49
+ * <TabsPanel value="billing">Three open invoices</TabsPanel>
50
+ * </Tabs>
51
+ * );
52
+ * }
53
+ * ```
54
+ */
20
55
  export function Tabs({
21
56
  size = "md",
22
57
  className,
23
58
  ...props
24
- }: WithClass<ComponentProps<typeof BaseTabs.Root>> & { size?: TabsSize }) {
59
+ }: WithClass<ComponentProps<typeof BaseTabs.Root>> & {
60
+ /** Rung for this tab set: tab heights of 26px (`sm`), 30px (`md`) or 36px (`lg`). @default "md" */
61
+ size?: TabsSize;
62
+ }) {
25
63
  return (
26
64
  <TabsSizeContext.Provider value={size}>
27
65
  <BaseTabs.Root className={cn("flex max-w-full min-w-0 flex-col gap-4", className)} {...props} />
@@ -44,6 +82,8 @@ const listClass: Record<TabsSize, string> = {
44
82
  *
45
83
  * `variant="underline"` is the document variant: no tray, a full-width 1px default border under
46
84
  * the tabs, and a 2px primary bar that slides under the active tab. `surface` does not apply to it.
85
+ *
86
+ * Part of {@link Tabs}; it also provides the variant every `TabsTab` inside reads.
47
87
  */
48
88
  export function TabsList({
49
89
  variant = "pill",
@@ -52,9 +92,9 @@ export function TabsList({
52
92
  children,
53
93
  ...props
54
94
  }: WithClass<ComponentProps<typeof BaseTabs.List>> & {
55
- /** `pill` (default) is the tray of pill tabs; `underline` is a text row with a sliding underline. */
95
+ /** `pill` (default) is the tray of pill tabs; `underline` is a text row with a sliding underline. @default "pill" */
56
96
  variant?: TabsListVariant;
57
- /** `solid` (default) is an opaque surface tray with a default border; `glass` is the regular glass tray. */
97
+ /** `solid` (default) is an opaque surface tray with a default border; `glass` is the regular glass tray. @default "solid" */
58
98
  surface?: "solid" | "glass";
59
99
  }) {
60
100
  const size = useContext(TabsSizeContext);
@@ -112,7 +152,14 @@ const countClass: Record<TabsSize, string> = {
112
152
  lg: "h-6 px-2",
113
153
  };
114
154
 
115
- /** Figma `Tabs` (62:335): a pill tab; the list's indicator draws the selection under the active tab. */
155
+ /**
156
+ * Figma `Tabs` (62:335): a pill tab; the list's indicator draws the selection under the active tab.
157
+ *
158
+ * Part of {@link Tabs}. The tab takes its height, text and icon size from the surrounding
159
+ * `Tabs`; on an underline list it drops the pill radius and side padding. Base UI marks it
160
+ * `role="tab"` with `aria-selected`, and hover or the active state moves the label to the
161
+ * primary text color.
162
+ */
116
163
  export function TabsTab({
117
164
  className,
118
165
  icon,
@@ -157,6 +204,13 @@ export function TabsTab({
157
204
  );
158
205
  }
159
206
 
207
+ /**
208
+ * The content region of one tab, rendered by Base UI with `role="tabpanel"` and matched to a
209
+ * `TabsTab` by `value`. The outline is suppressed, and a panel swap slides the content in 4px
210
+ * over the shared 200ms swap move; a change in content height is not animated.
211
+ *
212
+ * Part of {@link Tabs}.
213
+ */
160
214
  export function TabsPanel({ className, ...props }: WithClass<ComponentProps<typeof BaseTabs.Panel>>) {
161
215
  return <BaseTabs.Panel className={cn("animate-swap-move outline-none", className)} {...props} />;
162
216
  }
@@ -4,26 +4,46 @@ import type { ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { Spinner } from "./spinner";
6
6
 
7
+ /**
8
+ * Task lifecycle states a {@link TaskRow} renders: `running` shows a spinner and Cancel,
9
+ * `review` a check and Review, `failed` a warning and Retry, `done` a muted check and Archive.
10
+ */
7
11
  export type TaskStatus = "running" | "review" | "failed" | "done";
8
12
 
13
+ /** Props of {@link TaskRow}: the task's state and texts plus the actions offered for that state. */
9
14
  export type TaskRowProps = {
15
+ /** Drives the status icon, the message color and which actions appear. */
10
16
  status: TaskStatus;
17
+ /** Task name, one truncated line at `label-lg`. */
11
18
  title: ReactNode;
12
19
  /** Elapsed time, such as "1m 56s". */
13
20
  elapsed?: ReactNode;
14
21
  /** Bot initial and name. */
15
- bot: { initial: string; name: ReactNode; tileClassName?: string };
22
+ bot: {
23
+ /** One or two characters shown in the 18px tile; falls back to the status color when `tileClassName` is absent. */
24
+ initial: string;
25
+ /** Bot name after the tile, kept on one line. */
26
+ name: ReactNode;
27
+ /** Overrides the status-driven tile fill; the text is forced to `text-inverse` when set. */
28
+ tileClassName?: string;
29
+ };
30
+ /** Project shown after the bot name, separated by a dot and a folder icon. */
16
31
  project?: ReactNode;
17
32
  /** Status line: the current step, the result or the error. */
18
33
  message: ReactNode;
19
34
  /** Caption after the message, such as "2 drafts · 1 calendar change". */
20
35
  detail?: ReactNode;
36
+ /** Called by the Cancel pill; shown only while `running`. */
21
37
  onCancel?: () => void;
38
+ /** Called by the Retry pill; shown only when `failed`. */
22
39
  onRetry?: () => void;
40
+ /** Called by the Review pill; shown only when `review`. */
23
41
  onReview?: () => void;
42
+ /** Called by the archive icon button; shown when `review` or `done`. */
24
43
  onArchive?: () => void;
25
- /** `card` is a standalone subtle row; `list` is transparent with a 1px divider, for stacking inside a list. */
44
+ /** `card` is a standalone subtle row; `list` is transparent with a 1px divider, for stacking inside a list. @default "card" */
26
45
  variant?: "card" | "list";
46
+ /** Extra classes on the row container, which owns the radius and divider. */
27
47
  className?: string;
28
48
  };
29
49
 
@@ -37,7 +57,33 @@ const botTile: Record<TaskStatus, string> = {
37
57
  const pill =
38
58
  "motion-press flex h-7.5 shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-full px-3 text-label-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand";
39
59
 
40
- /** Figma `Task row` (Agent tasks 112:939): a flat subtle row, 16px radius, with Running / Review / Failed / Done. */
60
+ /**
61
+ * Figma `Task row` (Agent tasks 112:939): a flat subtle row, 16px radius, with Running / Review / Failed / Done.
62
+ *
63
+ * The status drives the icon, the message color and the one primary action offered: Cancel while
64
+ * running, Retry when failed, Review when waiting for a person, and Archive once review or done.
65
+ * Every state change animates in with the shared pop/fade motion.
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * import { TaskRow } from "@horon-ai/react";
70
+ *
71
+ * function RunningTask() {
72
+ * return (
73
+ * <TaskRow
74
+ * status="running"
75
+ * title="Draft the Q4 launch note"
76
+ * elapsed="1m 56s"
77
+ * bot={{ initial: "H", name: "Horon" }}
78
+ * project="Launch"
79
+ * message="Writing the second draft"
80
+ * detail="2 drafts · 1 calendar change"
81
+ * onCancel={() => cancelTask("launch-note")}
82
+ * />
83
+ * );
84
+ * }
85
+ * ```
86
+ */
41
87
  export function TaskRow({
42
88
  status,
43
89
  title,
@@ -5,20 +5,29 @@ 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
+ /** Field rungs, shared with `ControlSize`: `sm` 88px, `md` 112px and `lg` 136px boxes. */
8
9
  export type TextareaFieldSize = ControlSize;
9
10
 
11
+ /**
12
+ * Props of {@link TextareaField}: the native textarea attributes plus a label row, a description
13
+ * or error footer, an optional counter and the surface.
14
+ */
10
15
  export type TextareaFieldProps = Omit<ComponentProps<"textarea">, "className"> & {
16
+ /** Field label, one line at the rung's label size. */
11
17
  label: ReactNode;
12
18
  /** Right side of the label row, such as "Optional". */
13
19
  hint?: ReactNode;
20
+ /** Help text in the footer; replaced by `error` when one is set. */
14
21
  description?: ReactNode;
15
22
  /** Error message; replaces the description and turns the border and footer red. */
16
23
  error?: ReactNode;
17
24
  /** Shows a "count / max" counter when set. */
18
25
  maxLength?: number;
26
+ /** Box height, text and footer sizes: 88/112/136px. @default "md" */
19
27
  size?: TextareaFieldSize;
20
- /** Solid (default) or glass, as on `Input`. */
28
+ /** Solid (default) or glass, as on `Input`. @default "solid" */
21
29
  surface?: InputSurface;
30
+ /** Extra classes on the field root, which is capped at 360px by default. */
22
31
  className?: string;
23
32
  };
24
33
 
@@ -47,7 +56,32 @@ const boxClass: Record<TextareaFieldSize, string> = {
47
56
  lg: "h-34 px-5 py-3.5",
48
57
  };
49
58
 
50
- /** Figma `Textarea field` (60:466): label row, glass or solid box, footer with help text and counter. */
59
+ /**
60
+ * Figma `Textarea field` (60:466): label row, glass or solid box, footer with help text and counter.
61
+ *
62
+ * Built on Base UI's field: the label is linked to the textarea, `disabled` dims the whole field
63
+ * to 60%, and `error` marks the field invalid, swaps the description for the error and paints the
64
+ * border and footer with the danger color. The counter follows the live length, including a
65
+ * controlled `value` that changes without an input event.
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * import { TextareaField } from "@horon-ai/react";
70
+ *
71
+ * function PromptField() {
72
+ * return (
73
+ * <TextareaField
74
+ * label="Instructions"
75
+ * hint="Optional"
76
+ * description="Tell the bot how to answer."
77
+ * maxLength={280}
78
+ * size="md"
79
+ * placeholder="Be concise and cite sources."
80
+ * />
81
+ * );
82
+ * }
83
+ * ```
84
+ */
51
85
  export function TextareaField({
52
86
  label,
53
87
  hint,
@@ -5,14 +5,20 @@ 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
+ /** Textarea rungs, shared with `ControlSize`: `sm` 88px, `md` 112px and `lg` 136px minimum heights. */
8
9
  export type TextareaSize = ControlSize;
9
10
 
11
+ /** Props of {@link Textarea}: the native textarea attributes plus the rung, invalid state and surface. */
10
12
  export type TextareaProps = Omit<ComponentProps<"textarea">, "className"> & {
13
+ /** Minimum box height, padding and text rung: 88/112/136px. @default "md" */
11
14
  size?: TextareaSize;
15
+ /** Marks the box invalid (`data-invalid`) and the textarea `aria-invalid`, painting the danger border. */
12
16
  invalid?: boolean;
13
- /** Solid (default) or glass, as on `Input`. */
17
+ /** Solid (default) or glass, as on `Input`. @default "solid" */
14
18
  surface?: InputSurface;
19
+ /** Extra classes on the box that wraps the textarea. */
15
20
  className?: string;
21
+ /** Classes for the native `<textarea>` itself, such as a fixed height or font. */
16
22
  textareaClassName?: string;
17
23
  };
18
24
 
@@ -22,7 +28,34 @@ const sizeClass: Record<TextareaSize, string> = {
22
28
  lg: "min-h-34 px-5 py-3.5",
23
29
  };
24
30
 
25
- /** Figma `Textarea` (58:137): an 88/112/136px glass or solid box with 12/14/20px padding, the same field rungs as `Input`. */
31
+ /**
32
+ * Figma `Textarea` (58:137): an 88/112/136px glass or solid box with 12/14/20px padding, the same field rungs as `Input`.
33
+ *
34
+ * A bare field box: no label or footer (use {@link TextareaField} for those). The box draws the
35
+ * `:focus-within` ring and the invalid border; the textarea itself fills it, starts at three rows
36
+ * and is not user-resizable, so the rung keeps its height.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * import { useState } from "react";
41
+ * import { Textarea } from "@horon-ai/react";
42
+ *
43
+ * function Notes() {
44
+ * const [notes, setNotes] = useState("");
45
+ * return (
46
+ * <Textarea
47
+ * aria-label="Notes"
48
+ * size="md"
49
+ * surface="solid"
50
+ * rows={4}
51
+ * placeholder="Add a note"
52
+ * value={notes}
53
+ * onChange={(event) => setNotes(event.target.value)}
54
+ * />
55
+ * );
56
+ * }
57
+ * ```
58
+ */
26
59
  export function Textarea({
27
60
  size = "md",
28
61
  invalid,