@horon-ai/react 0.0.0-stage → 0.1.0

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 (93) hide show
  1. package/README.md +168 -2
  2. package/package.json +50 -5
  3. package/src/assets/sidebar-photo.jpg +0 -0
  4. package/src/components/agent-activity.tsx +202 -0
  5. package/src/components/agent-lane.tsx +80 -0
  6. package/src/components/agent-lanes-timeline.tsx +312 -0
  7. package/src/components/agent-panel.tsx +121 -0
  8. package/src/components/agent-steering-panel.tsx +274 -0
  9. package/src/components/approval-card.tsx +89 -0
  10. package/src/components/avatar.tsx +104 -0
  11. package/src/components/badge.tsx +63 -0
  12. package/src/components/banner.tsx +103 -0
  13. package/src/components/bot-instructions-panel.tsx +365 -0
  14. package/src/components/bot-list-row.tsx +42 -0
  15. package/src/components/bot-memory-panel.tsx +229 -0
  16. package/src/components/bottom-sheet.tsx +140 -0
  17. package/src/components/breadcrumb-header.tsx +87 -0
  18. package/src/components/breadcrumb.tsx +86 -0
  19. package/src/components/button.tsx +146 -0
  20. package/src/components/calendar.tsx +410 -0
  21. package/src/components/card.tsx +79 -0
  22. package/src/components/chat-bubble.tsx +36 -0
  23. package/src/components/chat-controls.tsx +118 -0
  24. package/src/components/checkbox.tsx +73 -0
  25. package/src/components/chip.tsx +81 -0
  26. package/src/components/choice-card.tsx +55 -0
  27. package/src/components/code-panel.tsx +27 -0
  28. package/src/components/combobox.tsx +312 -0
  29. package/src/components/command-palette.tsx +191 -0
  30. package/src/components/composer.tsx +124 -0
  31. package/src/components/data-table.tsx +429 -0
  32. package/src/components/date-field.tsx +100 -0
  33. package/src/components/dialog.tsx +93 -0
  34. package/src/components/directory-card.tsx +59 -0
  35. package/src/components/document-extract-panel.tsx +398 -0
  36. package/src/components/empty-state.tsx +29 -0
  37. package/src/components/field.tsx +78 -0
  38. package/src/components/file-drop.tsx +89 -0
  39. package/src/components/filter-bar.tsx +83 -0
  40. package/src/components/flue-background.tsx +402 -0
  41. package/src/components/horon-icon.tsx +280 -0
  42. package/src/components/horon-wordmark.tsx +26 -0
  43. package/src/components/icon-button.tsx +61 -0
  44. package/src/components/input-group.tsx +112 -0
  45. package/src/components/input.tsx +72 -0
  46. package/src/components/integration-row.tsx +103 -0
  47. package/src/components/kbd.tsx +35 -0
  48. package/src/components/knowledge-base-panel.tsx +185 -0
  49. package/src/components/liquid-glass.tsx +24 -0
  50. package/src/components/list-detail.tsx +275 -0
  51. package/src/components/marketplace-card.tsx +97 -0
  52. package/src/components/mcp-server-row.tsx +122 -0
  53. package/src/components/menu.tsx +165 -0
  54. package/src/components/message-extras.tsx +156 -0
  55. package/src/components/modal.tsx +108 -0
  56. package/src/components/orchestrator-panel.tsx +175 -0
  57. package/src/components/page-header.tsx +48 -0
  58. package/src/components/plan-card.tsx +70 -0
  59. package/src/components/plan-step.tsx +111 -0
  60. package/src/components/plugin-row.tsx +128 -0
  61. package/src/components/progress.tsx +21 -0
  62. package/src/components/radio-group.tsx +84 -0
  63. package/src/components/research-run-panel.tsx +232 -0
  64. package/src/components/schedule.tsx +332 -0
  65. package/src/components/segmented-control.tsx +136 -0
  66. package/src/components/select.tsx +154 -0
  67. package/src/components/settings-list.tsx +99 -0
  68. package/src/components/sidebar.tsx +542 -0
  69. package/src/components/skeleton.tsx +75 -0
  70. package/src/components/slider.tsx +29 -0
  71. package/src/components/source-row.tsx +112 -0
  72. package/src/components/spinner.tsx +29 -0
  73. package/src/components/split-button.tsx +72 -0
  74. package/src/components/switch.tsx +79 -0
  75. package/src/components/tab-bar.tsx +90 -0
  76. package/src/components/tabs.tsx +162 -0
  77. package/src/components/task-row.tsx +159 -0
  78. package/src/components/textarea-field.tsx +127 -0
  79. package/src/components/textarea.tsx +47 -0
  80. package/src/components/toast.tsx +128 -0
  81. package/src/components/tool-call.tsx +224 -0
  82. package/src/components/tool-calls-header.tsx +65 -0
  83. package/src/components/tool-permissions.tsx +105 -0
  84. package/src/components/tooltip.tsx +33 -0
  85. package/src/components/top-bar.tsx +65 -0
  86. package/src/components/usage-meter.tsx +47 -0
  87. package/src/components/workspace-menu.tsx +77 -0
  88. package/src/index.ts +86 -0
  89. package/src/lib/cn.ts +25 -0
  90. package/src/lib/cursor-ghost.ts +244 -0
  91. package/src/lib/size.ts +10 -0
  92. package/src/lib/use-active-indicator.ts +60 -0
  93. package/src/styles/globals.css +564 -0
@@ -0,0 +1,128 @@
1
+ import { ArrowUp, Check, DotsThree } from "@phosphor-icons/react";
2
+ import type { ComponentProps, ReactNode } from "react";
3
+
4
+ import { cn } from "../lib/cn";
5
+ import { Button } from "./button";
6
+ import { IconButton } from "./icon-button";
7
+ import { Menu, MenuContent, MenuTrigger } from "./menu";
8
+ import { Switch } from "./switch";
9
+
10
+ export type PluginRowProps = Omit<ComponentProps<"div">, "children"> & {
11
+ /** Two-letter initials shown on the 40px tile. */
12
+ initials: string;
13
+ name: ReactNode;
14
+ version?: ReactNode;
15
+ /** Text of the update pill next to the version; omit when up to date. */
16
+ updateBadge?: ReactNode;
17
+ author?: ReactNode;
18
+ verified?: boolean;
19
+ /** Capability chips, each with an optional 11px icon. */
20
+ chips?: { id: string; label: ReactNode; icon?: ReactNode }[];
21
+ enabled?: boolean;
22
+ onEnabledChange?: (enabled: boolean) => void;
23
+ /** Shows the Update button when set. */
24
+ onUpdate?: () => void;
25
+ updateLabel?: string;
26
+ toggleLabel?: string;
27
+ overflowLabel?: string;
28
+ /** Items of the overflow menu; the menu button is hidden without them. */
29
+ menu?: ReactNode;
30
+ };
31
+
32
+ /** Paper `Plugin row` (Settings 1VKS-0): tile, name with version and update pill, verified author, chips, and Update, Switch and overflow actions. */
33
+ export function PluginRow({
34
+ initials,
35
+ name,
36
+ version,
37
+ updateBadge,
38
+ author,
39
+ verified,
40
+ chips,
41
+ enabled,
42
+ onEnabledChange,
43
+ onUpdate,
44
+ updateLabel = "Update",
45
+ toggleLabel = "Enabled",
46
+ overflowLabel = "More",
47
+ menu,
48
+ className,
49
+ ...props
50
+ }: PluginRowProps) {
51
+ return (
52
+ <div
53
+ className={cn(
54
+ "flex flex-wrap items-center gap-x-3.5 gap-y-3 border-b border-border-default py-4 last:border-b-0",
55
+ className,
56
+ )}
57
+ {...props}
58
+ >
59
+ <span
60
+ aria-hidden
61
+ className="flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-sky-500 text-label-md text-neutral-50"
62
+ >
63
+ {initials}
64
+ </span>
65
+ <span className="flex min-w-0 grow basis-40 flex-col gap-1.5">
66
+ <span className="flex flex-wrap items-center gap-2">
67
+ <span className="min-w-0 truncate text-label-md text-text-primary">{name}</span>
68
+ {version && <span className="shrink-0 font-mono text-label-sm text-text-secondary">{version}</span>}
69
+ {updateBadge && (
70
+ <span className="inline-flex h-5.5 shrink-0 items-center rounded-full bg-accent-sky-500-18 px-2 text-label-sm text-text-brand">
71
+ {updateBadge}
72
+ </span>
73
+ )}
74
+ </span>
75
+ {author && (
76
+ <span className="flex items-center gap-1 text-label-sm text-text-secondary">
77
+ {author}
78
+ {verified && (
79
+ <span
80
+ role="img"
81
+ aria-label="Verified"
82
+ className="flex size-3.5 shrink-0 items-center justify-center rounded-full bg-sky-500 text-neutral-50"
83
+ >
84
+ <Check aria-hidden weight="bold" className="size-2" />
85
+ </span>
86
+ )}
87
+ </span>
88
+ )}
89
+ {chips && chips.length > 0 && (
90
+ <span className="flex flex-wrap gap-1.5">
91
+ {chips.map((chip) => (
92
+ <span
93
+ key={chip.id}
94
+ className="inline-flex h-5.5 items-center gap-1 rounded-full bg-overlay-ink-6 px-2 text-label-sm text-text-secondary [&_svg]:size-2.75"
95
+ >
96
+ {chip.icon}
97
+ {chip.label}
98
+ </span>
99
+ ))}
100
+ </span>
101
+ )}
102
+ </span>
103
+ <span className="flex shrink-0 flex-wrap items-center gap-2">
104
+ <span className="motion-reveal-x" data-collapsed={!onUpdate || undefined} inert={!onUpdate || undefined}>
105
+ <Button variant="solid" size="sm" icon={<ArrowUp aria-hidden className="size-3.5" />} onClick={onUpdate}>
106
+ {updateLabel}
107
+ </Button>
108
+ </span>
109
+ <Switch aria-label={toggleLabel} checked={enabled} onCheckedChange={onEnabledChange} />
110
+ {menu && (
111
+ <Menu>
112
+ <MenuTrigger
113
+ render={
114
+ <IconButton
115
+ label={overflowLabel}
116
+ variant="ghost"
117
+ size="sm"
118
+ icon={<DotsThree aria-hidden weight="bold" />}
119
+ />
120
+ }
121
+ />
122
+ <MenuContent align="end">{menu}</MenuContent>
123
+ </Menu>
124
+ )}
125
+ </span>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,21 @@
1
+ import { Progress as BaseProgress } from "@base-ui/react/progress";
2
+ import type { ComponentProps } from "react";
3
+
4
+ import { cn } from "../lib/cn";
5
+
6
+ export type ProgressProps = Omit<ComponentProps<typeof BaseProgress.Root>, "className" | "value"> & {
7
+ className?: string;
8
+ /** 0-100. Omit it (or pass `null`) for the indeterminate bar. */
9
+ value?: number | null;
10
+ };
11
+
12
+ /** Figma `Progress bar` (in Loading 109:733): a 6px 8% ink track with an ink fill. */
13
+ export function Progress({ className, value = null, ...props }: ProgressProps) {
14
+ return (
15
+ <BaseProgress.Root value={value} className={cn("w-60", className)} {...props}>
16
+ <BaseProgress.Track className="h-1.5 w-full overflow-clip rounded-full bg-overlay-ink-8">
17
+ <BaseProgress.Indicator className="h-full rounded-full bg-interactive-primary transition-[width] duration-400 ease-out-quint data-indeterminate:w-1/3 data-indeterminate:animate-sweep" />
18
+ </BaseProgress.Track>
19
+ </BaseProgress.Root>
20
+ );
21
+ }
@@ -0,0 +1,84 @@
1
+ import { Radio as BaseRadio } from "@base-ui/react/radio";
2
+ import { RadioGroup as BaseRadioGroup } from "@base-ui/react/radio-group";
3
+ import { type ComponentProps, createContext, type ReactNode, useContext } from "react";
4
+
5
+ import { cn } from "../lib/cn";
6
+ import type { ControlSize } from "../lib/size";
7
+
8
+ export type RadioSize = ControlSize;
9
+
10
+ /** The group owns the size: every radio in one list reads the same mark. */
11
+ const RadioSizeContext = createContext<ControlSize>("md");
12
+
13
+ export type RadioGroupProps = Omit<ComponentProps<typeof BaseRadioGroup>, "className"> & {
14
+ /** Mark size for every `Radio` inside; a radio can still override it. */
15
+ size?: RadioSize;
16
+ className?: string;
17
+ };
18
+
19
+ export function RadioGroup({ size = "md", className, ...props }: RadioGroupProps) {
20
+ return (
21
+ <RadioSizeContext.Provider value={size}>
22
+ <BaseRadioGroup className={cn("flex flex-col gap-3", className)} {...props} />
23
+ </RadioSizeContext.Provider>
24
+ );
25
+ }
26
+
27
+ const labelClass: Record<RadioSize, string> = {
28
+ sm: "gap-2 text-body-sm",
29
+ md: "gap-2.5 text-body-md",
30
+ lg: "gap-3 text-body-lg",
31
+ };
32
+
33
+ const circleClass: Record<RadioSize, string> = {
34
+ sm: "size-4",
35
+ md: "size-5",
36
+ lg: "size-6",
37
+ };
38
+
39
+ const dotClass: Record<RadioSize, string> = {
40
+ sm: "size-1.5",
41
+ md: "size-2",
42
+ lg: "size-2.5",
43
+ };
44
+
45
+ export type RadioProps = Omit<ComponentProps<typeof BaseRadio.Root>, "className"> & {
46
+ label?: ReactNode;
47
+ /** Overrides the enclosing `RadioGroup` size. */
48
+ size?: RadioSize;
49
+ className?: string;
50
+ };
51
+
52
+ /** Figma `Radio` (58:239): a 20px circle; checked fills with ink and an 8px dot. */
53
+ export function Radio({ label, size, className, ...props }: RadioProps) {
54
+ const groupSize = useContext(RadioSizeContext);
55
+ const resolvedSize = size ?? groupSize;
56
+ return (
57
+ <label
58
+ className={cn(
59
+ "inline-flex max-w-full min-w-0 cursor-pointer items-center text-text-primary has-[[data-disabled]]:cursor-not-allowed has-[[data-disabled]]:opacity-40",
60
+ labelClass[resolvedSize],
61
+ className,
62
+ )}
63
+ >
64
+ <BaseRadio.Root
65
+ className={cn(
66
+ "flex shrink-0 items-center justify-center rounded-full border-[1.5px] border-border-strong bg-bg-surface motion-colors",
67
+ circleClass[resolvedSize],
68
+ "hover:not-data-disabled:border-text-secondary",
69
+ "data-checked:border-interactive-primary data-checked:bg-interactive-primary hover:not-data-disabled:data-checked:border-interactive-primary-hover hover:not-data-disabled:data-checked:bg-interactive-primary-hover",
70
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand",
71
+ )}
72
+ {...props}
73
+ >
74
+ <BaseRadio.Indicator
75
+ className={cn(
76
+ "animate-pop-in rounded-full bg-interactive-on-primary data-ending-style:animate-scale-out",
77
+ dotClass[resolvedSize],
78
+ )}
79
+ />
80
+ </BaseRadio.Root>
81
+ {label}
82
+ </label>
83
+ );
84
+ }
@@ -0,0 +1,232 @@
1
+ import { ArrowUp, CaretDown, Check, MagnifyingGlass, Plus, Stop } from "@phosphor-icons/react";
2
+ import { Fragment, type CSSProperties, type ComponentProps, type ReactNode, useRef } from "react";
3
+
4
+ import { cn } from "../lib/cn";
5
+ import { ChatBubble } from "./chat-bubble";
6
+ import { Spinner } from "./spinner";
7
+
8
+ export type ResearchQuery = {
9
+ id: string;
10
+ /** `searching` shows a spinner and the muted text; `done` a check. */
11
+ state?: "done" | "searching";
12
+ text: ReactNode;
13
+ /** Result count shown at the end of the row, such as "8 results". */
14
+ results?: ReactNode;
15
+ };
16
+
17
+ export type ResearchRunPanelProps = Omit<ComponentProps<"section">, "title"> & {
18
+ title?: ReactNode;
19
+ newLabel?: ReactNode;
20
+ onNewResearch?: () => void;
21
+ /** The question bubble, right-aligned. */
22
+ question: ReactNode;
23
+ /** Shows the spinner, status line and caret while the run works. */
24
+ running?: boolean;
25
+ statusLabel?: ReactNode;
26
+ elapsed?: ReactNode;
27
+ read?: number;
28
+ total?: number;
29
+ queries?: ResearchQuery[];
30
+ /** Initials for the stacked source avatars under the queries. */
31
+ sources?: string[];
32
+ onStop?: () => void;
33
+ /** Makes the sources pill actionable; without it the pill is a plain label. */
34
+ onOpenSources?: () => void;
35
+ /** Makes the running status line expandable; without it the row carries no caret. */
36
+ onToggleStatus?: () => void;
37
+ followUpPlaceholder?: string;
38
+ onFollowUp?: (value: string) => void;
39
+ };
40
+
41
+ const stagger = (index: number) => ({ "--i": index, animationDelay: `calc(var(--i) * 30ms)` }) as CSSProperties;
42
+
43
+ /** Paper `Research run` (Agents N10): a 24px-radius panel with a header, the question, a live status line and the queries read so far. */
44
+ export function ResearchRunPanel({
45
+ title = "Research",
46
+ newLabel = "New research",
47
+ onNewResearch,
48
+ question,
49
+ running = true,
50
+ statusLabel = "Researching",
51
+ elapsed,
52
+ read,
53
+ total,
54
+ queries,
55
+ sources,
56
+ onStop,
57
+ onOpenSources,
58
+ onToggleStatus,
59
+ followUpPlaceholder = "Ask a follow-up",
60
+ onFollowUp,
61
+ className,
62
+ ...props
63
+ }: ResearchRunPanelProps) {
64
+ const field = useRef<HTMLInputElement>(null);
65
+ const count = read !== undefined && total !== undefined ? `${read} of ${total} read` : undefined;
66
+ const meta = [
67
+ { id: "elapsed", value: elapsed },
68
+ { id: "count", value: count },
69
+ ].filter((entry) => entry.value !== undefined);
70
+ const sourcesLabel =
71
+ read !== undefined && total !== undefined ? `${read} of ${total} sources read` : `${sources?.length ?? 0} sources`;
72
+ const avatarStack = (
73
+ <span className="flex shrink-0">
74
+ {sources?.map((initial, index) => (
75
+ <span
76
+ key={initial}
77
+ className="flex size-7 shrink-0 items-center justify-center rounded-full bg-sky-700 text-label-sm text-text-on-ink ring-2 ring-bg-surface"
78
+ style={{ marginLeft: index === 0 ? 0 : -6 }}
79
+ >
80
+ {initial}
81
+ </span>
82
+ ))}
83
+ </span>
84
+ );
85
+ return (
86
+ <section
87
+ className={cn(
88
+ "flex w-full max-w-150 min-w-0 flex-col overflow-clip rounded-[24px] border border-border-default bg-bg-surface",
89
+ className,
90
+ )}
91
+ {...props}
92
+ >
93
+ <header className="flex flex-wrap items-center gap-x-2 gap-y-2 px-5 pt-4 pb-3.5">
94
+ <MagnifyingGlass aria-hidden className="size-4 shrink-0 text-text-primary" />
95
+ <h3 className="min-w-0 flex-1 truncate text-label-lg text-text-primary">{title}</h3>
96
+ <button
97
+ type="button"
98
+ onClick={onNewResearch}
99
+ className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center gap-1.5 rounded-full bg-overlay-ink-6 pr-3 pl-3.5 text-label-sm text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-text-brand"
100
+ >
101
+ {newLabel}
102
+ <Plus aria-hidden className="size-3.5" />
103
+ </button>
104
+ </header>
105
+ <div role="separator" className="h-px bg-overlay-ink-8" />
106
+ <div className="flex flex-col gap-3 px-5 pt-4 pb-4">
107
+ <ChatBubble from="me" className="max-w-[85%] self-end">
108
+ {question}
109
+ </ChatBubble>
110
+ <div className="motion-reveal" data-collapsed={!running || undefined}>
111
+ <div>
112
+ {onToggleStatus ? (
113
+ <button
114
+ type="button"
115
+ onClick={onToggleStatus}
116
+ aria-live="polite"
117
+ className="flex w-full cursor-pointer flex-wrap items-center gap-x-1.5 gap-y-1 rounded-md text-left focus-visible:outline-2 focus-visible:outline-text-brand"
118
+ >
119
+ <Spinner size={14} />
120
+ <span className="min-w-0 text-label-md text-text-primary">{statusLabel}</span>
121
+ <span aria-hidden className="h-3 w-0.5 shrink-0 animate-caret rounded-[1px] bg-interactive-primary" />
122
+ {meta.length > 0 && (
123
+ <span className="text-caption text-text-secondary tabular-nums">
124
+ {meta.map((entry, index) => (
125
+ <Fragment key={entry.id}>
126
+ {index > 0 && " · "}
127
+ {entry.value}
128
+ </Fragment>
129
+ ))}
130
+ </span>
131
+ )}
132
+ <CaretDown aria-hidden className="size-3 shrink-0 text-text-secondary" />
133
+ </button>
134
+ ) : (
135
+ <div aria-live="polite" className="flex flex-wrap items-center gap-x-1.5 gap-y-1">
136
+ <Spinner size={14} />
137
+ <span className="min-w-0 text-label-md text-text-primary">{statusLabel}</span>
138
+ <span aria-hidden className="h-3 w-0.5 shrink-0 animate-caret rounded-[1px] bg-interactive-primary" />
139
+ {meta.length > 0 && (
140
+ <span className="text-caption text-text-secondary tabular-nums">
141
+ {meta.map((entry, index) => (
142
+ <Fragment key={entry.id}>
143
+ {index > 0 && " · "}
144
+ {entry.value}
145
+ </Fragment>
146
+ ))}
147
+ </span>
148
+ )}
149
+ </div>
150
+ )}
151
+ </div>
152
+ </div>
153
+ {queries && queries.length > 0 && (
154
+ <ul className="relative flex flex-col gap-2.5 py-0.5 pl-3.5">
155
+ <span aria-hidden className="absolute top-0 bottom-0 left-0 w-[1.5px] rounded-full bg-overlay-ink-14" />
156
+ {queries.map((query, index) => (
157
+ <li key={query.id} className="flex motion-rise items-center gap-2.5" style={stagger(index)}>
158
+ {query.state === "searching" ? (
159
+ <Spinner size={14} />
160
+ ) : (
161
+ <Check aria-hidden className="size-3.5 shrink-0 text-text-secondary" />
162
+ )}
163
+ <span
164
+ className={cn(
165
+ "min-w-0 flex-1 truncate text-body-sm",
166
+ query.state === "searching" ? "text-text-secondary" : "text-text-primary",
167
+ )}
168
+ >
169
+ {query.text}
170
+ </span>
171
+ {query.results !== undefined && (
172
+ <span className="shrink-0 text-caption text-text-secondary">{query.results}</span>
173
+ )}
174
+ </li>
175
+ ))}
176
+ </ul>
177
+ )}
178
+ {sources && sources.length > 0 && (
179
+ <div className="animate-pop-in">
180
+ {onOpenSources ? (
181
+ <button
182
+ type="button"
183
+ onClick={onOpenSources}
184
+ className="flex h-9 w-fit motion-press cursor-pointer items-center gap-2 rounded-full bg-overlay-ink-5 pr-3 pl-1 hover:bg-overlay-ink-8 focus-visible:outline-2 focus-visible:outline-text-brand"
185
+ >
186
+ {avatarStack}
187
+ <span className="text-label-md text-text-primary tabular-nums">{sourcesLabel}</span>
188
+ <CaretDown aria-hidden className="size-3 text-text-secondary" />
189
+ </button>
190
+ ) : (
191
+ <div className="flex h-9 w-fit items-center gap-2 rounded-full bg-overlay-ink-5 pr-3 pl-1">
192
+ {avatarStack}
193
+ <span className="text-label-md text-text-primary tabular-nums">{sourcesLabel}</span>
194
+ </div>
195
+ )}
196
+ </div>
197
+ )}
198
+ </div>
199
+ <form
200
+ className="px-5 pt-1 pb-4"
201
+ onSubmit={(event) => {
202
+ event.preventDefault();
203
+ const input = field.current;
204
+ if (!input) return;
205
+ onFollowUp?.(input.value);
206
+ input.value = "";
207
+ }}
208
+ >
209
+ <div className="flex h-11 items-center gap-2 rounded-full bg-overlay-ink-5 pr-1.5 pl-4 motion-colors focus-within:bg-overlay-ink-8">
210
+ <input
211
+ ref={field}
212
+ name="follow-up"
213
+ aria-label={followUpPlaceholder}
214
+ placeholder={followUpPlaceholder}
215
+ className="min-w-0 flex-1 bg-transparent text-body-sm text-text-primary outline-none placeholder:text-text-secondary"
216
+ />
217
+ <button
218
+ type={running ? "button" : "submit"}
219
+ onClick={running ? onStop : undefined}
220
+ aria-label={running ? "Stop" : "Send"}
221
+ className="flex size-8 shrink-0 motion-press cursor-pointer items-center justify-center rounded-full bg-interactive-primary text-text-inverse focus-visible:outline-2 focus-visible:outline-text-brand"
222
+ >
223
+ <span className="motion-swap size-8 place-items-center">
224
+ <Stop aria-hidden weight="fill" data-hidden={!running || undefined} className="size-3.5" />
225
+ <ArrowUp aria-hidden data-hidden={running || undefined} className="size-4" />
226
+ </span>
227
+ </button>
228
+ </div>
229
+ </form>
230
+ </section>
231
+ );
232
+ }