@horon-ai/react 0.1.0 → 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 +81 -11
- package/src/components/agent-lane.tsx +32 -3
- package/src/components/agent-lanes-timeline.tsx +85 -7
- package/src/components/agent-panel.tsx +53 -7
- package/src/components/agent-steering-panel.tsx +73 -21
- 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 +92 -9
- 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 +89 -10
- 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 +39 -2
- package/src/components/flue-background.tsx +91 -21
- 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 +37 -4
- 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 +54 -18
- 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 +58 -3
- 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 +48 -3
- 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 +91 -17
- 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/index.ts +8 -0
- package/src/{lib → hooks}/use-active-indicator.ts +44 -1
- package/src/hooks/use-color-scheme.ts +50 -0
- package/src/hooks/use-copy-to-clipboard.ts +55 -0
- package/src/hooks/use-device.ts +69 -0
- package/src/hooks/use-hotkey.ts +56 -0
- package/src/hooks/use-media-query.ts +43 -0
- package/src/hooks/use-platform.ts +68 -0
- package/src/hooks/use-reduced-motion.ts +25 -0
- package/src/index.ts +44 -0
- package/src/lib/cn.ts +13 -1
- package/src/lib/cursor-ghost.ts +16 -6
- package/src/lib/platform.ts +236 -0
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",
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { CaretDown, CaretRight, CheckCircle, WarningCircle, Wrench } from "@phosphor-icons/react";
|
|
2
2
|
import { type ComponentProps, type ReactNode, useEffect, useRef, useState } from "react";
|
|
3
3
|
|
|
4
|
+
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
4
5
|
import { cn } from "../lib/cn";
|
|
5
6
|
import { Spinner } from "./spinner";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
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
|
+
}) {
|
|
9
24
|
return (
|
|
10
25
|
<span
|
|
11
26
|
role="status"
|
|
@@ -37,7 +52,7 @@ function codePointEnd(text: string, end: number): number {
|
|
|
37
52
|
* bursty chunks read as steady typing; otherwise, and under reduced motion, everything shows at once.
|
|
38
53
|
* Text that no longer extends what was shown (a rewrite) restarts from the shared prefix.
|
|
39
54
|
*/
|
|
40
|
-
function useRevealedLength(text: string, smooth: boolean): number {
|
|
55
|
+
function useRevealedLength(text: string, smooth: boolean, reducedMotion: boolean): number {
|
|
41
56
|
const [shown, setShown] = useState(text.length);
|
|
42
57
|
const shownRef = useRef(text.length);
|
|
43
58
|
const previous = useRef(text);
|
|
@@ -54,7 +69,7 @@ function useRevealedLength(text: string, smooth: boolean): number {
|
|
|
54
69
|
while (shared < start && text[shared] === before[shared]) shared++;
|
|
55
70
|
start = shared;
|
|
56
71
|
|
|
57
|
-
if (!smooth ||
|
|
72
|
+
if (!smooth || reducedMotion) {
|
|
58
73
|
set(text.length);
|
|
59
74
|
return undefined;
|
|
60
75
|
}
|
|
@@ -73,18 +88,20 @@ function useRevealedLength(text: string, smooth: boolean): number {
|
|
|
73
88
|
if (length < text.length) frame = requestAnimationFrame(tick);
|
|
74
89
|
});
|
|
75
90
|
return () => cancelAnimationFrame(frame);
|
|
76
|
-
}, [text, smooth]);
|
|
91
|
+
}, [text, smooth, reducedMotion]);
|
|
77
92
|
|
|
78
93
|
return Math.min(shown, text.length);
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
/** Props for {@link StreamingText}: the growing answer, whether it is still streaming, and whether chunks are revealed smoothly. */
|
|
81
97
|
export type StreamingTextProps = Omit<ComponentProps<"div">, "className" | "children"> & {
|
|
82
98
|
/** Everything received so far; pass the growing string as chunks arrive. */
|
|
83
99
|
text: string;
|
|
84
|
-
/** 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 */
|
|
85
101
|
streaming?: boolean;
|
|
86
|
-
/** 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 */
|
|
87
103
|
smooth?: boolean;
|
|
104
|
+
/** Extra classes merged onto the bubble, such as a maximum width. */
|
|
88
105
|
className?: string;
|
|
89
106
|
};
|
|
90
107
|
|
|
@@ -93,9 +110,20 @@ export type StreamingTextProps = Omit<ComponentProps<"div">, "className" | "chil
|
|
|
93
110
|
* right after the last character and wraps with it. The caret holds still while characters land and blinks
|
|
94
111
|
* while waiting for the next chunk; it goes away when the stream ends. Screen readers get the answer once,
|
|
95
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
|
+
* ```
|
|
96
123
|
*/
|
|
97
124
|
export function StreamingText({ text, streaming = true, smooth = true, className, ...props }: StreamingTextProps) {
|
|
98
|
-
const
|
|
125
|
+
const reducedMotion = useReducedMotion();
|
|
126
|
+
const shown = useRevealedLength(text, streaming && smooth, reducedMotion);
|
|
99
127
|
const typing = shown < text.length;
|
|
100
128
|
return (
|
|
101
129
|
<div
|
|
@@ -122,16 +150,31 @@ export function StreamingText({ text, streaming = true, smooth = true, className
|
|
|
122
150
|
);
|
|
123
151
|
}
|
|
124
152
|
|
|
153
|
+
/** Progress of a tool call: `running` shows a spinner, `done` a check, `failed` a warning and danger text. */
|
|
125
154
|
export type ToolStepState = "running" | "done" | "failed";
|
|
126
155
|
|
|
156
|
+
/** Props for {@link ToolStep}: the tool line, an optional detail line and the progress state. */
|
|
127
157
|
export type ToolStepProps = Omit<ComponentProps<"button">, "className" | "title"> & {
|
|
158
|
+
/** Progress state, which also picks the leading icon and the title colour. @default "running" */
|
|
128
159
|
state?: ToolStepState;
|
|
160
|
+
/** The tool call line, truncated to one line. */
|
|
129
161
|
title: ReactNode;
|
|
162
|
+
/** Optional second line, such as the target or a short result, truncated to one line. */
|
|
130
163
|
detail?: ReactNode;
|
|
164
|
+
/** Extra classes merged onto the row, for margins. */
|
|
131
165
|
className?: string;
|
|
132
166
|
};
|
|
133
167
|
|
|
134
|
-
/**
|
|
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
|
+
*/
|
|
135
178
|
export function ToolStep({ state = "running", title, detail, className, ...props }: ToolStepProps) {
|
|
136
179
|
return (
|
|
137
180
|
<button
|
|
@@ -156,16 +199,43 @@ export function ToolStep({ state = "running", title, detail, className, ...props
|
|
|
156
199
|
);
|
|
157
200
|
}
|
|
158
201
|
|
|
159
|
-
|
|
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
|
+
};
|
|
160
213
|
|
|
161
|
-
/**
|
|
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
|
+
*/
|
|
162
229
|
export function WorkLog({
|
|
163
230
|
summary,
|
|
164
231
|
steps,
|
|
165
232
|
className,
|
|
166
233
|
}: {
|
|
234
|
+
/** Summary line in the toggle row, such as "4 steps". */
|
|
167
235
|
summary: ReactNode;
|
|
236
|
+
/** Steps listed under the summary. */
|
|
168
237
|
steps: WorkLogStep[];
|
|
238
|
+
/** Extra classes merged onto the card, for margins or a width override. */
|
|
169
239
|
className?: string;
|
|
170
240
|
}) {
|
|
171
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
|
});
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { CaretDown } from "@phosphor-icons/react";
|
|
2
2
|
import type { ComponentProps, ReactNode } from "react";
|
|
3
3
|
|
|
4
|
+
import { useDevice } from "../hooks/use-device";
|
|
5
|
+
import { usePlatform } from "../hooks/use-platform";
|
|
4
6
|
import { cn } from "../lib/cn";
|
|
7
|
+
import { isModKey } from "../lib/platform";
|
|
5
8
|
import { Kbd } from "./kbd";
|
|
6
9
|
|
|
7
|
-
|
|
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
|
+
};
|
|
8
15
|
|
|
9
|
-
/**
|
|
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
|
+
*/
|
|
10
26
|
export function AgentPanel({
|
|
11
27
|
className,
|
|
12
28
|
filter,
|
|
@@ -29,13 +45,18 @@ export function AgentPanel({
|
|
|
29
45
|
);
|
|
30
46
|
}
|
|
31
47
|
|
|
48
|
+
/** Props for {@link ComposerChip}: a leading tile letter or icon, its fill, and the usual button props. */
|
|
32
49
|
export type ComposerChipProps = WithClass<ComponentProps<"button">> & {
|
|
33
50
|
/** 16px tile initial (bots) or a 14px icon (projects). */
|
|
34
51
|
leading?: ReactNode;
|
|
52
|
+
/** Tile fill, defaulting to the brand; pass a sky or neutral shade to match the bot it stands for. */
|
|
35
53
|
tileClassName?: string;
|
|
36
54
|
};
|
|
37
55
|
|
|
38
|
-
/**
|
|
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
|
+
*/
|
|
39
60
|
export function ComposerChip({ leading, tileClassName, className, children, ...props }: ComposerChipProps) {
|
|
40
61
|
return (
|
|
41
62
|
<button
|
|
@@ -64,18 +85,42 @@ export function ComposerChip({ leading, tileClassName, className, children, ...p
|
|
|
64
85
|
);
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
/** Props for {@link TaskComposer}: the draft text, picker chips, the start handler and the labels. */
|
|
67
89
|
export type TaskComposerProps = {
|
|
90
|
+
/** Placeholder of the textarea; also its accessible name. @default "Describe a task for a bot" */
|
|
68
91
|
placeholder?: string;
|
|
92
|
+
/** Controlled draft text; leave undefined and pass `onValueChange` to lift the text, or omit both for a free draft. */
|
|
69
93
|
value?: string;
|
|
94
|
+
/** Fires on every keystroke with the next text, so the caller can hold the draft. */
|
|
70
95
|
onValueChange?: (value: string) => void;
|
|
71
96
|
/** Picker chips, usually two `ComposerChip`s. */
|
|
72
97
|
pickers?: ReactNode;
|
|
98
|
+
/** Fires on the Start button and on Mod+Enter; the composer does not clear the draft itself. */
|
|
73
99
|
onStart?: () => void;
|
|
100
|
+
/** Label of the start button. @default "Start" */
|
|
74
101
|
startLabel?: string;
|
|
102
|
+
/** Extra classes merged onto the box, for margins or width overrides. */
|
|
75
103
|
className?: string;
|
|
76
104
|
};
|
|
77
105
|
|
|
78
|
-
/**
|
|
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
|
+
*/
|
|
79
124
|
export function TaskComposer({
|
|
80
125
|
placeholder = "Describe a task for a bot",
|
|
81
126
|
value,
|
|
@@ -85,6 +130,8 @@ export function TaskComposer({
|
|
|
85
130
|
startLabel = "Start",
|
|
86
131
|
className,
|
|
87
132
|
}: TaskComposerProps) {
|
|
133
|
+
const { os } = usePlatform();
|
|
134
|
+
const { keyboardHints } = useDevice();
|
|
88
135
|
return (
|
|
89
136
|
<div
|
|
90
137
|
className={cn(
|
|
@@ -97,7 +144,7 @@ export function TaskComposer({
|
|
|
97
144
|
value={value}
|
|
98
145
|
onChange={(event) => onValueChange?.(event.target.value)}
|
|
99
146
|
onKeyDown={(event) => {
|
|
100
|
-
if (event.key === "Enter" && event
|
|
147
|
+
if (event.key === "Enter" && isModKey(event, os)) onStart?.();
|
|
101
148
|
}}
|
|
102
149
|
placeholder={placeholder}
|
|
103
150
|
aria-label={placeholder}
|
|
@@ -106,8 +153,7 @@ export function TaskComposer({
|
|
|
106
153
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
107
154
|
{pickers && <span className="flex min-w-0 flex-wrap items-center gap-1.5">{pickers}</span>}
|
|
108
155
|
<span className="min-w-0 flex-1" />
|
|
109
|
-
<Kbd
|
|
110
|
-
<Kbd>↵</Kbd>
|
|
156
|
+
{keyboardHints && <Kbd keys="Mod+Enter" />}
|
|
111
157
|
<button
|
|
112
158
|
type="button"
|
|
113
159
|
onClick={onStart}
|