@horon-ai/react 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -2
- package/package.json +50 -5
- package/src/assets/sidebar-photo.jpg +0 -0
- package/src/components/agent-activity.tsx +204 -0
- package/src/components/agent-lane.tsx +80 -0
- package/src/components/agent-lanes-timeline.tsx +312 -0
- package/src/components/agent-panel.tsx +125 -0
- package/src/components/agent-steering-panel.tsx +276 -0
- package/src/components/approval-card.tsx +89 -0
- package/src/components/avatar.tsx +104 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/banner.tsx +103 -0
- package/src/components/bot-instructions-panel.tsx +369 -0
- package/src/components/bot-list-row.tsx +42 -0
- package/src/components/bot-memory-panel.tsx +229 -0
- package/src/components/bottom-sheet.tsx +140 -0
- package/src/components/breadcrumb-header.tsx +87 -0
- package/src/components/breadcrumb.tsx +86 -0
- package/src/components/button.tsx +146 -0
- package/src/components/calendar.tsx +410 -0
- package/src/components/card.tsx +79 -0
- package/src/components/chat-bubble.tsx +36 -0
- package/src/components/chat-controls.tsx +118 -0
- package/src/components/checkbox.tsx +73 -0
- package/src/components/chip.tsx +81 -0
- package/src/components/choice-card.tsx +55 -0
- package/src/components/code-panel.tsx +27 -0
- package/src/components/combobox.tsx +312 -0
- package/src/components/command-palette.tsx +206 -0
- package/src/components/composer.tsx +124 -0
- package/src/components/data-table.tsx +429 -0
- package/src/components/date-field.tsx +100 -0
- package/src/components/dialog.tsx +93 -0
- package/src/components/directory-card.tsx +59 -0
- package/src/components/document-extract-panel.tsx +398 -0
- package/src/components/empty-state.tsx +29 -0
- package/src/components/field.tsx +78 -0
- package/src/components/file-drop.tsx +89 -0
- package/src/components/filter-bar.tsx +83 -0
- package/src/components/flue-background.tsx +388 -0
- package/src/components/horon-icon.tsx +280 -0
- package/src/components/horon-wordmark.tsx +26 -0
- package/src/components/icon-button.tsx +61 -0
- package/src/components/input-group.tsx +112 -0
- package/src/components/input.tsx +72 -0
- package/src/components/integration-row.tsx +103 -0
- package/src/components/kbd.tsx +42 -0
- package/src/components/knowledge-base-panel.tsx +185 -0
- package/src/components/liquid-glass.tsx +24 -0
- package/src/components/list-detail.tsx +275 -0
- package/src/components/marketplace-card.tsx +97 -0
- package/src/components/mcp-server-row.tsx +122 -0
- package/src/components/menu.tsx +165 -0
- package/src/components/message-extras.tsx +146 -0
- package/src/components/modal.tsx +108 -0
- package/src/components/orchestrator-panel.tsx +175 -0
- package/src/components/page-header.tsx +48 -0
- package/src/components/plan-card.tsx +70 -0
- package/src/components/plan-step.tsx +111 -0
- package/src/components/plugin-row.tsx +128 -0
- package/src/components/progress.tsx +21 -0
- package/src/components/radio-group.tsx +84 -0
- package/src/components/research-run-panel.tsx +232 -0
- package/src/components/schedule.tsx +332 -0
- package/src/components/segmented-control.tsx +136 -0
- package/src/components/select.tsx +154 -0
- package/src/components/settings-list.tsx +99 -0
- package/src/components/sidebar.tsx +542 -0
- package/src/components/skeleton.tsx +75 -0
- package/src/components/slider.tsx +29 -0
- package/src/components/source-row.tsx +112 -0
- package/src/components/spinner.tsx +29 -0
- package/src/components/split-button.tsx +72 -0
- package/src/components/switch.tsx +79 -0
- package/src/components/tab-bar.tsx +90 -0
- package/src/components/tabs.tsx +162 -0
- package/src/components/task-row.tsx +159 -0
- package/src/components/textarea-field.tsx +127 -0
- package/src/components/textarea.tsx +47 -0
- package/src/components/toast.tsx +128 -0
- package/src/components/tool-call.tsx +215 -0
- package/src/components/tool-calls-header.tsx +65 -0
- package/src/components/tool-permissions.tsx +105 -0
- package/src/components/tooltip.tsx +33 -0
- package/src/components/top-bar.tsx +65 -0
- package/src/components/usage-meter.tsx +47 -0
- package/src/components/workspace-menu.tsx +77 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/use-active-indicator.ts +60 -0
- package/src/hooks/use-color-scheme.ts +27 -0
- package/src/hooks/use-copy-to-clipboard.ts +32 -0
- package/src/hooks/use-device.ts +41 -0
- package/src/hooks/use-hotkey.ts +35 -0
- package/src/hooks/use-media-query.ts +25 -0
- package/src/hooks/use-platform.ts +39 -0
- package/src/hooks/use-reduced-motion.ts +8 -0
- package/src/index.ts +99 -0
- package/src/lib/cn.ts +25 -0
- package/src/lib/cursor-ghost.ts +244 -0
- package/src/lib/platform.ts +169 -0
- package/src/lib/size.ts +10 -0
- package/src/styles/globals.css +564 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { CaretDown, Pulse, Warning } from "@phosphor-icons/react";
|
|
2
|
+
import { type ComponentProps, type CSSProperties, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { Spinner } from "./spinner";
|
|
6
|
+
|
|
7
|
+
export type LaneTaskTone = "done" | "running" | "failed" | "waiting";
|
|
8
|
+
|
|
9
|
+
export type LaneTask = {
|
|
10
|
+
id: string;
|
|
11
|
+
label: ReactNode;
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
tone: LaneTaskTone;
|
|
14
|
+
/** 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
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
/** A pill inside the bar, such as "Needs you". */
|
|
18
|
+
badge?: ReactNode;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type LaneBotTone = "sky" | "steel" | "neutral" | "ink";
|
|
22
|
+
|
|
23
|
+
export type LaneBot = {
|
|
24
|
+
id: string;
|
|
25
|
+
name: ReactNode;
|
|
26
|
+
tools?: ReactNode;
|
|
27
|
+
initial: string;
|
|
28
|
+
tone: LaneBotTone;
|
|
29
|
+
/** A small tag next to the name: "Idle", "Needs you". */
|
|
30
|
+
badge?: ReactNode;
|
|
31
|
+
badgeTone?: "subtle" | "ink";
|
|
32
|
+
tasks: LaneTask[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type LaneHandoff = { from: string; to: string };
|
|
36
|
+
|
|
37
|
+
export type LaneStatus = { id: string; tone: "live" | "ink"; label: ReactNode };
|
|
38
|
+
|
|
39
|
+
export type AgentLanesTimelineApproval = {
|
|
40
|
+
message: ReactNode;
|
|
41
|
+
onLater?: () => void;
|
|
42
|
+
onApprove?: () => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type AgentLanesTimelineProps = Omit<ComponentProps<"section">, "className" | "title"> & {
|
|
46
|
+
title?: ReactNode;
|
|
47
|
+
statuses?: LaneStatus[];
|
|
48
|
+
/** The range pill at the header's right edge. */
|
|
49
|
+
range?: ReactNode;
|
|
50
|
+
bots: LaneBot[];
|
|
51
|
+
handoffs?: LaneHandoff[];
|
|
52
|
+
/** Window length in minutes; the tick labels sit every five. */
|
|
53
|
+
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. */
|
|
55
|
+
cursor?: number;
|
|
56
|
+
approval?: AgentLanesTimelineApproval;
|
|
57
|
+
/** Makes the range pill actionable; without it the range renders as a plain label. */
|
|
58
|
+
onRangeClick?: () => void;
|
|
59
|
+
className?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const avatarTone: Record<LaneBotTone, string> = {
|
|
63
|
+
sky: "bg-sky-500",
|
|
64
|
+
steel: "bg-sky-700",
|
|
65
|
+
neutral: "bg-neutral-500",
|
|
66
|
+
ink: "bg-interactive-primary text-text-inverse",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const barTone: Record<LaneTaskTone, string> = {
|
|
70
|
+
done: "bg-overlay-ink-6 text-text-secondary",
|
|
71
|
+
running: "bg-status-live/20 text-text-primary",
|
|
72
|
+
failed: "bg-red-500/14 text-text-danger",
|
|
73
|
+
waiting: "border border-interactive-primary bg-bg-surface-90 text-text-primary",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const noStatuses: LaneStatus[] = [];
|
|
77
|
+
const noHandoffs: LaneHandoff[] = [];
|
|
78
|
+
type RowStyle = CSSProperties & { "--i": number };
|
|
79
|
+
const rowStyle = (lane: number): RowStyle => ({ top: TOP + lane * ROW, height: ROW, "--i": lane });
|
|
80
|
+
|
|
81
|
+
const ROW = 64;
|
|
82
|
+
const TOP = 40;
|
|
83
|
+
const LABEL_WIDTH = 230;
|
|
84
|
+
const RIGHT_GUTTER = 18;
|
|
85
|
+
|
|
86
|
+
const TRACK = `(100% - ${LABEL_WIDTH + RIGHT_GUTTER}px)`;
|
|
87
|
+
const trackX = (fraction: number) => `calc(${LABEL_WIDTH}px + ${TRACK} * ${fraction})`;
|
|
88
|
+
const trackW = (fraction: number) => `calc(${TRACK} * ${fraction})`;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Paper `Agent lanes` (YCO-1): a 24px-radius surface panel with one lane per bot on a shared time axis, a current
|
|
92
|
+
* line, hand-off curves between bots and an approval strip. The current line glides in 280ms with the emphasized
|
|
93
|
+
* easing, rows tint on hover in 150ms, and running bars carry a spinner.
|
|
94
|
+
*/
|
|
95
|
+
export function AgentLanesTimeline({
|
|
96
|
+
title = "Bots",
|
|
97
|
+
statuses = noStatuses,
|
|
98
|
+
range = "Last 30m",
|
|
99
|
+
bots,
|
|
100
|
+
handoffs = noHandoffs,
|
|
101
|
+
windowMinutes = 30,
|
|
102
|
+
cursor = 0,
|
|
103
|
+
approval,
|
|
104
|
+
onRangeClick,
|
|
105
|
+
className,
|
|
106
|
+
...props
|
|
107
|
+
}: AgentLanesTimelineProps) {
|
|
108
|
+
const ticks = Array.from({ length: windowMinutes / 5 - 1 }, (_, i) => -(windowMinutes - 5 * (i + 1)));
|
|
109
|
+
const taskAt = new Map<string, { task: LaneTask; lane: number }>();
|
|
110
|
+
bots.forEach((bot, lane) => {
|
|
111
|
+
for (const task of bot.tasks) taskAt.set(task.id, { task, lane });
|
|
112
|
+
});
|
|
113
|
+
const at = (minutes: number) => (windowMinutes + minutes) / windowMinutes;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<section
|
|
117
|
+
className={cn(
|
|
118
|
+
"flex w-full max-w-252.5 min-w-0 flex-col overflow-clip rounded-[24px] border border-border-default bg-bg-surface",
|
|
119
|
+
className,
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
>
|
|
123
|
+
<div className="flex flex-wrap items-center gap-x-2.5 gap-y-2 px-5 pt-4 pb-3.5">
|
|
124
|
+
<Pulse aria-hidden className="size-4.5 shrink-0 text-text-primary" />
|
|
125
|
+
<h3 className="min-w-0 text-label-lg text-text-primary">{title}</h3>
|
|
126
|
+
{statuses.map((status) => (
|
|
127
|
+
<span key={status.id} className="flex items-center gap-1.5 text-body-sm text-text-secondary">
|
|
128
|
+
<span
|
|
129
|
+
aria-hidden
|
|
130
|
+
className={cn(
|
|
131
|
+
"size-1.75 rounded-full",
|
|
132
|
+
status.tone === "live" ? "bg-status-live" : "bg-interactive-primary",
|
|
133
|
+
)}
|
|
134
|
+
/>
|
|
135
|
+
{status.label}
|
|
136
|
+
</span>
|
|
137
|
+
))}
|
|
138
|
+
<span className="flex-1" />
|
|
139
|
+
{onRangeClick ? (
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={onRangeClick}
|
|
143
|
+
className="flex h-7.5 motion-press cursor-pointer items-center gap-1 rounded-full bg-overlay-ink-6 px-3 text-label-sm text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
144
|
+
>
|
|
145
|
+
{range}
|
|
146
|
+
<CaretDown aria-hidden className="size-3 text-text-secondary" />
|
|
147
|
+
</button>
|
|
148
|
+
) : (
|
|
149
|
+
<span className="flex h-7.5 items-center gap-1 rounded-full bg-overlay-ink-6 px-3 text-label-sm text-text-primary">
|
|
150
|
+
{range}
|
|
151
|
+
<CaretDown aria-hidden className="size-3 text-text-secondary" />
|
|
152
|
+
</span>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
<div className="h-px bg-overlay-ink-8" />
|
|
156
|
+
|
|
157
|
+
<div className="overflow-x-auto">
|
|
158
|
+
<div className="relative min-w-175" style={{ height: TOP + Math.max(2, bots.length) * ROW }}>
|
|
159
|
+
{ticks.map((minutes) => (
|
|
160
|
+
<span key={minutes}>
|
|
161
|
+
<span
|
|
162
|
+
aria-hidden
|
|
163
|
+
className="absolute top-9 bottom-0 w-px bg-overlay-ink-5"
|
|
164
|
+
style={{ left: trackX(at(minutes)) }}
|
|
165
|
+
/>
|
|
166
|
+
<span
|
|
167
|
+
className="absolute top-2.5 -translate-x-1/2 text-caption whitespace-nowrap text-text-secondary"
|
|
168
|
+
style={{ left: trackX(at(minutes)) }}
|
|
169
|
+
>
|
|
170
|
+
{minutes}m
|
|
171
|
+
</span>
|
|
172
|
+
</span>
|
|
173
|
+
))}
|
|
174
|
+
|
|
175
|
+
{bots.map((bot, lane) => (
|
|
176
|
+
<div
|
|
177
|
+
key={bot.id}
|
|
178
|
+
style={rowStyle(lane)}
|
|
179
|
+
className={cn(
|
|
180
|
+
"absolute inset-x-0 flex motion-rise items-center gap-3 pl-5 motion-colors hover:bg-overlay-ink-3",
|
|
181
|
+
lane < bots.length - 1 && "border-b border-overlay-ink-6",
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
<span
|
|
185
|
+
className={cn(
|
|
186
|
+
"flex size-8.5 shrink-0 items-center justify-center rounded-[10px] text-label-md text-text-on-ink",
|
|
187
|
+
avatarTone[bot.tone],
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
{bot.initial}
|
|
191
|
+
</span>
|
|
192
|
+
<span className="flex flex-col">
|
|
193
|
+
<span className="flex items-center gap-1.5 text-label-md text-text-primary">
|
|
194
|
+
{bot.name}
|
|
195
|
+
{bot.badge && (
|
|
196
|
+
<span
|
|
197
|
+
className={cn(
|
|
198
|
+
"rounded-md px-1.5 py-px text-label-sm",
|
|
199
|
+
bot.badgeTone === "ink"
|
|
200
|
+
? "bg-interactive-primary text-text-inverse"
|
|
201
|
+
: "bg-overlay-ink-6 text-text-secondary",
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
{bot.badge}
|
|
205
|
+
</span>
|
|
206
|
+
)}
|
|
207
|
+
</span>
|
|
208
|
+
{bot.tools && <span className="text-caption text-text-secondary">{bot.tools}</span>}
|
|
209
|
+
</span>
|
|
210
|
+
</div>
|
|
211
|
+
))}
|
|
212
|
+
|
|
213
|
+
{handoffs.map(({ from, to }) => {
|
|
214
|
+
const a = taskAt.get(from);
|
|
215
|
+
const b = taskAt.get(to);
|
|
216
|
+
if (!a || !b) return null;
|
|
217
|
+
const x1 = at(a.task.end);
|
|
218
|
+
const x2 = at(b.task.start);
|
|
219
|
+
const y1 = TOP + a.lane * ROW + 32;
|
|
220
|
+
const y2 = TOP + b.lane * ROW + 32;
|
|
221
|
+
return (
|
|
222
|
+
<span
|
|
223
|
+
key={`${from}-${to}`}
|
|
224
|
+
aria-hidden
|
|
225
|
+
className="absolute"
|
|
226
|
+
style={{
|
|
227
|
+
left: trackX(x1),
|
|
228
|
+
width: trackW(x2 - x1),
|
|
229
|
+
top: y1,
|
|
230
|
+
height: y2 - y1,
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="size-full overflow-visible">
|
|
234
|
+
<path
|
|
235
|
+
d="M0 0 C60 0 40 100 100 100"
|
|
236
|
+
fill="none"
|
|
237
|
+
strokeDasharray="3 3"
|
|
238
|
+
vectorEffect="non-scaling-stroke"
|
|
239
|
+
className="stroke-status-live"
|
|
240
|
+
strokeWidth="1.5"
|
|
241
|
+
/>
|
|
242
|
+
</svg>
|
|
243
|
+
</span>
|
|
244
|
+
);
|
|
245
|
+
})}
|
|
246
|
+
|
|
247
|
+
{bots.flatMap((bot, lane) =>
|
|
248
|
+
bot.tasks.map((task) => (
|
|
249
|
+
<div
|
|
250
|
+
key={task.id}
|
|
251
|
+
className={cn(
|
|
252
|
+
"absolute flex h-9 motion-rise items-center gap-1.5 overflow-clip rounded-[10px] px-2.5 text-label-sm transition-[left,width,background-color,color,border-color] duration-280 ease-emphasized [&>svg]:size-3.5",
|
|
253
|
+
barTone[task.tone],
|
|
254
|
+
)}
|
|
255
|
+
style={{
|
|
256
|
+
top: TOP + lane * ROW + 14,
|
|
257
|
+
left: trackX(at(task.start)),
|
|
258
|
+
width: trackW((task.end - task.start) / windowMinutes),
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
{task.icon}
|
|
262
|
+
<span className="min-w-0 flex-1 truncate">{task.label}</span>
|
|
263
|
+
{task.badge && (
|
|
264
|
+
<span className="flex items-center gap-1 rounded-full bg-interactive-primary py-0.5 pr-2 pl-1.5 text-label-sm text-text-inverse [&>svg]:size-3">
|
|
265
|
+
{task.badge}
|
|
266
|
+
</span>
|
|
267
|
+
)}
|
|
268
|
+
{task.tone === "running" && <Spinner size={12} label="Running" />}
|
|
269
|
+
</div>
|
|
270
|
+
)),
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
<span
|
|
274
|
+
className="absolute top-2.5 -translate-x-full transition-[left] duration-280 ease-emphasized"
|
|
275
|
+
style={{ left: trackX(at(-cursor)) }}
|
|
276
|
+
>
|
|
277
|
+
<span className="rounded-md bg-status-live/20 px-1.5 py-px text-caption text-text-secondary">now</span>
|
|
278
|
+
</span>
|
|
279
|
+
<span
|
|
280
|
+
aria-hidden
|
|
281
|
+
className="absolute top-9 bottom-0 w-[1.5px] bg-status-live/60 transition-[left] duration-280 ease-emphasized"
|
|
282
|
+
style={{ left: trackX(at(-cursor)) }}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{approval && (
|
|
288
|
+
<>
|
|
289
|
+
<div className="h-px bg-overlay-ink-8" />
|
|
290
|
+
<div className="flex flex-wrap items-center gap-3 px-5 py-3">
|
|
291
|
+
<Warning aria-hidden className="size-3.5 shrink-0 text-text-primary" />
|
|
292
|
+
<p className="min-w-0 flex-1 text-body-sm text-text-primary">{approval.message}</p>
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
onClick={approval.onLater}
|
|
296
|
+
className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center rounded-full bg-overlay-ink-6 px-3 text-label-sm text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
297
|
+
>
|
|
298
|
+
Later
|
|
299
|
+
</button>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={approval.onApprove}
|
|
303
|
+
className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center rounded-full bg-interactive-primary px-3 text-label-sm text-text-inverse focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand"
|
|
304
|
+
>
|
|
305
|
+
Approve
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
</section>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { CaretDown } from "@phosphor-icons/react";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { useDevice } from "../hooks/use-device";
|
|
5
|
+
import { usePlatform } from "../hooks/use-platform";
|
|
6
|
+
import { cn } from "../lib/cn";
|
|
7
|
+
import { isModKey } from "../lib/platform";
|
|
8
|
+
import { Kbd } from "./kbd";
|
|
9
|
+
|
|
10
|
+
type WithClass<T> = Omit<T, "className"> & { className?: string };
|
|
11
|
+
|
|
12
|
+
/** The solid panel that hosts the Figma Agents blocks (Agent tasks 112:797): surface fill, 24px radius, 16px padding. */
|
|
13
|
+
export function AgentPanel({
|
|
14
|
+
className,
|
|
15
|
+
filter,
|
|
16
|
+
children,
|
|
17
|
+
...props
|
|
18
|
+
}: WithClass<ComponentProps<"section">> & {
|
|
19
|
+
/** Filter slot under the panel top, such as a `FilterBar`. */ filter?: ReactNode;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<section
|
|
23
|
+
className={cn(
|
|
24
|
+
"flex min-w-0 flex-col items-stretch gap-3 rounded-[24px] border border-border-default bg-bg-surface px-5 pt-4 pb-2",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
{filter && <div className="flex">{filter}</div>}
|
|
30
|
+
{children}
|
|
31
|
+
</section>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ComposerChipProps = WithClass<ComponentProps<"button">> & {
|
|
36
|
+
/** 16px tile initial (bots) or a 14px icon (projects). */
|
|
37
|
+
leading?: ReactNode;
|
|
38
|
+
tileClassName?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** A 30px 5% ink picker chip with a 12px caret, used for the bot and project in the task composer. */
|
|
42
|
+
export function ComposerChip({ leading, tileClassName, className, children, ...props }: ComposerChipProps) {
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
className={cn(
|
|
47
|
+
"flex h-7.5 shrink-0 motion-press cursor-pointer items-center gap-1.5 rounded-full bg-overlay-ink-5 pr-2.5 pl-2 text-label-sm text-text-primary focus-visible:outline-2 focus-visible:outline-text-brand",
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
{typeof leading === "string" ? (
|
|
53
|
+
<span
|
|
54
|
+
className={cn(
|
|
55
|
+
"flex size-4 items-center justify-center rounded-[5px] text-label-sm text-text-inverse",
|
|
56
|
+
tileClassName ?? "bg-interactive-primary",
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{leading}
|
|
60
|
+
</span>
|
|
61
|
+
) : (
|
|
62
|
+
<span className="flex text-text-secondary [&>svg]:size-3.5">{leading}</span>
|
|
63
|
+
)}
|
|
64
|
+
{children}
|
|
65
|
+
<CaretDown aria-hidden className="size-3 text-text-secondary" />
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type TaskComposerProps = {
|
|
71
|
+
placeholder?: string;
|
|
72
|
+
value?: string;
|
|
73
|
+
onValueChange?: (value: string) => void;
|
|
74
|
+
/** Picker chips, usually two `ComposerChip`s. */
|
|
75
|
+
pickers?: ReactNode;
|
|
76
|
+
onStart?: () => void;
|
|
77
|
+
startLabel?: string;
|
|
78
|
+
className?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Figma task `Composer` (Agent tasks 112:797): a 4% ink box, 18px radius, with pickers, a Mod+Enter hint and Start. */
|
|
82
|
+
export function TaskComposer({
|
|
83
|
+
placeholder = "Describe a task for a bot",
|
|
84
|
+
value,
|
|
85
|
+
onValueChange,
|
|
86
|
+
pickers,
|
|
87
|
+
onStart,
|
|
88
|
+
startLabel = "Start",
|
|
89
|
+
className,
|
|
90
|
+
}: TaskComposerProps) {
|
|
91
|
+
const { os } = usePlatform();
|
|
92
|
+
const { keyboardHints } = useDevice();
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
className={cn(
|
|
96
|
+
"flex flex-col gap-3.5 rounded-xl bg-overlay-ink-4 px-4 pt-3.5 pb-2.5 focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-text-brand",
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
<textarea
|
|
101
|
+
rows={1}
|
|
102
|
+
value={value}
|
|
103
|
+
onChange={(event) => onValueChange?.(event.target.value)}
|
|
104
|
+
onKeyDown={(event) => {
|
|
105
|
+
if (event.key === "Enter" && isModKey(event, os)) onStart?.();
|
|
106
|
+
}}
|
|
107
|
+
placeholder={placeholder}
|
|
108
|
+
aria-label={placeholder}
|
|
109
|
+
className="field-sizing-content max-h-40 min-h-5.5 w-full resize-none overflow-y-auto bg-transparent text-body-sm text-text-primary outline-none placeholder:text-text-secondary"
|
|
110
|
+
/>
|
|
111
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
112
|
+
{pickers && <span className="flex min-w-0 flex-wrap items-center gap-1.5">{pickers}</span>}
|
|
113
|
+
<span className="min-w-0 flex-1" />
|
|
114
|
+
{keyboardHints && <Kbd keys="Mod+Enter" />}
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={onStart}
|
|
118
|
+
className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center rounded-full bg-interactive-primary px-3.5 text-label-sm text-text-inverse focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand"
|
|
119
|
+
>
|
|
120
|
+
{startLabel}
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|