@horon-ai/react 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@horon-ai/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/horon-ai/design.git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@base-ui/react": "^1.9.0",
|
|
32
32
|
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
33
|
-
"@horon-ai/tokens": "0.1.
|
|
33
|
+
"@horon-ai/tokens": "0.1.2",
|
|
34
34
|
"@phosphor-icons/react": "^2.1.10",
|
|
35
35
|
"@tanstack/react-table": "^9.3.1",
|
|
36
36
|
"pretendard": "^1.3.9",
|
|
@@ -5,8 +5,22 @@ import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import { Spinner } from "./spinner";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Figma `Thinking` (Agent activity 108:660): a 69x31 subtle pill with three 7px dots at 35/55/80% ink.
|
|
10
|
+
* Announced as `role="status"`; the three dots bounce with a 150ms stagger.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <ThinkingIndicator label="Searching the workspace…" />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
// prettier-ignore
|
|
18
|
+
export function ThinkingIndicator({ label = "Thinking", className }: {
|
|
19
|
+
/** Accessible status text, not rendered visibly. @default "Thinking" */
|
|
20
|
+
label?: string;
|
|
21
|
+
/** Extra classes merged onto the pill, for margins or alignment. */
|
|
22
|
+
className?: string;
|
|
23
|
+
}) {
|
|
10
24
|
return (
|
|
11
25
|
<span
|
|
12
26
|
role="status"
|
|
@@ -79,13 +93,15 @@ function useRevealedLength(text: string, smooth: boolean, reducedMotion: boolean
|
|
|
79
93
|
return Math.min(shown, text.length);
|
|
80
94
|
}
|
|
81
95
|
|
|
96
|
+
/** Props for {@link StreamingText}: the growing answer, whether it is still streaming, and whether chunks are revealed smoothly. */
|
|
82
97
|
export type StreamingTextProps = Omit<ComponentProps<"div">, "className" | "children"> & {
|
|
83
98
|
/** Everything received so far; pass the growing string as chunks arrive. */
|
|
84
99
|
text: string;
|
|
85
|
-
/** True while more text is coming: the caret shows, and the rest is revealed when it turns false. */
|
|
100
|
+
/** True while more text is coming: the caret shows, and the rest is revealed when it turns false. @default true */
|
|
86
101
|
streaming?: boolean;
|
|
87
|
-
/** Reveal arriving chunks a few characters per frame instead of in jumps. Off under reduced motion. */
|
|
102
|
+
/** Reveal arriving chunks a few characters per frame instead of in jumps. Off under reduced motion. @default true */
|
|
88
103
|
smooth?: boolean;
|
|
104
|
+
/** Extra classes merged onto the bubble, such as a maximum width. */
|
|
89
105
|
className?: string;
|
|
90
106
|
};
|
|
91
107
|
|
|
@@ -94,6 +110,16 @@ export type StreamingTextProps = Omit<ComponentProps<"div">, "className" | "chil
|
|
|
94
110
|
* right after the last character and wraps with it. The caret holds still while characters land and blinks
|
|
95
111
|
* while waiting for the next chunk; it goes away when the stream ends. Screen readers get the answer once,
|
|
96
112
|
* when it is complete, instead of every chunk.
|
|
113
|
+
*
|
|
114
|
+
* `text` is the whole answer so far, not a delta: passing only the newest chunk would drop earlier text. The
|
|
115
|
+
* reveal restarts from the shared prefix when `text` is rewritten rather than extended.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```tsx
|
|
119
|
+
* function Answer({ text, done }: { text: string; done: boolean }) {
|
|
120
|
+
* return <StreamingText text={text} streaming={!done} />;
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
97
123
|
*/
|
|
98
124
|
export function StreamingText({ text, streaming = true, smooth = true, className, ...props }: StreamingTextProps) {
|
|
99
125
|
const reducedMotion = useReducedMotion();
|
|
@@ -124,16 +150,31 @@ export function StreamingText({ text, streaming = true, smooth = true, className
|
|
|
124
150
|
);
|
|
125
151
|
}
|
|
126
152
|
|
|
153
|
+
/** Progress of a tool call: `running` shows a spinner, `done` a check, `failed` a warning and danger text. */
|
|
127
154
|
export type ToolStepState = "running" | "done" | "failed";
|
|
128
155
|
|
|
156
|
+
/** Props for {@link ToolStep}: the tool line, an optional detail line and the progress state. */
|
|
129
157
|
export type ToolStepProps = Omit<ComponentProps<"button">, "className" | "title"> & {
|
|
158
|
+
/** Progress state, which also picks the leading icon and the title colour. @default "running" */
|
|
130
159
|
state?: ToolStepState;
|
|
160
|
+
/** The tool call line, truncated to one line. */
|
|
131
161
|
title: ReactNode;
|
|
162
|
+
/** Optional second line, such as the target or a short result, truncated to one line. */
|
|
132
163
|
detail?: ReactNode;
|
|
164
|
+
/** Extra classes merged onto the row, for margins. */
|
|
133
165
|
className?: string;
|
|
134
166
|
};
|
|
135
167
|
|
|
136
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Figma `Tool step` (108:660): a flat 12px-radius row with spinner, check or warning, and a caret.
|
|
170
|
+
* Renders a real `button`, so pass `onClick` to open the call's detail; the caret does not expand anything
|
|
171
|
+
* on its own.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```tsx
|
|
175
|
+
* <ToolStep state="done" title="Search the web" detail="3 results" onClick={openCall} />
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
137
178
|
export function ToolStep({ state = "running", title, detail, className, ...props }: ToolStepProps) {
|
|
138
179
|
return (
|
|
139
180
|
<button
|
|
@@ -158,16 +199,43 @@ export function ToolStep({ state = "running", title, detail, className, ...props
|
|
|
158
199
|
);
|
|
159
200
|
}
|
|
160
201
|
|
|
161
|
-
|
|
202
|
+
/** One compact line inside a work log. */
|
|
203
|
+
export type WorkLogStep = {
|
|
204
|
+
/** Stable key for the step. */
|
|
205
|
+
id: string;
|
|
206
|
+
/** Step text, truncated to one line. */
|
|
207
|
+
title: ReactNode;
|
|
208
|
+
/** Optional second line with a short result or target. */
|
|
209
|
+
detail?: ReactNode;
|
|
210
|
+
/** Shows a small spinner instead of the check while the step is still running. */
|
|
211
|
+
running?: boolean;
|
|
212
|
+
};
|
|
162
213
|
|
|
163
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Figma `Work log` (108:660): a flat subtle card with a wrench summary row and compact steps. It is a native
|
|
216
|
+
* `<details open>`, so the summary collapses and expands without extra state; the caret rotates while closed.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```tsx
|
|
220
|
+
* <WorkLog
|
|
221
|
+
* summary="4 steps"
|
|
222
|
+
* steps={[
|
|
223
|
+
* { id: "read", title: "Read the sheet", detail: "18 rows", running: false },
|
|
224
|
+
* { id: "write", title: "Draft the summary", running: true },
|
|
225
|
+
* ]}
|
|
226
|
+
* />
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
164
229
|
export function WorkLog({
|
|
165
230
|
summary,
|
|
166
231
|
steps,
|
|
167
232
|
className,
|
|
168
233
|
}: {
|
|
234
|
+
/** Summary line in the toggle row, such as "4 steps". */
|
|
169
235
|
summary: ReactNode;
|
|
236
|
+
/** Steps listed under the summary. */
|
|
170
237
|
steps: WorkLogStep[];
|
|
238
|
+
/** Extra classes merged onto the card, for margins or a width override. */
|
|
171
239
|
className?: string;
|
|
172
240
|
}) {
|
|
173
241
|
return (
|
|
@@ -4,20 +4,36 @@ import type { ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { Spinner } from "./spinner";
|
|
6
6
|
|
|
7
|
+
/** Lifecycle of the lane: `running` shows a spinner and a pulsing tile, `done` a check, `failed` a warning and a danger rim. */
|
|
7
8
|
export type AgentLaneStatus = "running" | "done" | "failed";
|
|
8
9
|
|
|
10
|
+
/** Props for {@link AgentLane}: the sub-agent identity, its task, and the optional tool, output, usage and timing lines. */
|
|
9
11
|
export type AgentLaneProps = {
|
|
12
|
+
/** Lifecycle state; also picks the tile shade (sky-400, sky-600, sky-800) and the result icon. */
|
|
10
13
|
status: AgentLaneStatus;
|
|
11
|
-
|
|
14
|
+
/** Identity of the sub-agent shown in the header. */
|
|
15
|
+
bot: {
|
|
16
|
+
/** One-character tile label, usually the first letter of the name. */
|
|
17
|
+
initial: string;
|
|
18
|
+
/** Display name, truncated when the header runs out of room. */
|
|
19
|
+
name: ReactNode;
|
|
20
|
+
};
|
|
12
21
|
/** Duration, such as "1.9s". */
|
|
13
22
|
elapsed?: ReactNode;
|
|
23
|
+
/** The short task line under the header, wrapping onto several lines. */
|
|
14
24
|
task: ReactNode;
|
|
15
25
|
/** 14px tool icon and source, such as "Notion · Q3 metrics dashboard". */
|
|
16
|
-
tool?: {
|
|
26
|
+
tool?: {
|
|
27
|
+
/** Tool icon, sized 14px by the row. */
|
|
28
|
+
icon: ReactNode;
|
|
29
|
+
/** Tool and source description, truncated to one line. */
|
|
30
|
+
label: ReactNode;
|
|
31
|
+
};
|
|
17
32
|
/** Latest output, or the error when failed. */
|
|
18
33
|
output?: ReactNode;
|
|
19
34
|
/** Usage caption, such as "2.6k tokens". */
|
|
20
35
|
usage?: ReactNode;
|
|
36
|
+
/** Extra classes merged onto the card, for layout inside a lane grid. */
|
|
21
37
|
className?: string;
|
|
22
38
|
};
|
|
23
39
|
|
|
@@ -27,7 +43,20 @@ const botTile: Record<AgentLaneStatus, string> = {
|
|
|
27
43
|
failed: "bg-sky-800",
|
|
28
44
|
};
|
|
29
45
|
|
|
30
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Figma `Agent lane` (Agent orchestrator 113:953): one sub-agent's card in a fan-out, flat subtle with a red rim on failure.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <AgentLane
|
|
52
|
+
* status="running"
|
|
53
|
+
* bot={{ initial: "R", name: "Researcher" }}
|
|
54
|
+
* task="Gather the Q3 metrics"
|
|
55
|
+
* tool={{ icon: <span aria-hidden>N</span>, label: "Notion · Q3 metrics dashboard" }}
|
|
56
|
+
* elapsed="1.9s"
|
|
57
|
+
* />
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
31
60
|
export function AgentLane({ status, bot, elapsed, task, tool, output, usage, className }: AgentLaneProps) {
|
|
32
61
|
return (
|
|
33
62
|
<div
|
|
@@ -4,58 +4,99 @@ import { type ComponentProps, type CSSProperties, type ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { Spinner } from "./spinner";
|
|
6
6
|
|
|
7
|
+
/** Bar tone: done is 6% ink, running is 20% live green, failed is 14% red, waiting is an outlined brand bar. */
|
|
7
8
|
export type LaneTaskTone = "done" | "running" | "failed" | "waiting";
|
|
8
9
|
|
|
10
|
+
/** One bar on a bot's row: its label, time range in minutes before the window end, tone and optional badge. */
|
|
9
11
|
export type LaneTask = {
|
|
12
|
+
/** Stable key; also how a `LaneHandoff` refers to this task. */
|
|
10
13
|
id: string;
|
|
14
|
+
/** Text inside the bar, truncated when the bar is too narrow. */
|
|
11
15
|
label: ReactNode;
|
|
16
|
+
/** 14px icon before the label. */
|
|
12
17
|
icon?: ReactNode;
|
|
18
|
+
/** Bar fill and text colour. */
|
|
13
19
|
tone: LaneTaskTone;
|
|
14
20
|
/** Minutes before the end of the window, 0 is now: a task from 28 minutes ago to 16 minutes ago is `-28` to `-16`. */
|
|
15
21
|
start: number;
|
|
22
|
+
/** Minutes before the end of the window where the bar ends; must be greater than `start`. */
|
|
16
23
|
end: number;
|
|
17
24
|
/** A pill inside the bar, such as "Needs you". */
|
|
18
25
|
badge?: ReactNode;
|
|
19
26
|
};
|
|
20
27
|
|
|
28
|
+
/** Avatar tint of a bot row: `sky` is sky-500, `steel` sky-700, `neutral` neutral-500 and `ink` the brand fill. */
|
|
21
29
|
export type LaneBotTone = "sky" | "steel" | "neutral" | "ink";
|
|
22
30
|
|
|
31
|
+
/** One lane in the timeline: the bot's identity, its tag and the task bars on its row. */
|
|
23
32
|
export type LaneBot = {
|
|
33
|
+
/** Stable key for the row; also the lane order. */
|
|
24
34
|
id: string;
|
|
35
|
+
/** Bot name shown beside the avatar. */
|
|
25
36
|
name: ReactNode;
|
|
37
|
+
/** Second line under the name, such as the tools the bot uses. */
|
|
26
38
|
tools?: ReactNode;
|
|
39
|
+
/** One-character avatar label, usually the first letter of the name. */
|
|
27
40
|
initial: string;
|
|
41
|
+
/** Avatar fill, chosen to keep neighbouring bots distinct. */
|
|
28
42
|
tone: LaneBotTone;
|
|
29
43
|
/** A small tag next to the name: "Idle", "Needs you". */
|
|
30
44
|
badge?: ReactNode;
|
|
45
|
+
/** Tag fill: `ink` is the brand fill, `subtle` the default 6% ink. @default "subtle" */
|
|
31
46
|
badgeTone?: "subtle" | "ink";
|
|
47
|
+
/** Bars on this bot's row; tasks from different bots may overlap in time. */
|
|
32
48
|
tasks: LaneTask[];
|
|
33
49
|
};
|
|
34
50
|
|
|
35
|
-
|
|
51
|
+
/** A dashed curve from one task's end to another task's start, such as a hand-off to the next bot. */
|
|
52
|
+
export type LaneHandoff = {
|
|
53
|
+
/** Task id the curve starts from. */
|
|
54
|
+
from: string;
|
|
55
|
+
/** Task id the curve ends at. */
|
|
56
|
+
to: string;
|
|
57
|
+
};
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
/** A legend entry in the header: a dot plus a label. */
|
|
60
|
+
export type LaneStatus = {
|
|
61
|
+
/** Stable key for the entry. */
|
|
62
|
+
id: string;
|
|
63
|
+
/** `live` paints the dot green, `ink` paints it with the brand fill. */
|
|
64
|
+
tone: "live" | "ink";
|
|
65
|
+
/** Legend text, such as "2 running". */
|
|
66
|
+
label: ReactNode;
|
|
67
|
+
};
|
|
38
68
|
|
|
69
|
+
/** The approval strip below the timeline: one question and its two actions. */
|
|
39
70
|
export type AgentLanesTimelineApproval = {
|
|
71
|
+
/** The approval question in the strip at the bottom. */
|
|
40
72
|
message: ReactNode;
|
|
73
|
+
/** Fires when "Later" is pressed. */
|
|
41
74
|
onLater?: () => void;
|
|
75
|
+
/** Fires when "Approve" is pressed. */
|
|
42
76
|
onApprove?: () => void;
|
|
43
77
|
};
|
|
44
78
|
|
|
79
|
+
/** Props for {@link AgentLanesTimeline}: the lanes, the time window and optional header, hand-offs and approval strip. */
|
|
45
80
|
export type AgentLanesTimelineProps = Omit<ComponentProps<"section">, "className" | "title"> & {
|
|
81
|
+
/** Panel heading. @default "Bots" */
|
|
46
82
|
title?: ReactNode;
|
|
83
|
+
/** Legend entries shown next to the heading. @default [] */
|
|
47
84
|
statuses?: LaneStatus[];
|
|
48
|
-
/** The range pill at the header's right edge. */
|
|
85
|
+
/** The range pill at the header's right edge. @default "Last 30m" */
|
|
49
86
|
range?: ReactNode;
|
|
87
|
+
/** One row per bot, in lane order; at least two row heights are always reserved. */
|
|
50
88
|
bots: LaneBot[];
|
|
89
|
+
/** Hand-off curves drawn over the rows; a hand-off naming an unknown task is skipped. @default [] */
|
|
51
90
|
handoffs?: LaneHandoff[];
|
|
52
|
-
/** Window length in minutes; the tick labels sit every five. */
|
|
91
|
+
/** Window length in minutes; the tick labels sit every five. @default 30 */
|
|
53
92
|
windowMinutes?: number;
|
|
54
|
-
/** Where the current line sits, in minutes before the end of the window. The line glides to a new value in 280ms. */
|
|
93
|
+
/** Where the current line sits, in minutes before the end of the window. The line glides to a new value in 280ms. @default 0 */
|
|
55
94
|
cursor?: number;
|
|
95
|
+
/** Adds the approval strip below the timeline. */
|
|
56
96
|
approval?: AgentLanesTimelineApproval;
|
|
57
97
|
/** Makes the range pill actionable; without it the range renders as a plain label. */
|
|
58
98
|
onRangeClick?: () => void;
|
|
99
|
+
/** Extra classes merged onto the panel, for margins or a width override. */
|
|
59
100
|
className?: string;
|
|
60
101
|
};
|
|
61
102
|
|
|
@@ -75,7 +116,10 @@ const barTone: Record<LaneTaskTone, string> = {
|
|
|
75
116
|
|
|
76
117
|
const noStatuses: LaneStatus[] = [];
|
|
77
118
|
const noHandoffs: LaneHandoff[] = [];
|
|
78
|
-
type RowStyle = CSSProperties & {
|
|
119
|
+
type RowStyle = CSSProperties & {
|
|
120
|
+
/** Lane index, consumed by the staggered `motion-rise` delay. */
|
|
121
|
+
"--i": number;
|
|
122
|
+
};
|
|
79
123
|
const rowStyle = (lane: number): RowStyle => ({ top: TOP + lane * ROW, height: ROW, "--i": lane });
|
|
80
124
|
|
|
81
125
|
const ROW = 64;
|
|
@@ -91,6 +135,32 @@ const trackW = (fraction: number) => `calc(${TRACK} * ${fraction})`;
|
|
|
91
135
|
* Paper `Agent lanes` (YCO-1): a 24px-radius surface panel with one lane per bot on a shared time axis, a current
|
|
92
136
|
* line, hand-off curves between bots and an approval strip. The current line glides in 280ms with the emphasized
|
|
93
137
|
* easing, rows tint on hover in 150ms, and running bars carry a spinner.
|
|
138
|
+
*
|
|
139
|
+
* The track has a 700px minimum width and scrolls horizontally, so narrow panels stay readable instead of
|
|
140
|
+
* squeezing the bars. Times are minutes before the window's end, not timestamps.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```tsx
|
|
144
|
+
* <AgentLanesTimeline
|
|
145
|
+
* bots={[
|
|
146
|
+
* {
|
|
147
|
+
* id: "researcher",
|
|
148
|
+
* name: "Researcher",
|
|
149
|
+
* initial: "R",
|
|
150
|
+
* tone: "sky",
|
|
151
|
+
* tasks: [{ id: "pull", label: "Pull metrics", tone: "done", start: -28, end: -16 }],
|
|
152
|
+
* },
|
|
153
|
+
* {
|
|
154
|
+
* id: "writer",
|
|
155
|
+
* name: "Writer",
|
|
156
|
+
* initial: "W",
|
|
157
|
+
* tone: "steel",
|
|
158
|
+
* tasks: [{ id: "draft", label: "Draft summary", tone: "running", start: -16, end: 0 }],
|
|
159
|
+
* },
|
|
160
|
+
* ]}
|
|
161
|
+
* handoffs={[{ from: "pull", to: "draft" }]}
|
|
162
|
+
* />
|
|
163
|
+
* ```
|
|
94
164
|
*/
|
|
95
165
|
export function AgentLanesTimeline({
|
|
96
166
|
title = "Bots",
|
|
@@ -106,7 +176,15 @@ export function AgentLanesTimeline({
|
|
|
106
176
|
...props
|
|
107
177
|
}: AgentLanesTimelineProps) {
|
|
108
178
|
const ticks = Array.from({ length: windowMinutes / 5 - 1 }, (_, i) => -(windowMinutes - 5 * (i + 1)));
|
|
109
|
-
const taskAt = new Map<
|
|
179
|
+
const taskAt = new Map<
|
|
180
|
+
string,
|
|
181
|
+
{
|
|
182
|
+
/** The task the hand-off refers to. */
|
|
183
|
+
task: LaneTask;
|
|
184
|
+
/** Lane index of the bot that owns the task, used for the curve's vertical endpoints. */
|
|
185
|
+
lane: number;
|
|
186
|
+
}
|
|
187
|
+
>();
|
|
110
188
|
bots.forEach((bot, lane) => {
|
|
111
189
|
for (const task of bot.tasks) taskAt.set(task.id, { task, lane });
|
|
112
190
|
});
|
|
@@ -7,9 +7,22 @@ import { cn } from "../lib/cn";
|
|
|
7
7
|
import { isModKey } from "../lib/platform";
|
|
8
8
|
import { Kbd } from "./kbd";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** Drops the base type's `className` so the panel can own it. */
|
|
11
|
+
type WithClass<T> = Omit<T, "className"> & {
|
|
12
|
+
/** Extra classes merged onto the root element. */
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
11
15
|
|
|
12
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* The solid panel that hosts the Figma Agents blocks (Agent tasks 112:797): surface fill, 24px radius, 16px padding.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <AgentPanel filter={<FilterBar items={[{ value: "all", label: "All" }]} />}>
|
|
22
|
+
* <TaskComposer onStart={start} pickers={<ComposerChip leading="R">Researcher</ComposerChip>} />
|
|
23
|
+
* </AgentPanel>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
13
26
|
export function AgentPanel({
|
|
14
27
|
className,
|
|
15
28
|
filter,
|
|
@@ -32,13 +45,18 @@ export function AgentPanel({
|
|
|
32
45
|
);
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
/** Props for {@link ComposerChip}: a leading tile letter or icon, its fill, and the usual button props. */
|
|
35
49
|
export type ComposerChipProps = WithClass<ComponentProps<"button">> & {
|
|
36
50
|
/** 16px tile initial (bots) or a 14px icon (projects). */
|
|
37
51
|
leading?: ReactNode;
|
|
52
|
+
/** Tile fill, defaulting to the brand; pass a sky or neutral shade to match the bot it stands for. */
|
|
38
53
|
tileClassName?: string;
|
|
39
54
|
};
|
|
40
55
|
|
|
41
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* A 30px 5% ink picker chip with a 12px caret, used for the bot and project in the task composer.
|
|
58
|
+
* Part of {@link TaskComposer}; the chip is a plain button, so it needs its own `onClick` or menu wiring.
|
|
59
|
+
*/
|
|
42
60
|
export function ComposerChip({ leading, tileClassName, className, children, ...props }: ComposerChipProps) {
|
|
43
61
|
return (
|
|
44
62
|
<button
|
|
@@ -67,18 +85,42 @@ export function ComposerChip({ leading, tileClassName, className, children, ...p
|
|
|
67
85
|
);
|
|
68
86
|
}
|
|
69
87
|
|
|
88
|
+
/** Props for {@link TaskComposer}: the draft text, picker chips, the start handler and the labels. */
|
|
70
89
|
export type TaskComposerProps = {
|
|
90
|
+
/** Placeholder of the textarea; also its accessible name. @default "Describe a task for a bot" */
|
|
71
91
|
placeholder?: string;
|
|
92
|
+
/** Controlled draft text; leave undefined and pass `onValueChange` to lift the text, or omit both for a free draft. */
|
|
72
93
|
value?: string;
|
|
94
|
+
/** Fires on every keystroke with the next text, so the caller can hold the draft. */
|
|
73
95
|
onValueChange?: (value: string) => void;
|
|
74
96
|
/** Picker chips, usually two `ComposerChip`s. */
|
|
75
97
|
pickers?: ReactNode;
|
|
98
|
+
/** Fires on the Start button and on Mod+Enter; the composer does not clear the draft itself. */
|
|
76
99
|
onStart?: () => void;
|
|
100
|
+
/** Label of the start button. @default "Start" */
|
|
77
101
|
startLabel?: string;
|
|
102
|
+
/** Extra classes merged onto the box, for margins or width overrides. */
|
|
78
103
|
className?: string;
|
|
79
104
|
};
|
|
80
105
|
|
|
81
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Figma task `Composer` (Agent tasks 112:797): a 4% ink box, 20px radius, with pickers, a key hint and Start.
|
|
108
|
+
*
|
|
109
|
+
* The textarea starts at one row and grows with its content up to 160px (`max-h-40`) before it scrolls.
|
|
110
|
+
* Mod+Enter submits: ⌘Enter on macOS and iOS, Ctrl+Enter on Windows, Linux and Android. The hint is a
|
|
111
|
+
* `Kbd keys="Mod+Enter"` chip, so it draws the same OS keys, and it is hidden on touch devices that have
|
|
112
|
+
* no hovering fine pointer.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```tsx
|
|
116
|
+
* <TaskComposer
|
|
117
|
+
* value={draft}
|
|
118
|
+
* onValueChange={setDraft}
|
|
119
|
+
* onStart={start}
|
|
120
|
+
* pickers={<ComposerChip leading="R">Researcher</ComposerChip>}
|
|
121
|
+
* />
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
82
124
|
export function TaskComposer({
|
|
83
125
|
placeholder = "Describe a task for a bot",
|
|
84
126
|
value,
|
|
@@ -8,45 +8,72 @@ import { isModKey } from "../lib/platform";
|
|
|
8
8
|
import { Kbd } from "./kbd";
|
|
9
9
|
import { Spinner } from "./spinner";
|
|
10
10
|
|
|
11
|
+
/** Progress of one step: `done` fills the marker green, `active` shows a spinner, `todo` a dashed ring. */
|
|
11
12
|
export type SteeringStepStatus = "done" | "active" | "todo";
|
|
12
13
|
|
|
14
|
+
/** One step in the steering rail: its label, progress status and optional duration. */
|
|
13
15
|
export type SteeringStep = {
|
|
16
|
+
/** Stable key for the step. */
|
|
14
17
|
id: string;
|
|
18
|
+
/** Step text; secondary colour while the step is still `todo`. */
|
|
15
19
|
label: ReactNode;
|
|
20
|
+
/** Progress marker to draw. */
|
|
16
21
|
status: SteeringStepStatus;
|
|
17
22
|
/** Duration of a finished or running step, such as "1.4s". */
|
|
18
23
|
time?: ReactNode;
|
|
19
24
|
};
|
|
20
25
|
|
|
26
|
+
/** One steer note: the text and whether it has already been applied. */
|
|
21
27
|
export type SteeringNote = {
|
|
28
|
+
/** Stable key for the note. */
|
|
22
29
|
id: string;
|
|
30
|
+
/** The steer text, shown on one row and wrapping as needed. */
|
|
23
31
|
text: ReactNode;
|
|
24
|
-
/** An applied note has left the queue and carries the "Now" pill. */
|
|
32
|
+
/** An applied note has left the queue and carries the "Now" pill. @default false */
|
|
25
33
|
applied?: boolean;
|
|
26
34
|
};
|
|
27
35
|
|
|
36
|
+
/** Props for {@link AgentSteeringPanel}: the task header, the steps, queued notes and the steer handlers. */
|
|
28
37
|
export type AgentSteeringPanelProps = Omit<ComponentProps<"section">, "className" | "title"> & {
|
|
38
|
+
/** Task name in the header, truncated to one line. */
|
|
29
39
|
title: ReactNode;
|
|
40
|
+
/** Second header line, such as the bot or workspace, truncated to one line. */
|
|
30
41
|
subtitle?: ReactNode;
|
|
31
42
|
/** The status text beside the live dot, such as "Running · 2m 10s". */
|
|
32
43
|
status?: ReactNode;
|
|
44
|
+
/** Pauses the live dot pulse and swaps the button to Play; the panel does not stop work on its own. @default false */
|
|
33
45
|
paused?: boolean;
|
|
46
|
+
/** Fires when the pause/resume button is pressed, with the next paused value. */
|
|
34
47
|
onPausedChange?: (paused: boolean) => void;
|
|
48
|
+
/** Ordered steps of the running task; the connector under a done step turns live green. */
|
|
35
49
|
steps: SteeringStep[];
|
|
50
|
+
/** Notes queued for the next step, plus any already applied. @default [] */
|
|
36
51
|
notes?: SteeringNote[];
|
|
52
|
+
/** Fires with the note id when its "Now" button is pressed, to apply it immediately. */
|
|
37
53
|
onApplyNow?: (id: string) => void;
|
|
54
|
+
/** Fires with the note id after its pop-out animation ends; remove it from `notes` here. */
|
|
38
55
|
onRemoveNote?: (id: string) => void;
|
|
39
56
|
/** Called with the text typed in the steer box: Enter queues it for the next step, Mod+Enter interrupts now. */
|
|
40
57
|
onSteer?: (text: string, interrupt: boolean) => void;
|
|
58
|
+
/** Placeholder of the steer input; also its accessible name. @default "Steer the bot…" */
|
|
41
59
|
placeholder?: string;
|
|
60
|
+
/** Extra classes merged onto the panel, for margins or a width override. */
|
|
42
61
|
className?: string;
|
|
43
62
|
};
|
|
44
63
|
|
|
45
64
|
const noNotes: SteeringNote[] = [];
|
|
46
|
-
type IndexStyle = CSSProperties & {
|
|
65
|
+
type IndexStyle = CSSProperties & {
|
|
66
|
+
/** Step index, consumed by the staggered `motion-rise` delay. */
|
|
67
|
+
"--i": number;
|
|
68
|
+
};
|
|
47
69
|
const indexStyle = (i: number): IndexStyle => ({ "--i": i });
|
|
48
70
|
|
|
49
|
-
|
|
71
|
+
/** Marker for one step: green check when done, spinner when active, dashed ring while waiting. */
|
|
72
|
+
// prettier-ignore
|
|
73
|
+
function Marker({ status }: {
|
|
74
|
+
/** Progress state, also used as the marker's React key so the pop animation replays on change. */
|
|
75
|
+
status: SteeringStepStatus;
|
|
76
|
+
}) {
|
|
50
77
|
if (status === "done") {
|
|
51
78
|
return (
|
|
52
79
|
<span className="flex size-4.5 animate-pop-in items-center justify-center rounded-full bg-status-live text-text-on-ink">
|
|
@@ -58,13 +85,17 @@ function Marker({ status }: { status: SteeringStepStatus }) {
|
|
|
58
85
|
return <span aria-label="Waiting" className="size-4.5 rounded-full border border-dashed border-border-dashed" />;
|
|
59
86
|
}
|
|
60
87
|
|
|
88
|
+
/** One queued note: the text, a "Now" action and a remove button, or a filled "Now" pill once applied. */
|
|
61
89
|
function SteeringNoteRow({
|
|
62
90
|
note,
|
|
63
91
|
onApplyNow,
|
|
64
92
|
onRemoveNote,
|
|
65
93
|
}: {
|
|
94
|
+
/** The note to render; `applied` swaps the actions for the filled pill. */
|
|
66
95
|
note: SteeringNote;
|
|
96
|
+
/** Called with the note id when "Now" is pressed. */
|
|
67
97
|
onApplyNow?: (id: string) => void;
|
|
98
|
+
/** Called with the note id when the pop-out animation finishes, so the parent can drop it. */
|
|
68
99
|
onRemoveNote?: (id: string) => void;
|
|
69
100
|
}) {
|
|
70
101
|
const [leaving, setLeaving] = useState(false);
|
|
@@ -121,6 +152,25 @@ function SteeringNoteRow({
|
|
|
121
152
|
* Paper `Agent steering` (Y51-1): a 24px-radius surface panel with the running task header, a step rail, the notes
|
|
122
153
|
* queued for the next step, and a steer box. The live dot pulses every 2s, a step marker swaps with a short pop,
|
|
123
154
|
* new steps rise in 220ms, and send and pause press in 150ms.
|
|
155
|
+
*
|
|
156
|
+
* The steer draft is local state: it clears after `onSteer` fires, and the panel never queues a note itself.
|
|
157
|
+
* Enter queues the text for the next step and Mod+Enter sends it as an interrupt — ⌘Enter on macOS and iOS,
|
|
158
|
+
* Ctrl+Enter on Windows, Linux and Android. The two hint chips draw the same OS keys through `Kbd` and are
|
|
159
|
+
* hidden on touch devices that show no keyboard hints. IME composition keystrokes are ignored so Korean and
|
|
160
|
+
* Japanese input do not submit mid-character.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```tsx
|
|
164
|
+
* <AgentSteeringPanel
|
|
165
|
+
* title="Quarterly revenue review"
|
|
166
|
+
* status="Running · 2m 10s"
|
|
167
|
+
* steps={[
|
|
168
|
+
* { id: "read", label: "Read the sheet", status: "done", time: "1.4s" },
|
|
169
|
+
* { id: "summarise", label: "Summarise the rows", status: "active" },
|
|
170
|
+
* ]}
|
|
171
|
+
* onSteer={(text, interrupt) => steer(text, interrupt)}
|
|
172
|
+
* />
|
|
173
|
+
* ```
|
|
124
174
|
*/
|
|
125
175
|
export function AgentSteeringPanel({
|
|
126
176
|
title,
|
|
@@ -5,12 +5,16 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { Button } from "./button";
|
|
6
6
|
import { LiquidGlass } from "./liquid-glass";
|
|
7
7
|
|
|
8
|
+
/** Lifecycle of the request: `pending` keeps the brand rim and both actions, the other states hide the actions. */
|
|
8
9
|
export type ApprovalState = "pending" | "approved" | "declined";
|
|
10
|
+
/** `solid` paints the raised surface, `glass` layers the glass material for use over imagery. */
|
|
9
11
|
export type ApprovalCardSurface = "solid" | "glass";
|
|
10
12
|
|
|
13
|
+
/** Props for {@link ApprovalCard}: the state, the question, the optional attachment and the action handlers. */
|
|
11
14
|
export type ApprovalCardProps = {
|
|
15
|
+
/** Lifecycle state; drives the status dot, status text and whether the actions stay visible. @default "pending" */
|
|
12
16
|
state?: ApprovalState;
|
|
13
|
-
/** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. */
|
|
17
|
+
/** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. @default "solid" */
|
|
14
18
|
surface?: ApprovalCardSurface;
|
|
15
19
|
/** Status line, defaults to "Needs your yes", "Approved" or "Declined". */
|
|
16
20
|
status?: ReactNode;
|
|
@@ -18,14 +22,29 @@ export type ApprovalCardProps = {
|
|
|
18
22
|
title: ReactNode;
|
|
19
23
|
/** Name of the attached item. */
|
|
20
24
|
attachment?: ReactNode;
|
|
25
|
+
/** Fires when the primary action is pressed; the card does not change its own state. */
|
|
21
26
|
onApprove?: () => void;
|
|
27
|
+
/** Fires when the secondary action is pressed, for deferring without deciding. */
|
|
22
28
|
onLater?: () => void;
|
|
29
|
+
/** Label of the primary button. @default "Approve" */
|
|
23
30
|
approveLabel?: string;
|
|
31
|
+
/** Label of the secondary button. @default "Later" */
|
|
24
32
|
laterLabel?: string;
|
|
33
|
+
/** Extra classes merged onto the card, for margins or a width override. */
|
|
25
34
|
className?: string;
|
|
26
35
|
};
|
|
27
36
|
|
|
28
|
-
const statusClass: Record<
|
|
37
|
+
const statusClass: Record<
|
|
38
|
+
ApprovalState,
|
|
39
|
+
{
|
|
40
|
+
/** Background of the status dot. */
|
|
41
|
+
dot: string;
|
|
42
|
+
/** Text colour of the status line. */
|
|
43
|
+
text: string;
|
|
44
|
+
/** Default status line when `status` is omitted. */
|
|
45
|
+
label: string;
|
|
46
|
+
}
|
|
47
|
+
> = {
|
|
29
48
|
pending: { dot: "bg-status-live", text: "text-text-brand", label: "Needs your yes" },
|
|
30
49
|
approved: { dot: "bg-text-secondary", text: "text-text-secondary", label: "Approved" },
|
|
31
50
|
declined: { dot: "bg-accent-red-500-90", text: "text-text-danger", label: "Declined" },
|
|
@@ -34,6 +53,19 @@ const statusClass: Record<ApprovalState, { dot: string; text: string; label: str
|
|
|
34
53
|
/**
|
|
35
54
|
* Figma `Approval card` (64:398): the conversation part that asks for an action.
|
|
36
55
|
* Pending keeps the brand rim and the actions.
|
|
56
|
+
*
|
|
57
|
+
* Controlled by `state`: the card never flips itself, and once it leaves `pending` the actions collapse and
|
|
58
|
+
* become inert, so a settled request cannot be answered twice.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <ApprovalCard
|
|
63
|
+
* title="Send the Q3 board deck to the team?"
|
|
64
|
+
* attachment="Q3-board-deck.pdf"
|
|
65
|
+
* onApprove={send}
|
|
66
|
+
* onLater={dismiss}
|
|
67
|
+
* />
|
|
68
|
+
* ```
|
|
37
69
|
*/
|
|
38
70
|
export function ApprovalCard({
|
|
39
71
|
state = "pending",
|