@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.
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/components/agent-activity.tsx +75 -7
- package/src/components/agent-lane.tsx +32 -3
- package/src/components/agent-lanes-timeline.tsx +85 -7
- package/src/components/agent-panel.tsx +46 -4
- package/src/components/agent-steering-panel.tsx +53 -3
- package/src/components/approval-card.tsx +34 -2
- package/src/components/avatar.tsx +20 -4
- package/src/components/badge.tsx +24 -3
- package/src/components/banner.tsx +19 -3
- package/src/components/bot-instructions-panel.tsx +86 -7
- package/src/components/bot-list-row.tsx +13 -1
- package/src/components/bot-memory-panel.tsx +58 -3
- package/src/components/bottom-sheet.tsx +61 -7
- package/src/components/breadcrumb-header.tsx +32 -2
- package/src/components/breadcrumb.tsx +31 -1
- package/src/components/button.tsx +46 -1
- package/src/components/calendar.tsx +76 -6
- package/src/components/card.tsx +30 -1
- package/src/components/chat-bubble.tsx +32 -3
- package/src/components/chat-controls.tsx +60 -7
- package/src/components/checkbox.tsx +22 -1
- package/src/components/chip.tsx +29 -2
- package/src/components/choice-card.tsx +25 -1
- package/src/components/code-panel.tsx +28 -3
- package/src/components/combobox.tsx +52 -2
- package/src/components/command-palette.tsx +65 -1
- package/src/components/composer.tsx +28 -1
- package/src/components/data-table.tsx +198 -16
- package/src/components/date-field.tsx +31 -2
- package/src/components/dialog.tsx +45 -3
- package/src/components/directory-card.tsx +23 -2
- package/src/components/document-extract-panel.tsx +83 -5
- package/src/components/empty-state.tsx +21 -2
- package/src/components/field.tsx +49 -3
- package/src/components/file-drop.tsx +39 -4
- package/src/components/filter-bar.tsx +38 -1
- package/src/components/flue-background.tsx +88 -4
- package/src/components/horon-icon.tsx +50 -3
- package/src/components/horon-wordmark.tsx +26 -1
- package/src/components/icon-button.tsx +38 -1
- package/src/components/input-group.tsx +57 -6
- package/src/components/input.tsx +48 -2
- package/src/components/integration-row.tsx +43 -2
- package/src/components/kbd.tsx +28 -2
- package/src/components/knowledge-base-panel.tsx +40 -2
- package/src/components/liquid-glass.tsx +16 -0
- package/src/components/list-detail.tsx +89 -7
- package/src/components/marketplace-card.tsx +40 -4
- package/src/components/mcp-server-row.tsx +49 -3
- package/src/components/menu.tsx +83 -3
- package/src/components/message-extras.tsx +50 -4
- package/src/components/modal.tsx +71 -4
- package/src/components/orchestrator-panel.tsx +48 -4
- package/src/components/page-header.tsx +26 -3
- package/src/components/plan-card.tsx +28 -1
- package/src/components/plan-step.tsx +41 -1
- package/src/components/plugin-row.tsx +61 -3
- package/src/components/progress.tsx +32 -2
- package/src/components/radio-group.tsx +55 -2
- package/src/components/research-run-panel.tsx +73 -3
- package/src/components/schedule.tsx +163 -12
- package/src/components/segmented-control.tsx +57 -2
- package/src/components/select.tsx +60 -0
- package/src/components/settings-list.tsx +66 -8
- package/src/components/sidebar.tsx +214 -23
- package/src/components/skeleton.tsx +61 -4
- package/src/components/slider.tsx +30 -0
- package/src/components/source-row.tsx +43 -4
- package/src/components/spinner.tsx +29 -2
- package/src/components/split-button.tsx +37 -3
- package/src/components/switch.tsx +31 -1
- package/src/components/tab-bar.tsx +47 -2
- package/src/components/tabs.tsx +59 -5
- package/src/components/task-row.tsx +49 -3
- package/src/components/textarea-field.tsx +36 -2
- package/src/components/textarea.tsx +35 -2
- package/src/components/toast.tsx +52 -3
- package/src/components/tool-call.tsx +87 -4
- package/src/components/tool-calls-header.tsx +32 -1
- package/src/components/tool-permissions.tsx +50 -2
- package/src/components/tooltip.tsx +33 -1
- package/src/components/top-bar.tsx +35 -2
- package/src/components/usage-meter.tsx +28 -2
- package/src/components/workspace-menu.tsx +46 -1
- package/src/hooks/use-active-indicator.ts +44 -1
- package/src/hooks/use-color-scheme.ts +24 -1
- package/src/hooks/use-copy-to-clipboard.ts +23 -0
- package/src/hooks/use-device.ts +29 -1
- package/src/hooks/use-hotkey.ts +21 -0
- package/src/hooks/use-media-query.ts +18 -0
- package/src/hooks/use-platform.ts +30 -1
- package/src/hooks/use-reduced-motion.ts +18 -1
- package/src/index.ts +31 -0
- package/src/lib/cn.ts +13 -1
- package/src/lib/cursor-ghost.ts +16 -6
- package/src/lib/platform.ts +71 -4
package/src/components/modal.tsx
CHANGED
|
@@ -4,28 +4,71 @@ import type { ComponentProps, ReactNode } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Root of a modal; owns open state. Re-exported from Base UI's `Dialog.Root`, so
|
|
9
|
+
* it accepts `open`, `defaultOpen`, `onOpenChange` and `modal`.
|
|
10
|
+
*/
|
|
7
11
|
export const Modal = BaseDialog.Root;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The element that opens the modal. Base UI gives it the expanded state and the
|
|
15
|
+
* `aria-controls` link to the popup; use `render` to make an existing button the
|
|
16
|
+
* trigger.
|
|
17
|
+
*
|
|
18
|
+
* Part of {@link ModalContent}.
|
|
19
|
+
*/
|
|
8
20
|
export const ModalTrigger = BaseDialog.Trigger;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A button that closes the modal when clicked. Any `Button`-like element can be
|
|
24
|
+
* its child; it also returns focus to the trigger.
|
|
25
|
+
*
|
|
26
|
+
* Part of {@link ModalContent}.
|
|
27
|
+
*/
|
|
9
28
|
export const ModalClose = BaseDialog.Close;
|
|
10
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Width step of the modal popup.
|
|
32
|
+
*
|
|
33
|
+
* - `sm` — 400px, for a short form.
|
|
34
|
+
* - `md` — 560px, the default reading width.
|
|
35
|
+
* - `lg` — 720px, for a wide editor or table.
|
|
36
|
+
*/
|
|
11
37
|
export type ModalSize = "sm" | "md" | "lg";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Surface of the modal popup.
|
|
41
|
+
*
|
|
42
|
+
* - `solid` — the opaque page fill with a `0 24 64` shadow.
|
|
43
|
+
* - `glass` — the `veil-overlay` material, for a modal that floats over content.
|
|
44
|
+
*/
|
|
12
45
|
export type ModalSurface = "solid" | "glass";
|
|
13
46
|
|
|
14
|
-
type DivProps = Omit<ComponentProps<"div">, "className"> & {
|
|
47
|
+
type DivProps = Omit<ComponentProps<"div">, "className"> & {
|
|
48
|
+
/** Extra classes on the element, merged after the built-in ones. */
|
|
49
|
+
className?: string;
|
|
50
|
+
};
|
|
15
51
|
|
|
52
|
+
/** Props for {@link ModalContent}: title and description, the size and surface, and an optional footer. */
|
|
16
53
|
export type ModalContentProps = Omit<ComponentProps<typeof BaseDialog.Popup>, "className" | "title"> & {
|
|
54
|
+
/** Modal heading, rendered as the `Dialog.Title` that labels the popup. */
|
|
17
55
|
title: ReactNode;
|
|
56
|
+
/** Supporting line under the title, rendered as the `Dialog.Description`. */
|
|
18
57
|
description?: ReactNode;
|
|
19
|
-
/** Width: `sm` 400px, `md` 560px (default), `lg` 720px. Never wider than the viewport minus 32px.
|
|
58
|
+
/** Width: `sm` 400px, `md` 560px (default), `lg` 720px. Never wider than the viewport minus 32px.
|
|
59
|
+
* @default "md" */
|
|
20
60
|
size?: ModalSize;
|
|
21
|
-
/** `solid` (default) is the opaque page fill with a 0 24 64 shadow; `glass` is the veil (`veil-overlay`).
|
|
61
|
+
/** `solid` (default) is the opaque page fill with a 0 24 64 shadow; `glass` is the veil (`veil-overlay`).
|
|
62
|
+
* @default "solid" */
|
|
22
63
|
surface?: ModalSurface;
|
|
23
64
|
/** Tile or icon before the title. */
|
|
24
65
|
leading?: ReactNode;
|
|
25
|
-
/** Label for the close button; hides it when `null`.
|
|
66
|
+
/** Label for the close button; hides it when `null`.
|
|
67
|
+
* @default "Close" */
|
|
26
68
|
closeLabel?: string | null;
|
|
27
69
|
/** `ModalFooter`, kept below the scrolling body. */
|
|
28
70
|
footer?: ReactNode;
|
|
71
|
+
/** Extra classes on the popup, merged after the built-in ones. */
|
|
29
72
|
className?: string;
|
|
30
73
|
};
|
|
31
74
|
|
|
@@ -39,6 +82,30 @@ const widthClass: Record<ModalSize, string> = {
|
|
|
39
82
|
* A modal for work that needs a form or a longer read, where `Dialog` is a short confirmation. It has three
|
|
40
83
|
* fixed parts: a header (title, description and close), a body that scrolls on its own once the modal reaches
|
|
41
84
|
* the viewport height, and a footer whose actions sit at the right. 24px padding, 20px radius, 32% scrim.
|
|
85
|
+
*
|
|
86
|
+
* The popup portals to `document.body` and renders its own backdrop and centred
|
|
87
|
+
* viewport, so it is unaffected by the caller's stacking context; Base UI traps
|
|
88
|
+
* focus, closes on Escape and on backdrop press, and restores focus to the
|
|
89
|
+
* trigger. Only the body scrolls: the header and footer stay put.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```tsx
|
|
93
|
+
* <Modal>
|
|
94
|
+
* <ModalTrigger render={<Button variant="solid" />}>Rename workspace</ModalTrigger>
|
|
95
|
+
* <ModalContent
|
|
96
|
+
* title="Rename workspace"
|
|
97
|
+
* description="This changes the name for every member."
|
|
98
|
+
* footer={
|
|
99
|
+
* <ModalFooter>
|
|
100
|
+
* <ModalClose render={<Button variant="solid" />}>Cancel</ModalClose>
|
|
101
|
+
* <Button onClick={save}>Save</Button>
|
|
102
|
+
* </ModalFooter>
|
|
103
|
+
* }
|
|
104
|
+
* >
|
|
105
|
+
* <Input aria-label="Workspace name" defaultValue="Horon" />
|
|
106
|
+
* </ModalContent>
|
|
107
|
+
* </Modal>
|
|
108
|
+
* ```
|
|
42
109
|
*/
|
|
43
110
|
export function ModalContent({
|
|
44
111
|
title,
|
|
@@ -5,20 +5,26 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { SegmentedControl } from "./segmented-control";
|
|
6
6
|
import { Spinner } from "./spinner";
|
|
7
7
|
|
|
8
|
+
/** The lead bot row at the top of {@link OrchestratorPanel}. */
|
|
8
9
|
export type OrchestratorLead = {
|
|
9
10
|
/** One letter shown in the 22px ink tile. */
|
|
10
11
|
initial: string;
|
|
12
|
+
/** Bot name, next to the tile. */
|
|
11
13
|
name: ReactNode;
|
|
12
|
-
/** Role caption next to the name, such as "Lead".
|
|
14
|
+
/** Role caption next to the name, such as "Lead". Defaults to "Lead" when unset.
|
|
15
|
+
* @default "Lead" */
|
|
13
16
|
role?: ReactNode;
|
|
14
17
|
/** Status text, such as "1 working". */
|
|
15
18
|
status?: ReactNode;
|
|
16
19
|
/** Shows the 13px spinner while the lead waits on its lanes. */
|
|
17
20
|
running?: boolean;
|
|
21
|
+
/** Timing label at the right, in caption colour. */
|
|
18
22
|
elapsed?: ReactNode;
|
|
19
23
|
};
|
|
20
24
|
|
|
25
|
+
/** One step of the lead's plan, listed under its row. */
|
|
21
26
|
export type OrchestratorPlanItem = {
|
|
27
|
+
/** Step text; also the React key, so keep steps unique. */
|
|
22
28
|
label: string;
|
|
23
29
|
/** Dot color of the lane that runs this step, such as "bg-sky-600". Defaults to the live blue. */
|
|
24
30
|
dotClassName?: string;
|
|
@@ -26,35 +32,73 @@ export type OrchestratorPlanItem = {
|
|
|
26
32
|
running?: boolean;
|
|
27
33
|
};
|
|
28
34
|
|
|
35
|
+
/** The closing summary card of {@link OrchestratorPanel}. */
|
|
29
36
|
export type OrchestratorSummary = {
|
|
37
|
+
/** Title of the card.
|
|
38
|
+
* @default "Summary" */
|
|
30
39
|
title?: ReactNode;
|
|
31
40
|
/** Right-aligned count, such as "1 of 3 returned". */
|
|
32
41
|
count?: ReactNode;
|
|
42
|
+
/** Body text of the summary. */
|
|
33
43
|
body: ReactNode;
|
|
34
44
|
};
|
|
35
45
|
|
|
46
|
+
/** Props for {@link OrchestratorPanel}: the header controls, the lead and its plan, the lanes and the summary. */
|
|
36
47
|
export type OrchestratorPanelProps = {
|
|
48
|
+
/** Panel heading, rendered as the `h3`. */
|
|
37
49
|
title: ReactNode;
|
|
38
|
-
/** Label before the segmented picker, such as "Bots in parallel". */
|
|
50
|
+
/** Label before the segmented picker, such as "Bots in parallel". @default "Bots in parallel" */
|
|
39
51
|
parallelLabel?: ReactNode;
|
|
40
|
-
/** Choices of the parallel picker. */
|
|
52
|
+
/** Choices of the parallel picker. The picker only renders when this is set. */
|
|
41
53
|
parallelOptions?: number[];
|
|
54
|
+
/** Controlled picked value; the picker is uncontrolled-looking when unset. */
|
|
42
55
|
parallelValue?: number;
|
|
56
|
+
/** Fires with the picked number, converted from the segmented control's string value. */
|
|
43
57
|
onParallelChange?: (value: number) => void;
|
|
58
|
+
/** Text of the Stop button.
|
|
59
|
+
* @default "Stop" */
|
|
44
60
|
stopLabel?: ReactNode;
|
|
45
61
|
/** Renders the Stop button when set. */
|
|
46
62
|
onStop?: () => void;
|
|
63
|
+
/** The lead row: tile, name, role, status and elapsed. */
|
|
47
64
|
lead: OrchestratorLead;
|
|
65
|
+
/** Steps under the lead; each row rises in with a 30ms stagger. Hides the list when empty. */
|
|
48
66
|
plan?: OrchestratorPlanItem[];
|
|
49
67
|
/** `AgentLane` cards; the branch connector draws one elbow per lane. */
|
|
50
68
|
children?: ReactNode;
|
|
69
|
+
/** Closing summary card; the reveal wrapper collapses when unset. */
|
|
51
70
|
summary?: OrchestratorSummary;
|
|
71
|
+
/** Extra classes on the panel `section`, merged after the built-in ones. */
|
|
52
72
|
className?: string;
|
|
53
73
|
};
|
|
54
74
|
|
|
55
75
|
const stagger = (index: number) => ({ "--i": index, animationDelay: `calc(var(--i) * 30ms)` }) as CSSProperties;
|
|
56
76
|
|
|
57
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Figma `Orchestrator` (Agents N1): a 24px-radius solid panel, header with a parallel picker and Stop, a lead row, its plan, branching lanes and a summary.
|
|
79
|
+
*
|
|
80
|
+
* Renders a lead bot that fans work out to lane cards: the connector draws one
|
|
81
|
+
* elbow per child and a vertical trunk between them, and the plan rows and lane
|
|
82
|
+
* cards rise in with a 30ms stagger. The summary does not rise; it is revealed by
|
|
83
|
+
* the `motion-reveal` wrapper around it. Every part is optional except `title` and
|
|
84
|
+
* `lead`, so the panel works as a bare lead view too.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* <OrchestratorPanel
|
|
89
|
+
* title="Orchestrator"
|
|
90
|
+
* parallelOptions={[1, 2, 3]}
|
|
91
|
+
* parallelValue={2}
|
|
92
|
+
* onParallelChange={setParallel}
|
|
93
|
+
* onStop={stop}
|
|
94
|
+
* lead={{ initial: "H", name: "Horon", status: "2 working", running: true }}
|
|
95
|
+
* plan={[{ label: "Collect sources" }, { label: "Draft the summary", running: true }]}
|
|
96
|
+
* summary={{ count: "0 of 2 returned", body: "Lanes are still running." }}
|
|
97
|
+
* >
|
|
98
|
+
* <AgentLane status="running" bot={{ initial: "R", name: "Researcher" }} task="Collect sources" />
|
|
99
|
+
* </OrchestratorPanel>
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
58
102
|
export function OrchestratorPanel({
|
|
59
103
|
title,
|
|
60
104
|
parallelLabel = "Bots in parallel",
|
|
@@ -5,20 +5,43 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { IconButton } from "./icon-button";
|
|
6
6
|
import { LiquidGlass } from "./liquid-glass";
|
|
7
7
|
|
|
8
|
+
/** Props for {@link PageHeader}: the title row, an optional back button, trailing actions and a filter row. */
|
|
8
9
|
export type PageHeaderProps = {
|
|
10
|
+
/** Page title, rendered as the `h1`; truncated with an ellipsis when it does not fit. */
|
|
9
11
|
title: ReactNode;
|
|
12
|
+
/** Renders a leading 40px back `IconButton` when set; the header drops the title's left padding in exchange. */
|
|
10
13
|
onBack?: () => void;
|
|
14
|
+
/** Accessible label of the back button; `IconButton` sets it as `aria-label` and shows no visible tooltip.
|
|
15
|
+
* @default "Back" */
|
|
11
16
|
backLabel?: string;
|
|
12
17
|
/** Trailing 40px icon buttons, such as search. */
|
|
13
18
|
actions?: ReactNode;
|
|
14
|
-
/** Content under the top row, such as a `SegmentedControl` filter. */
|
|
19
|
+
/** Content under the top row, such as a `SegmentedControl` filter. Animates in with `swap-move` when it appears. */
|
|
15
20
|
children?: ReactNode;
|
|
16
|
-
/** `solid` (default) is an opaque surface header with a default border; `glass` is the regular glass header. */
|
|
21
|
+
/** `solid` (default) is an opaque surface header with a default border; `glass` is the regular glass header. @default "solid" */
|
|
17
22
|
surface?: "solid" | "glass";
|
|
23
|
+
/** Extra classes on the `header` element, merged after the built-in ones. */
|
|
18
24
|
className?: string;
|
|
19
25
|
};
|
|
20
26
|
|
|
21
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Figma `Header with filter` (Segmented filters 88:723): a 28px-radius header, 8px padding, solid or regular glass.
|
|
29
|
+
*
|
|
30
|
+
* A single row of back button, title and actions, plus an optional filter row
|
|
31
|
+
* below it. The `glass` surface paints a `LiquidGlass` layer behind the content
|
|
32
|
+
* and keeps the header transparent.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* <PageHeader
|
|
37
|
+
* title="Sources"
|
|
38
|
+
* onBack={() => history.back()}
|
|
39
|
+
* actions={<IconButton label="Search" icon={<MagnifyingGlass />} />}
|
|
40
|
+
* >
|
|
41
|
+
* <SegmentedControl value="all" onValueChange={() => {}} items={[{ value: "all", label: "All" }]} />
|
|
42
|
+
* </PageHeader>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
22
45
|
export function PageHeader({
|
|
23
46
|
title,
|
|
24
47
|
onBack,
|
|
@@ -5,11 +5,20 @@ import type { ReactNode } from "react";
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import { Spinner } from "./spinner";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Footer state of a `PlanCard`: at most one at a time, and setting it expands the
|
|
10
|
+
* footer.
|
|
11
|
+
*
|
|
12
|
+
* - `running` shows a 14px spinner and keeps the footer text in the primary colour.
|
|
13
|
+
* - `failed` shows a warning icon and tints the footer text with `text-text-danger`.
|
|
14
|
+
*/
|
|
8
15
|
export type PlanCardStatus = "running" | "failed";
|
|
9
16
|
|
|
17
|
+
/** Props for {@link PlanCard}: a label, title and step list, plus the optional status footer. */
|
|
10
18
|
export type PlanCardProps = {
|
|
11
19
|
/** Label line next to the 16px list icon, such as "Plan · 5 steps". */
|
|
12
20
|
label?: ReactNode;
|
|
21
|
+
/** Plan title, rendered as the `h3` under the label. */
|
|
13
22
|
title: ReactNode;
|
|
14
23
|
/** `PlanStep`s, stacked without dividers. */
|
|
15
24
|
children?: ReactNode;
|
|
@@ -17,11 +26,29 @@ export type PlanCardProps = {
|
|
|
17
26
|
status?: PlanCardStatus;
|
|
18
27
|
/** Footer text, such as "Stopped · step 4 failed" or "Running · 1 of 5". */
|
|
19
28
|
statusLabel?: ReactNode;
|
|
29
|
+
/** Right-aligned footer timestamp or duration, kept in caption colour. */
|
|
20
30
|
elapsed?: ReactNode;
|
|
31
|
+
/** Extra classes on the `section` element, merged after the built-in ones. */
|
|
21
32
|
className?: string;
|
|
22
33
|
};
|
|
23
34
|
|
|
24
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Figma `Plan card` (Agents N3): a 20px-radius solid card with a label, title, the steps and a status footer.
|
|
37
|
+
*
|
|
38
|
+
* The footer is a `Collapsible` panel that stays mounted, so passing and clearing
|
|
39
|
+
* `status` plays the collapse animation instead of mounting and unmounting. Only
|
|
40
|
+
* the footer text fades in again, and only when `status` changes; the label line
|
|
41
|
+
* above the title does not animate.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <PlanCard label="Plan · 3 steps" title="Ship the parser" status="running" statusLabel="Running · 1 of 3">
|
|
46
|
+
* <PlanStep status="done" index={1} title="Read the spec" />
|
|
47
|
+
* <PlanStep status="running" index={2} title="Write the parser" />
|
|
48
|
+
* <PlanStep status="pending" index={3} title="Publish" last />
|
|
49
|
+
* </PlanCard>
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
25
52
|
export function PlanCard({ label, title, children, status, statusLabel, elapsed, className }: PlanCardProps) {
|
|
26
53
|
return (
|
|
27
54
|
<section
|
|
@@ -4,25 +4,65 @@ import type { ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { Spinner } from "./spinner";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* State of one step in a plan.
|
|
9
|
+
*
|
|
10
|
+
* - `done` — finished: sky marker with a check and a sky rail running to the next step.
|
|
11
|
+
* - `running` — in progress: spinner on a subtle fill.
|
|
12
|
+
* - `failed` — stopped with an error: danger marker with `!` and the error line expanded.
|
|
13
|
+
* - `pending` — not started: outlined marker that shows the step number.
|
|
14
|
+
*/
|
|
7
15
|
export type PlanStepStatus = "done" | "running" | "failed" | "pending";
|
|
8
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Props for {@link PlanStep}. Only `status`, `index` and `title` are required; everything else is
|
|
19
|
+
* optional decoration around the marker and the title line.
|
|
20
|
+
*/
|
|
9
21
|
export type PlanStepProps = {
|
|
22
|
+
/** Current step state; drives the marker glyph, the marker fill and the rail color. */
|
|
10
23
|
status: PlanStepStatus;
|
|
11
24
|
/** Step number, shown on pending steps. */
|
|
12
25
|
index: number;
|
|
26
|
+
/** Step title; primary while done, running or failed, secondary while pending. */
|
|
13
27
|
title: ReactNode;
|
|
28
|
+
/** Secondary line under the title, next to the tool tag. */
|
|
14
29
|
description?: ReactNode;
|
|
15
30
|
/** Tool id in mono, such as "stripe.query". */
|
|
16
31
|
tool?: ReactNode;
|
|
32
|
+
/** Timing label at the right of the title; rendered only while `done` or `running`. */
|
|
17
33
|
elapsed?: ReactNode;
|
|
34
|
+
/** Error line under the title, revealed only when `status` is `failed`. */
|
|
18
35
|
error?: ReactNode;
|
|
36
|
+
/** Adds a "Retry from here" button in the expanded error block; it is inert unless `status` is `failed`. */
|
|
19
37
|
onRetry?: () => void;
|
|
20
38
|
/** Hides the rail line under the last step. */
|
|
21
39
|
last?: boolean;
|
|
40
|
+
/** Classes merged onto the outer row. */
|
|
22
41
|
className?: string;
|
|
23
42
|
};
|
|
24
43
|
|
|
25
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Figma `Plan step` (Agent plan 191:8296): a 24px marker on a 2px rail, sky when done.
|
|
46
|
+
*
|
|
47
|
+
* Stack the steps of an agent plan vertically and pass `last` to the final one so the rail stops
|
|
48
|
+
* there. The marker cross-fades on every status change, and the error block collapses unless the
|
|
49
|
+
* step failed.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* import { PlanStep } from "@horon-ai/react";
|
|
54
|
+
*
|
|
55
|
+
* function Example() {
|
|
56
|
+
* return (
|
|
57
|
+
* <div>
|
|
58
|
+
* <PlanStep status="done" index={1} title="Read the brief" elapsed="2s" />
|
|
59
|
+
* <PlanStep status="running" index={2} title="Search the web" tool="web.search" />
|
|
60
|
+
* <PlanStep status="pending" index={3} title="Write the summary" last />
|
|
61
|
+
* </div>
|
|
62
|
+
* );
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
26
66
|
export function PlanStep({
|
|
27
67
|
status,
|
|
28
68
|
index,
|
|
@@ -7,29 +7,87 @@ import { IconButton } from "./icon-button";
|
|
|
7
7
|
import { Menu, MenuContent, MenuTrigger } from "./menu";
|
|
8
8
|
import { Switch } from "./switch";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Props for {@link PluginRow}. The row also forwards every other `div` prop (except `children`)
|
|
12
|
+
* to the outer element, so `id`, `data-*` and DOM handlers work as usual.
|
|
13
|
+
*/
|
|
10
14
|
export type PluginRowProps = Omit<ComponentProps<"div">, "children"> & {
|
|
11
15
|
/** Two-letter initials shown on the 40px tile. */
|
|
12
16
|
initials: string;
|
|
17
|
+
/** Plugin name; truncated when the row gets narrow. */
|
|
13
18
|
name: ReactNode;
|
|
19
|
+
/** Version string drawn in mono next to the name. */
|
|
14
20
|
version?: ReactNode;
|
|
15
21
|
/** Text of the update pill next to the version; omit when up to date. */
|
|
16
22
|
updateBadge?: ReactNode;
|
|
23
|
+
/** Author line under the name. */
|
|
17
24
|
author?: ReactNode;
|
|
25
|
+
/** Adds the sky "Verified" check after the author; only shown when `author` is set. */
|
|
18
26
|
verified?: boolean;
|
|
19
27
|
/** Capability chips, each with an optional 11px icon. */
|
|
20
|
-
chips?: {
|
|
28
|
+
chips?: {
|
|
29
|
+
/** Stable key for the chip. */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Chip text. */
|
|
32
|
+
label: ReactNode;
|
|
33
|
+
/** Optional leading glyph, sized 11px. */
|
|
34
|
+
icon?: ReactNode;
|
|
35
|
+
}[];
|
|
36
|
+
/** Switch state for the plugin. Pass it with `onEnabledChange` to control the toggle. */
|
|
21
37
|
enabled?: boolean;
|
|
38
|
+
/** Fires with the next switch state when the plugin is enabled or disabled. */
|
|
22
39
|
onEnabledChange?: (enabled: boolean) => void;
|
|
23
|
-
/** Shows the Update button when set. */
|
|
40
|
+
/** Shows the Update button when set; without it the button stays collapsed. */
|
|
24
41
|
onUpdate?: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Label of the Update button.
|
|
44
|
+
* @default "Update"
|
|
45
|
+
*/
|
|
25
46
|
updateLabel?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Accessible name of the enable/disable switch.
|
|
49
|
+
* @default "Enabled"
|
|
50
|
+
*/
|
|
26
51
|
toggleLabel?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Accessible name of the overflow menu button.
|
|
54
|
+
* @default "More"
|
|
55
|
+
*/
|
|
27
56
|
overflowLabel?: string;
|
|
28
57
|
/** Items of the overflow menu; the menu button is hidden without them. */
|
|
29
58
|
menu?: ReactNode;
|
|
30
59
|
};
|
|
31
60
|
|
|
32
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Paper `Plugin row` (Settings 1VKS-0): tile, name with version and update pill, verified author, chips, and Update, Switch and overflow actions.
|
|
63
|
+
*
|
|
64
|
+
* Rows stack directly in a list: each one draws its own bottom divider and drops it on the last
|
|
65
|
+
* child. The Update button collapses when `onUpdate` is missing, and the overflow menu only renders
|
|
66
|
+
* when `menu` has content.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* import { MenuItem, PluginRow } from "@horon-ai/react";
|
|
71
|
+
*
|
|
72
|
+
* function Example() {
|
|
73
|
+
* return (
|
|
74
|
+
* <PluginRow
|
|
75
|
+
* initials="ST"
|
|
76
|
+
* name="Stripe"
|
|
77
|
+
* version="2.4.1"
|
|
78
|
+
* updateBadge="Update ready"
|
|
79
|
+
* author="Stripe"
|
|
80
|
+
* verified
|
|
81
|
+
* chips={[{ id: "payments", label: "Payments" }]}
|
|
82
|
+
* enabled
|
|
83
|
+
* onEnabledChange={() => {}}
|
|
84
|
+
* onUpdate={() => {}}
|
|
85
|
+
* menu={<MenuItem>Uninstall</MenuItem>}
|
|
86
|
+
* />
|
|
87
|
+
* );
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
33
91
|
export function PluginRow({
|
|
34
92
|
initials,
|
|
35
93
|
name,
|
|
@@ -3,13 +3,43 @@ import type { ComponentProps } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Props for {@link Progress}. The remaining Base UI `Progress.Root` props (`min`, `max`, `format`,
|
|
8
|
+
* `locale`, `getAriaValueText`, `aria-valuetext`) pass through; the label is a separate
|
|
9
|
+
* `Progress.Label` part, which this wrapper does not render.
|
|
10
|
+
*/
|
|
6
11
|
export type ProgressProps = Omit<ComponentProps<typeof BaseProgress.Root>, "className" | "value"> & {
|
|
12
|
+
/** Classes merged onto the 240px root. */
|
|
7
13
|
className?: string;
|
|
8
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Determinate progress in percent (0-100). Omit it (or pass `null`) for the indeterminate bar,
|
|
16
|
+
* which animates a third-width sweep inside the track.
|
|
17
|
+
* @default null
|
|
18
|
+
*/
|
|
9
19
|
value?: number | null;
|
|
10
20
|
};
|
|
11
21
|
|
|
12
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Figma `Progress bar` (in Loading 109:733): a 6px 8% ink track with an ink fill.
|
|
24
|
+
*
|
|
25
|
+
* The width animates on a 400ms quint curve when `value` changes, so a value fed from state reads
|
|
26
|
+
* as movement rather than a jump. Base UI supplies the `progressbar` role and the `aria-valuenow`
|
|
27
|
+
* wiring.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* import { Progress } from "@horon-ai/react";
|
|
32
|
+
*
|
|
33
|
+
* function Example() {
|
|
34
|
+
* return (
|
|
35
|
+
* <div>
|
|
36
|
+
* <Progress value={64} aria-label="Upload" />
|
|
37
|
+
* <Progress aria-label="Loading" />
|
|
38
|
+
* </div>
|
|
39
|
+
* );
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
13
43
|
export function Progress({ className, value = null, ...props }: ProgressProps) {
|
|
14
44
|
return (
|
|
15
45
|
<BaseProgress.Root value={value} className={cn("w-60", className)} {...props}>
|
|
@@ -5,17 +5,45 @@ import { type ComponentProps, createContext, type ReactNode, useContext } from "
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import type { ControlSize } from "../lib/size";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Mark size of a radio, on the shared control scale: `sm` 16px, `md` 20px, `lg` 24px.
|
|
10
|
+
* The group sets it once for every radio inside.
|
|
11
|
+
*/
|
|
8
12
|
export type RadioSize = ControlSize;
|
|
9
13
|
|
|
10
14
|
/** The group owns the size: every radio in one list reads the same mark. */
|
|
11
15
|
const RadioSizeContext = createContext<ControlSize>("md");
|
|
12
16
|
|
|
17
|
+
/** Props for {@link RadioGroup}; Base UI `RadioGroup` props such as `value`, `defaultValue`, `onValueChange` and `name` pass through. */
|
|
13
18
|
export type RadioGroupProps = Omit<ComponentProps<typeof BaseRadioGroup>, "className"> & {
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Mark size for every `Radio` inside; a radio can still override it.
|
|
21
|
+
* @default "md"
|
|
22
|
+
*/
|
|
15
23
|
size?: RadioSize;
|
|
24
|
+
/** Classes merged onto the group column, which stacks radios 12px apart. */
|
|
16
25
|
className?: string;
|
|
17
26
|
};
|
|
18
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Groups {@link Radio} items so only one can be selected, and shares one mark size with all of
|
|
30
|
+
* them. It renders a vertical flex column with a 12px gap and forwards its selection props to
|
|
31
|
+
* Base UI's radio group (keyboard roving included).
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* import { Radio, RadioGroup } from "@horon-ai/react";
|
|
36
|
+
*
|
|
37
|
+
* function Example() {
|
|
38
|
+
* return (
|
|
39
|
+
* <RadioGroup defaultValue="week" aria-label="Repeat">
|
|
40
|
+
* <Radio value="day" label="Every day" />
|
|
41
|
+
* <Radio value="week" label="Every week" />
|
|
42
|
+
* </RadioGroup>
|
|
43
|
+
* );
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
19
47
|
export function RadioGroup({ size = "md", className, ...props }: RadioGroupProps) {
|
|
20
48
|
return (
|
|
21
49
|
<RadioSizeContext.Provider value={size}>
|
|
@@ -42,14 +70,39 @@ const dotClass: Record<RadioSize, string> = {
|
|
|
42
70
|
lg: "size-2.5",
|
|
43
71
|
};
|
|
44
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Props for {@link Radio}; remaining Base UI `Radio.Root` props (`value`, `disabled`,
|
|
75
|
+
* `required`) pass through.
|
|
76
|
+
*/
|
|
45
77
|
export type RadioProps = Omit<ComponentProps<typeof BaseRadio.Root>, "className"> & {
|
|
78
|
+
/** Text or node after the mark; clicking it toggles the radio because both sit in one label. */
|
|
46
79
|
label?: ReactNode;
|
|
47
80
|
/** Overrides the enclosing `RadioGroup` size. */
|
|
48
81
|
size?: RadioSize;
|
|
82
|
+
/** Classes merged onto the wrapping label. */
|
|
49
83
|
className?: string;
|
|
50
84
|
};
|
|
51
85
|
|
|
52
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Figma `Radio` (58:239): a 20px circle; checked fills with ink and an 8px dot.
|
|
88
|
+
*
|
|
89
|
+
* The mark and the label share one `<label>`, so the label text is a click target and the mark
|
|
90
|
+
* takes focus from the group's keyboard roving. The dot pops in on check and scales out on uncheck.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* import { Radio, RadioGroup } from "@horon-ai/react";
|
|
95
|
+
*
|
|
96
|
+
* function Example() {
|
|
97
|
+
* return (
|
|
98
|
+
* <RadioGroup size="sm" defaultValue="one" aria-label="Priority">
|
|
99
|
+
* <Radio value="one" label="Low" />
|
|
100
|
+
* <Radio value="two" label="High" />
|
|
101
|
+
* </RadioGroup>
|
|
102
|
+
* );
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
53
106
|
export function Radio({ label, size, className, ...props }: RadioProps) {
|
|
54
107
|
const groupSize = useContext(RadioSizeContext);
|
|
55
108
|
const resolvedSize = size ?? groupSize;
|