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