@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
|
@@ -1,49 +1,79 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ArrowUp, Check, Lightning, Path, Pause, Play, X } from "@phosphor-icons/react";
|
|
2
2
|
import { type ComponentProps, type CSSProperties, type ReactNode, useState } 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
|
import { Spinner } from "./spinner";
|
|
7
10
|
|
|
11
|
+
/** Progress of one step: `done` fills the marker green, `active` shows a spinner, `todo` a dashed ring. */
|
|
8
12
|
export type SteeringStepStatus = "done" | "active" | "todo";
|
|
9
13
|
|
|
14
|
+
/** One step in the steering rail: its label, progress status and optional duration. */
|
|
10
15
|
export type SteeringStep = {
|
|
16
|
+
/** Stable key for the step. */
|
|
11
17
|
id: string;
|
|
18
|
+
/** Step text; secondary colour while the step is still `todo`. */
|
|
12
19
|
label: ReactNode;
|
|
20
|
+
/** Progress marker to draw. */
|
|
13
21
|
status: SteeringStepStatus;
|
|
14
22
|
/** Duration of a finished or running step, such as "1.4s". */
|
|
15
23
|
time?: ReactNode;
|
|
16
24
|
};
|
|
17
25
|
|
|
26
|
+
/** One steer note: the text and whether it has already been applied. */
|
|
18
27
|
export type SteeringNote = {
|
|
28
|
+
/** Stable key for the note. */
|
|
19
29
|
id: string;
|
|
30
|
+
/** The steer text, shown on one row and wrapping as needed. */
|
|
20
31
|
text: ReactNode;
|
|
21
|
-
/** 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 */
|
|
22
33
|
applied?: boolean;
|
|
23
34
|
};
|
|
24
35
|
|
|
36
|
+
/** Props for {@link AgentSteeringPanel}: the task header, the steps, queued notes and the steer handlers. */
|
|
25
37
|
export type AgentSteeringPanelProps = Omit<ComponentProps<"section">, "className" | "title"> & {
|
|
38
|
+
/** Task name in the header, truncated to one line. */
|
|
26
39
|
title: ReactNode;
|
|
40
|
+
/** Second header line, such as the bot or workspace, truncated to one line. */
|
|
27
41
|
subtitle?: ReactNode;
|
|
28
42
|
/** The status text beside the live dot, such as "Running · 2m 10s". */
|
|
29
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 */
|
|
30
45
|
paused?: boolean;
|
|
46
|
+
/** Fires when the pause/resume button is pressed, with the next paused value. */
|
|
31
47
|
onPausedChange?: (paused: boolean) => void;
|
|
48
|
+
/** Ordered steps of the running task; the connector under a done step turns live green. */
|
|
32
49
|
steps: SteeringStep[];
|
|
50
|
+
/** Notes queued for the next step, plus any already applied. @default [] */
|
|
33
51
|
notes?: SteeringNote[];
|
|
52
|
+
/** Fires with the note id when its "Now" button is pressed, to apply it immediately. */
|
|
34
53
|
onApplyNow?: (id: string) => void;
|
|
54
|
+
/** Fires with the note id after its pop-out animation ends; remove it from `notes` here. */
|
|
35
55
|
onRemoveNote?: (id: string) => void;
|
|
36
|
-
/** Called with the text typed in the steer box: Enter queues it for the next step,
|
|
56
|
+
/** Called with the text typed in the steer box: Enter queues it for the next step, Mod+Enter interrupts now. */
|
|
37
57
|
onSteer?: (text: string, interrupt: boolean) => void;
|
|
58
|
+
/** Placeholder of the steer input; also its accessible name. @default "Steer the bot…" */
|
|
38
59
|
placeholder?: string;
|
|
60
|
+
/** Extra classes merged onto the panel, for margins or a width override. */
|
|
39
61
|
className?: string;
|
|
40
62
|
};
|
|
41
63
|
|
|
42
64
|
const noNotes: SteeringNote[] = [];
|
|
43
|
-
type IndexStyle = CSSProperties & {
|
|
65
|
+
type IndexStyle = CSSProperties & {
|
|
66
|
+
/** Step index, consumed by the staggered `motion-rise` delay. */
|
|
67
|
+
"--i": number;
|
|
68
|
+
};
|
|
44
69
|
const indexStyle = (i: number): IndexStyle => ({ "--i": i });
|
|
45
70
|
|
|
46
|
-
|
|
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
|
+
}) {
|
|
47
77
|
if (status === "done") {
|
|
48
78
|
return (
|
|
49
79
|
<span className="flex size-4.5 animate-pop-in items-center justify-center rounded-full bg-status-live text-text-on-ink">
|
|
@@ -55,13 +85,17 @@ function Marker({ status }: { status: SteeringStepStatus }) {
|
|
|
55
85
|
return <span aria-label="Waiting" className="size-4.5 rounded-full border border-dashed border-border-dashed" />;
|
|
56
86
|
}
|
|
57
87
|
|
|
88
|
+
/** One queued note: the text, a "Now" action and a remove button, or a filled "Now" pill once applied. */
|
|
58
89
|
function SteeringNoteRow({
|
|
59
90
|
note,
|
|
60
91
|
onApplyNow,
|
|
61
92
|
onRemoveNote,
|
|
62
93
|
}: {
|
|
94
|
+
/** The note to render; `applied` swaps the actions for the filled pill. */
|
|
63
95
|
note: SteeringNote;
|
|
96
|
+
/** Called with the note id when "Now" is pressed. */
|
|
64
97
|
onApplyNow?: (id: string) => void;
|
|
98
|
+
/** Called with the note id when the pop-out animation finishes, so the parent can drop it. */
|
|
65
99
|
onRemoveNote?: (id: string) => void;
|
|
66
100
|
}) {
|
|
67
101
|
const [leaving, setLeaving] = useState(false);
|
|
@@ -118,6 +152,25 @@ function SteeringNoteRow({
|
|
|
118
152
|
* Paper `Agent steering` (Y51-1): a 24px-radius surface panel with the running task header, a step rail, the notes
|
|
119
153
|
* queued for the next step, and a steer box. The live dot pulses every 2s, a step marker swaps with a short pop,
|
|
120
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
|
+
* ```
|
|
121
174
|
*/
|
|
122
175
|
export function AgentSteeringPanel({
|
|
123
176
|
title,
|
|
@@ -135,6 +188,8 @@ export function AgentSteeringPanel({
|
|
|
135
188
|
...props
|
|
136
189
|
}: AgentSteeringPanelProps) {
|
|
137
190
|
const [draft, setDraft] = useState("");
|
|
191
|
+
const { os } = usePlatform();
|
|
192
|
+
const { keyboardHints } = useDevice();
|
|
138
193
|
const queued = notes.filter((note) => !note.applied);
|
|
139
194
|
const submit = (interrupt: boolean) => {
|
|
140
195
|
const text = draft.trim();
|
|
@@ -236,7 +291,7 @@ export function AgentSteeringPanel({
|
|
|
236
291
|
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
237
292
|
if (event.key === "Enter") {
|
|
238
293
|
event.preventDefault();
|
|
239
|
-
submit(event
|
|
294
|
+
submit(isModKey(event, os));
|
|
240
295
|
}
|
|
241
296
|
}}
|
|
242
297
|
placeholder={placeholder}
|
|
@@ -253,21 +308,18 @@ export function AgentSteeringPanel({
|
|
|
253
308
|
</button>
|
|
254
309
|
</div>
|
|
255
310
|
|
|
256
|
-
|
|
257
|
-
<
|
|
258
|
-
<
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
interrupt now
|
|
269
|
-
</span>
|
|
270
|
-
</div>
|
|
311
|
+
{keyboardHints && (
|
|
312
|
+
<div className="flex flex-wrap items-center gap-x-3.5 gap-y-1.5 text-caption text-text-secondary">
|
|
313
|
+
<span className="flex items-center gap-1.25">
|
|
314
|
+
<Kbd keys="Enter" />
|
|
315
|
+
next step
|
|
316
|
+
</span>
|
|
317
|
+
<span className="flex items-center gap-1.25">
|
|
318
|
+
<Kbd keys="Mod+Enter" />
|
|
319
|
+
interrupt now
|
|
320
|
+
</span>
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
271
323
|
</div>
|
|
272
324
|
</section>
|
|
273
325
|
);
|
|
@@ -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",
|
|
@@ -4,26 +4,32 @@ import type { ReactNode } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
|
|
7
|
+
/** Diameter of the avatar: `sm` 24px, `md` 32px, `lg` 40px, `xl` 56px. */
|
|
7
8
|
export type AvatarSize = "sm" | "md" | "lg" | "xl";
|
|
8
9
|
|
|
10
|
+
/** Props for {@link Avatar}: the avatar type and size, the image source, fallback content and the live dot. */
|
|
9
11
|
export type AvatarProps = {
|
|
10
|
-
/** `initials` (default), `bot` (robot on the brand tint) or `image`. */
|
|
12
|
+
/** `initials` (default), `bot` (robot on the brand tint) or `image`. @default "initials" */
|
|
11
13
|
type?: "initials" | "bot" | "image";
|
|
14
|
+
/** Diameter token; also scales the initials, the bot icon and the live dot. @default "md" */
|
|
12
15
|
size?: AvatarSize;
|
|
13
16
|
/** Image URL for `type="image"`; initials show while it loads or if it fails. */
|
|
14
17
|
src?: string;
|
|
18
|
+
/** Alternative text for `type="image"`; pass `""` when the name is already shown next to the avatar. @default "" */
|
|
15
19
|
alt?: string;
|
|
16
20
|
/** Initials, or any fallback content. */
|
|
17
21
|
children?: ReactNode;
|
|
18
|
-
/** Shows the live status dot at the bottom right. */
|
|
22
|
+
/** Shows the live status dot at the bottom right. @default false */
|
|
19
23
|
live?: boolean;
|
|
20
|
-
/** Initials colour: `neutral` (default) or `sky`, the Paper account profile avatar (sky-100 fill, sky-700 text). */
|
|
24
|
+
/** Initials colour: `neutral` (default) or `sky`, the Paper account profile avatar (sky-100 fill, sky-700 text). @default "neutral" */
|
|
21
25
|
tone?: "neutral" | "sky";
|
|
22
26
|
/**
|
|
23
27
|
* Border colour of the live dot, so it matches the surface behind it (glass panels, `bg-subtle`, brand tints).
|
|
24
28
|
* Defaults to `border-bg-surface`.
|
|
29
|
+
* @default "border-bg-surface"
|
|
25
30
|
*/
|
|
26
31
|
liveRingClassName?: string;
|
|
32
|
+
/** Extra classes merged onto the wrapper, for positioning or a custom size override. */
|
|
27
33
|
className?: string;
|
|
28
34
|
};
|
|
29
35
|
|
|
@@ -55,7 +61,17 @@ const typeClass = {
|
|
|
55
61
|
image: "bg-sky-200 text-text-secondary",
|
|
56
62
|
} as const;
|
|
57
63
|
|
|
58
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Figma `Avatar` (61:347): Initials, Bot and Image in four sizes, with an optional live dot. Built on Base UI's
|
|
66
|
+
* avatar primitive, so an image that fails to load falls back to the children instead of a broken icon.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* <Avatar type="image" size="lg" src={user.photoUrl} alt="" live>
|
|
71
|
+
* {initials}
|
|
72
|
+
* </Avatar>
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
59
75
|
export function Avatar({
|
|
60
76
|
type = "initials",
|
|
61
77
|
size = "md",
|
package/src/components/badge.tsx
CHANGED
|
@@ -3,16 +3,27 @@ import type { ComponentProps } from "react";
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import type { ControlSize } from "../lib/size";
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/** Tint of the pill and its dot; `neutral` is the default.
|
|
7
|
+
*
|
|
8
|
+
* - `neutral`: 6% ink pill with secondary text, the default.
|
|
9
|
+
* - `brand`: sky-tinted pill with brand text, for counts and states tied to the brand.
|
|
10
|
+
* - `danger`: red-tinted pill with danger text, for errors and destructive states.
|
|
11
|
+
* - `solid`: filled brand pill with on-primary text, for the strongest emphasis.
|
|
12
|
+
* - `surface`: the white (raised surface) pill with a 1px default border and primary text.
|
|
13
|
+
*/
|
|
7
14
|
export type BadgeTone = "neutral" | "brand" | "danger" | "solid" | "surface";
|
|
15
|
+
/** Size token shared with the form controls (`sm`, `md`, `lg`); the rendered heights are documented on {@link Badge}. */
|
|
8
16
|
export type BadgeSize = ControlSize;
|
|
9
17
|
|
|
18
|
+
/** Props for {@link Badge}: the tone, size, optional status dot and extra span props. */
|
|
10
19
|
export type BadgeProps = Omit<ComponentProps<"span">, "className"> & {
|
|
20
|
+
/** Colour treatment of the pill and its dot. @default "neutral" */
|
|
11
21
|
tone?: BadgeTone;
|
|
12
22
|
/** Leading status dot; 4px (sm), 6px (md) or 8px (lg). */
|
|
13
23
|
dot?: boolean;
|
|
14
|
-
/** `md` is the 22px Figma Badge; `sm` 20px and `lg` 26px. */
|
|
24
|
+
/** `md` is the 22px Figma Badge; `sm` 20px and `lg` 26px. @default "md" */
|
|
15
25
|
size?: BadgeSize;
|
|
26
|
+
/** Extra classes merged onto the pill, for margins or alignment. */
|
|
16
27
|
className?: string;
|
|
17
28
|
};
|
|
18
29
|
|
|
@@ -44,7 +55,17 @@ const dotClass: Record<BadgeTone, string> = {
|
|
|
44
55
|
surface: "bg-text-primary",
|
|
45
56
|
};
|
|
46
57
|
|
|
47
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Figma `Badge` (61:283): a 22px translucent pill with label-sm text. Renders a non-interactive `span`, so it can
|
|
60
|
+
* sit inside buttons, list rows and headings without changing their layout height.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```tsx
|
|
64
|
+
* <Badge tone="brand" dot>
|
|
65
|
+
* Running
|
|
66
|
+
* </Badge>
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
48
69
|
export function Badge({ tone = "neutral", dot, size = "md", className, children, ...props }: BadgeProps) {
|
|
49
70
|
return (
|
|
50
71
|
<span
|
|
@@ -5,18 +5,24 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import type { ControlSize } from "../lib/size";
|
|
6
6
|
import { LiquidGlass } from "./liquid-glass";
|
|
7
7
|
|
|
8
|
+
/** Severity of the strip: `info` tints the rim and icon brand, `danger` uses the danger rim and role="alert". */
|
|
8
9
|
export type BannerTone = "info" | "danger";
|
|
10
|
+
/** Size token shared with the form controls; the rendered heights are documented on {@link Banner}. */
|
|
9
11
|
export type BannerSize = ControlSize;
|
|
12
|
+
/** `solid` paints the raised surface, `glass` layers the glass material for use over imagery. */
|
|
10
13
|
export type BannerSurface = "solid" | "glass";
|
|
11
14
|
|
|
15
|
+
/** Props for {@link Banner}: the tone, size and surface of the strip plus its optional trailing action. */
|
|
12
16
|
export type BannerProps = Omit<ComponentProps<"div">, "className"> & {
|
|
17
|
+
/** Severity; also picks the icon and the ARIA role. @default "info" */
|
|
13
18
|
tone?: BannerTone;
|
|
14
19
|
/** Trailing underlined action, such as a link or button. */
|
|
15
20
|
action?: ReactNode;
|
|
16
|
-
/** Banner size: `sm` 32px minimum, `md` the Figma 48px strip (default), `lg` 56px; scales icon, text and padding. */
|
|
21
|
+
/** Banner size: `sm` 32px minimum, `md` the Figma 48px strip (default), `lg` 56px; scales icon, text and padding. @default "md" */
|
|
17
22
|
size?: BannerSize;
|
|
18
|
-
/** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. */
|
|
23
|
+
/** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. @default "solid" */
|
|
19
24
|
surface?: BannerSurface;
|
|
25
|
+
/** Extra classes merged onto the strip, for margins or width overrides. */
|
|
20
26
|
className?: string;
|
|
21
27
|
};
|
|
22
28
|
|
|
@@ -59,7 +65,17 @@ const solidEdgeClass: Record<BannerTone, string> = {
|
|
|
59
65
|
danger: "border-border-danger",
|
|
60
66
|
};
|
|
61
67
|
|
|
62
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Figma `Banner` (62:318): a strip with a tinted rim, 16px icon and an underlined action. The tone sets the ARIA
|
|
70
|
+
* role — `danger` renders `role="alert"` (announced immediately), `info` renders `role="status"`.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <Banner tone="danger" action={<a href="/settings">Review</a>}>
|
|
75
|
+
* Your API key expired.
|
|
76
|
+
* </Banner>
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
63
79
|
export function Banner({
|
|
64
80
|
tone = "info",
|
|
65
81
|
action,
|
|
@@ -2,23 +2,54 @@ import { CaretDown, Play } from "@phosphor-icons/react";
|
|
|
2
2
|
import { type ComponentProps, type KeyboardEvent, type ReactNode, useRef, useState } from "react";
|
|
3
3
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
-
import { useActiveIndicator } from "../
|
|
5
|
+
import { useActiveIndicator } from "../hooks/use-active-indicator";
|
|
6
|
+
import { useDevice } from "../hooks/use-device";
|
|
6
7
|
import { Kbd } from "./kbd";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/** Drops the base type's `className` so the panel can own it. */
|
|
10
|
+
type WithClass<T> = Omit<T, "className"> & {
|
|
11
|
+
/** Extra classes merged onto the root element. */
|
|
12
|
+
className?: string;
|
|
13
|
+
};
|
|
9
14
|
|
|
15
|
+
/** Props for {@link PanelPillTabs}: the tab items, the controlled or default selection and the change handler. */
|
|
10
16
|
export type PanelPillTabsProps = {
|
|
11
|
-
|
|
17
|
+
/** Tabs in display order; the first one is the default selection. */
|
|
18
|
+
items: {
|
|
19
|
+
/** Value handed to `onValueChange` and compared against `value`. */
|
|
20
|
+
value: string;
|
|
21
|
+
/** Tab label. */
|
|
22
|
+
label: ReactNode;
|
|
23
|
+
}[];
|
|
24
|
+
/** Controlled selected value; omit it to let the track keep its own selection. */
|
|
12
25
|
value?: string;
|
|
26
|
+
/** Selection used before the first click, falling back to the first item. */
|
|
13
27
|
defaultValue?: string;
|
|
28
|
+
/** Fires with the next value on click or arrow-key navigation. */
|
|
14
29
|
onValueChange?: (value: string) => void;
|
|
30
|
+
/** Accessible name of the tab list. */
|
|
15
31
|
"aria-label"?: string;
|
|
32
|
+
/** Extra classes merged onto the track, for width overrides. */
|
|
16
33
|
className?: string;
|
|
17
34
|
};
|
|
18
35
|
|
|
19
36
|
/**
|
|
20
|
-
* The 5% ink pill track shared by the Bot panels (Paper `YNQ-1`): 3px padding, 2px gap,
|
|
37
|
+
* The 5% ink pill track shared by the Bot panels (Paper `YNQ-1`): 3px padding, 2px gap, 30px options with
|
|
21
38
|
* 12px inset and a 10% ink selection that slides between options in 280ms.
|
|
39
|
+
*
|
|
40
|
+
* Works controlled (`value`) or uncontrolled (`defaultValue`); the arrow keys wrap around, move the selection
|
|
41
|
+
* and focus the newly selected tab, while only the selected tab stays in the tab order.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <PanelPillTabs
|
|
46
|
+
* aria-label="Instructions view"
|
|
47
|
+
* items={[
|
|
48
|
+
* { value: "instructions", label: "Instructions" },
|
|
49
|
+
* { value: "example", label: "Example" },
|
|
50
|
+
* ]}
|
|
51
|
+
* />
|
|
52
|
+
* ```
|
|
22
53
|
*/
|
|
23
54
|
export function PanelPillTabs({ items, value, defaultValue, onValueChange, className, ...props }: PanelPillTabsProps) {
|
|
24
55
|
const [inner, setInner] = useState(defaultValue ?? items[0]?.value ?? "");
|
|
@@ -81,6 +112,7 @@ export function PanelPillTabs({ items, value, defaultValue, onValueChange, class
|
|
|
81
112
|
);
|
|
82
113
|
}
|
|
83
114
|
|
|
115
|
+
/** Chip tint: `sky` the default light blue, `brand` the deeper sky-700, `neutral` grey. */
|
|
84
116
|
export type InstructionVariableTone = "sky" | "brand" | "neutral";
|
|
85
117
|
|
|
86
118
|
const variableToneClass: Record<InstructionVariableTone, string> = {
|
|
@@ -96,26 +128,53 @@ const variableDotClass: Record<InstructionVariableTone, string> = {
|
|
|
96
128
|
};
|
|
97
129
|
|
|
98
130
|
/** One piece of an instruction line: plain text, or a `{{variable}}` chip. */
|
|
99
|
-
export type InstructionPart =
|
|
131
|
+
export type InstructionPart =
|
|
132
|
+
| string
|
|
133
|
+
| {
|
|
134
|
+
/** Variable name, rendered inside the chip without the braces. */
|
|
135
|
+
variable: string;
|
|
136
|
+
/** Chip tint. @default "sky" */
|
|
137
|
+
tone?: InstructionVariableTone;
|
|
138
|
+
};
|
|
100
139
|
|
|
101
|
-
|
|
140
|
+
/** A test value for one `{{variable}}` in the prompt, shown under the instructions. */
|
|
141
|
+
export type InstructionVariable = {
|
|
142
|
+
/** Variable name shown above the test value. */
|
|
143
|
+
name: string;
|
|
144
|
+
/** Dot tint matching the chip in the prompt. @default "sky" */
|
|
145
|
+
tone?: InstructionVariableTone;
|
|
146
|
+
/** Test value used when the instruction is run. */
|
|
147
|
+
value: string;
|
|
148
|
+
};
|
|
102
149
|
|
|
150
|
+
/** Props for {@link BotInstructionsPanel}: the bot, the prompt lines, the optional example, variables and scenarios, and the run handler. */
|
|
103
151
|
export type BotInstructionsPanelProps = WithClass<Omit<ComponentProps<"section">, "title">> & {
|
|
152
|
+
/** Panel heading. @default "Instructions" */
|
|
104
153
|
title?: ReactNode;
|
|
105
154
|
/** Version label in the header pill, for example `v4`. */
|
|
106
155
|
version?: string;
|
|
107
156
|
/** Status next to the version, rendered with a live dot. */
|
|
108
157
|
versionStatus?: string;
|
|
109
|
-
|
|
110
|
-
|
|
158
|
+
/** Bot whose prompt is shown; the initial fills the 18px tile. */
|
|
159
|
+
bot: {
|
|
160
|
+
/** One-character tile label. */
|
|
161
|
+
initial: string;
|
|
162
|
+
/** Bot name next to the tile. */
|
|
163
|
+
name: string;
|
|
164
|
+
};
|
|
165
|
+
/** The prompt text, one array of parts per line, rendered read-only. */
|
|
111
166
|
instructions: InstructionPart[][];
|
|
112
167
|
/** Shown under the Example tab instead of the prompt. */
|
|
113
168
|
example?: ReactNode;
|
|
169
|
+
/** Prompt length shown at the right of the tab row; hidden when undefined. */
|
|
114
170
|
tokenCount?: number;
|
|
115
171
|
/** Test values for each variable in the prompt. */
|
|
116
172
|
variables?: InstructionVariable[];
|
|
173
|
+
/** Test scenarios offered as pills under the prompt. */
|
|
117
174
|
scenarios?: string[];
|
|
175
|
+
/** Scenario selected on first render, falling back to the first scenario. */
|
|
118
176
|
defaultScenario?: string;
|
|
177
|
+
/** Fires with the selected scenario (or undefined) when Run is pressed; the panel does not run anything itself. */
|
|
119
178
|
onRun?: (scenario: string | undefined) => void;
|
|
120
179
|
/** The last run's output, or a hint until there is one. */
|
|
121
180
|
output?: ReactNode;
|
|
@@ -129,13 +188,17 @@ function partKey(part: InstructionPart) {
|
|
|
129
188
|
return typeof part === "string" ? part : part.variable;
|
|
130
189
|
}
|
|
131
190
|
|
|
191
|
+
/** Inline rounded chip that stands in for a `{{variable}}` inside a prompt line. */
|
|
132
192
|
function VariableChip({
|
|
133
193
|
tone = "sky",
|
|
134
194
|
className,
|
|
135
195
|
children,
|
|
136
196
|
}: {
|
|
197
|
+
/** Chip tint. @default "sky" */
|
|
137
198
|
tone?: InstructionVariableTone;
|
|
199
|
+
/** Extra classes merged onto the chip. */
|
|
138
200
|
className?: string;
|
|
201
|
+
/** Variable name shown without braces. */
|
|
139
202
|
children: ReactNode;
|
|
140
203
|
}) {
|
|
141
204
|
return (
|
|
@@ -152,6 +215,23 @@ function Divider() {
|
|
|
152
215
|
/**
|
|
153
216
|
* Bot instructions panel (Paper `YN3-1` / `GOZ-0`): a 24px-radius surface with the bot header, an
|
|
154
217
|
* Instructions / Example tab pair over the prompt text with `{{variable}}` chips, and a test-run block.
|
|
218
|
+
*
|
|
219
|
+
* The tab and the selected scenario are local state; the panel only reports a run through `onRun`. Output is
|
|
220
|
+
* re-keyed per run so it animates in every time. A caret blinks at the end of the prompt while the prompt area
|
|
221
|
+
* has focus. The Run button carries a `Mod+Enter` chip — ⌘↵ on macOS and iOS, Ctrl+Enter elsewhere — which is
|
|
222
|
+
* hidden on touch devices that show no keyboard hints.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```tsx
|
|
226
|
+
* <BotInstructionsPanel
|
|
227
|
+
* version="v4"
|
|
228
|
+
* bot={{ initial: "R", name: "Researcher" }}
|
|
229
|
+
* instructions={[["Summarise ", { variable: "sheet" }, " for the team."]]}
|
|
230
|
+
* variables={[{ name: "sheet", value: "Q3 metrics" }]}
|
|
231
|
+
* scenarios={["Weekly digest", "Board update"]}
|
|
232
|
+
* onRun={(scenario) => run(scenario)}
|
|
233
|
+
* />
|
|
234
|
+
* ```
|
|
155
235
|
*/
|
|
156
236
|
export function BotInstructionsPanel({
|
|
157
237
|
title = "Instructions",
|
|
@@ -174,6 +254,7 @@ export function BotInstructionsPanel({
|
|
|
174
254
|
const [tab, setTab] = useState("instructions");
|
|
175
255
|
const [scenario, setScenario] = useState(defaultScenario ?? scenarios?.[0]);
|
|
176
256
|
const [runCount, setRunCount] = useState(0);
|
|
257
|
+
const { keyboardHints } = useDevice();
|
|
177
258
|
|
|
178
259
|
return (
|
|
179
260
|
<section
|
|
@@ -342,7 +423,9 @@ export function BotInstructionsPanel({
|
|
|
342
423
|
>
|
|
343
424
|
<Play aria-hidden className="size-3.5" />
|
|
344
425
|
Run
|
|
345
|
-
|
|
426
|
+
{keyboardHints && (
|
|
427
|
+
<Kbd keys="Mod+Enter" className="bg-bg-surface-6 text-text-inverse/80" />
|
|
428
|
+
)}
|
|
346
429
|
</button>
|
|
347
430
|
</div>
|
|
348
431
|
</div>
|
|
@@ -3,7 +3,9 @@ import type { ComponentProps, ReactNode } from "react";
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { Avatar } from "./avatar";
|
|
5
5
|
|
|
6
|
+
/** Props for {@link BotListRow}: the bot name, last-activity time and preview, the live and selected states. */
|
|
6
7
|
export type BotListRowProps = Omit<ComponentProps<"button">, "className"> & {
|
|
8
|
+
/** Bot display name, truncated with an ellipsis when it does not fit. */
|
|
7
9
|
name: ReactNode;
|
|
8
10
|
/** Last activity time, such as "8:41 AM". */
|
|
9
11
|
time?: ReactNode;
|
|
@@ -11,11 +13,21 @@ export type BotListRowProps = Omit<ComponentProps<"button">, "className"> & {
|
|
|
11
13
|
preview?: ReactNode;
|
|
12
14
|
/** Running bots show the live dot on the avatar. */
|
|
13
15
|
live?: boolean;
|
|
16
|
+
/** The row for the bot currently open in the panel; paints the 10% ink selected fill and sets `aria-pressed`. */
|
|
14
17
|
selected?: boolean;
|
|
18
|
+
/** Extra classes merged onto the button, for margins or width overrides. */
|
|
15
19
|
className?: string;
|
|
16
20
|
};
|
|
17
21
|
|
|
18
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Figma `Bot list row` (64:456): a flat 12px-padded row; hover 5% ink, selected 10% ink. The whole row is the
|
|
24
|
+
* button, so the accessible name comes from its text content (name, time and preview).
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <BotListRow name="Researcher" time="8:41 AM" preview="Pulled the Q3 numbers" live selected />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
19
31
|
export function BotListRow({ name, time, preview, live, selected, className, ...props }: BotListRowProps) {
|
|
20
32
|
return (
|
|
21
33
|
<button
|