@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.
Files changed (97) hide show
  1. package/package.json +2 -2
  2. package/src/components/agent-activity.tsx +75 -7
  3. package/src/components/agent-lane.tsx +32 -3
  4. package/src/components/agent-lanes-timeline.tsx +85 -7
  5. package/src/components/agent-panel.tsx +46 -4
  6. package/src/components/agent-steering-panel.tsx +53 -3
  7. package/src/components/approval-card.tsx +34 -2
  8. package/src/components/avatar.tsx +20 -4
  9. package/src/components/badge.tsx +24 -3
  10. package/src/components/banner.tsx +19 -3
  11. package/src/components/bot-instructions-panel.tsx +86 -7
  12. package/src/components/bot-list-row.tsx +13 -1
  13. package/src/components/bot-memory-panel.tsx +58 -3
  14. package/src/components/bottom-sheet.tsx +61 -7
  15. package/src/components/breadcrumb-header.tsx +32 -2
  16. package/src/components/breadcrumb.tsx +31 -1
  17. package/src/components/button.tsx +46 -1
  18. package/src/components/calendar.tsx +76 -6
  19. package/src/components/card.tsx +30 -1
  20. package/src/components/chat-bubble.tsx +32 -3
  21. package/src/components/chat-controls.tsx +60 -7
  22. package/src/components/checkbox.tsx +22 -1
  23. package/src/components/chip.tsx +29 -2
  24. package/src/components/choice-card.tsx +25 -1
  25. package/src/components/code-panel.tsx +28 -3
  26. package/src/components/combobox.tsx +52 -2
  27. package/src/components/command-palette.tsx +65 -1
  28. package/src/components/composer.tsx +28 -1
  29. package/src/components/data-table.tsx +198 -16
  30. package/src/components/date-field.tsx +31 -2
  31. package/src/components/dialog.tsx +45 -3
  32. package/src/components/directory-card.tsx +23 -2
  33. package/src/components/document-extract-panel.tsx +83 -5
  34. package/src/components/empty-state.tsx +21 -2
  35. package/src/components/field.tsx +49 -3
  36. package/src/components/file-drop.tsx +39 -4
  37. package/src/components/filter-bar.tsx +38 -1
  38. package/src/components/flue-background.tsx +88 -4
  39. package/src/components/horon-icon.tsx +50 -3
  40. package/src/components/horon-wordmark.tsx +26 -1
  41. package/src/components/icon-button.tsx +38 -1
  42. package/src/components/input-group.tsx +57 -6
  43. package/src/components/input.tsx +48 -2
  44. package/src/components/integration-row.tsx +43 -2
  45. package/src/components/kbd.tsx +28 -2
  46. package/src/components/knowledge-base-panel.tsx +40 -2
  47. package/src/components/liquid-glass.tsx +16 -0
  48. package/src/components/list-detail.tsx +89 -7
  49. package/src/components/marketplace-card.tsx +40 -4
  50. package/src/components/mcp-server-row.tsx +49 -3
  51. package/src/components/menu.tsx +83 -3
  52. package/src/components/message-extras.tsx +50 -4
  53. package/src/components/modal.tsx +71 -4
  54. package/src/components/orchestrator-panel.tsx +48 -4
  55. package/src/components/page-header.tsx +26 -3
  56. package/src/components/plan-card.tsx +28 -1
  57. package/src/components/plan-step.tsx +41 -1
  58. package/src/components/plugin-row.tsx +61 -3
  59. package/src/components/progress.tsx +32 -2
  60. package/src/components/radio-group.tsx +55 -2
  61. package/src/components/research-run-panel.tsx +73 -3
  62. package/src/components/schedule.tsx +163 -12
  63. package/src/components/segmented-control.tsx +57 -2
  64. package/src/components/select.tsx +60 -0
  65. package/src/components/settings-list.tsx +66 -8
  66. package/src/components/sidebar.tsx +214 -23
  67. package/src/components/skeleton.tsx +61 -4
  68. package/src/components/slider.tsx +30 -0
  69. package/src/components/source-row.tsx +43 -4
  70. package/src/components/spinner.tsx +29 -2
  71. package/src/components/split-button.tsx +37 -3
  72. package/src/components/switch.tsx +31 -1
  73. package/src/components/tab-bar.tsx +47 -2
  74. package/src/components/tabs.tsx +59 -5
  75. package/src/components/task-row.tsx +49 -3
  76. package/src/components/textarea-field.tsx +36 -2
  77. package/src/components/textarea.tsx +35 -2
  78. package/src/components/toast.tsx +52 -3
  79. package/src/components/tool-call.tsx +87 -4
  80. package/src/components/tool-calls-header.tsx +32 -1
  81. package/src/components/tool-permissions.tsx +50 -2
  82. package/src/components/tooltip.tsx +33 -1
  83. package/src/components/top-bar.tsx +35 -2
  84. package/src/components/usage-meter.tsx +28 -2
  85. package/src/components/workspace-menu.tsx +46 -1
  86. package/src/hooks/use-active-indicator.ts +44 -1
  87. package/src/hooks/use-color-scheme.ts +24 -1
  88. package/src/hooks/use-copy-to-clipboard.ts +23 -0
  89. package/src/hooks/use-device.ts +29 -1
  90. package/src/hooks/use-hotkey.ts +21 -0
  91. package/src/hooks/use-media-query.ts +18 -0
  92. package/src/hooks/use-platform.ts +30 -1
  93. package/src/hooks/use-reduced-motion.ts +18 -1
  94. package/src/index.ts +31 -0
  95. package/src/lib/cn.ts +13 -1
  96. package/src/lib/cursor-ghost.ts +16 -6
  97. 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.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.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
- /** Figma `Thinking` (Agent activity 108:660): a 69x31 subtle pill with three 7px dots at 35/55/80% ink. */
9
- export function ThinkingIndicator({ label = "Thinking", className }: { label?: string; className?: string }) {
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
- /** Figma `Tool step` (108:660): a flat 12px-radius row with spinner, check or warning, and a caret. */
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
- export type WorkLogStep = { id: string; title: ReactNode; detail?: ReactNode; running?: boolean };
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
- /** Figma `Work log` (108:660): a flat subtle card with a wrench summary row and compact steps. */
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
- bot: { initial: string; name: ReactNode };
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?: { icon: ReactNode; label: ReactNode };
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
- /** Figma `Agent lane` (Agent orchestrator 113:953): one sub-agent's card in a fan-out, flat subtle with a red rim on failure. */
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
- export type LaneHandoff = { from: string; to: string };
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
- export type LaneStatus = { id: string; tone: "live" | "ink"; label: ReactNode };
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 & { "--i": number };
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<string, { task: LaneTask; lane: number }>();
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
- type WithClass<T> = Omit<T, "className"> & { className?: string };
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
- /** The solid panel that hosts the Figma Agents blocks (Agent tasks 112:797): surface fill, 24px radius, 16px padding. */
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
- /** A 30px 5% ink picker chip with a 12px caret, used for the bot and project in the task composer. */
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
- /** Figma task `Composer` (Agent tasks 112:797): a 4% ink box, 18px radius, with pickers, a Mod+Enter hint and Start. */
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 & { "--i": number };
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
- function Marker({ status }: { status: SteeringStepStatus }) {
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<ApprovalState, { dot: string; text: string; label: string }> = {
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",