@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
|
@@ -6,33 +6,73 @@ import { Button } from "./button";
|
|
|
6
6
|
import { CodePanel } from "./code-panel";
|
|
7
7
|
import { LiquidGlass } from "./liquid-glass";
|
|
8
8
|
|
|
9
|
+
/** One selectable row of a {@link ListDetail} list. */
|
|
9
10
|
export type ListDetailItem = {
|
|
11
|
+
/** Stable identifier; it is the value the selection reports and the React key. */
|
|
10
12
|
id: string;
|
|
13
|
+
/** Row title, drawn in mono and truncated to one line. */
|
|
11
14
|
title: ReactNode;
|
|
15
|
+
/** Secondary line under the title, in secondary colour. */
|
|
12
16
|
meta?: ReactNode;
|
|
13
17
|
};
|
|
14
18
|
|
|
19
|
+
/** A captioned group of {@link ListDetailItem} rows. */
|
|
15
20
|
export type ListDetailGroup = {
|
|
21
|
+
/** Stable identifier used as the group's React key. */
|
|
16
22
|
id: string;
|
|
23
|
+
/** Caption above the rows, in 11px semibold secondary text. */
|
|
17
24
|
label: ReactNode;
|
|
25
|
+
/** Rows of this group, in display order. */
|
|
18
26
|
items: ListDetailItem[];
|
|
19
27
|
};
|
|
20
28
|
|
|
29
|
+
/** Props for {@link ListDetail}: the groups and one render function for the selected item. */
|
|
21
30
|
export type ListDetailProps = Omit<ComponentProps<"div">, "className" | "children" | "onChange"> & {
|
|
31
|
+
/** Groups rendered into the left list; the flat order also drives the selection fallback. */
|
|
22
32
|
groups: ListDetailGroup[];
|
|
33
|
+
/** Controlled selected id. Leave unset to let the component hold the selection itself. */
|
|
23
34
|
value?: string;
|
|
35
|
+
/** Initial selection when uncontrolled. Falls back to the first item of the first group. */
|
|
24
36
|
defaultValue?: string;
|
|
37
|
+
/** Fires with the new id whenever a row is clicked, controlled or not. */
|
|
25
38
|
onValueChange?: (id: string) => void;
|
|
26
39
|
/** Renders the detail pane for the selected item. */
|
|
27
40
|
children: (item: ListDetailItem) => ReactNode;
|
|
28
|
-
/** `solid` (default) is an opaque surface shell with a default border; `glass` is the thick glass shell. */
|
|
41
|
+
/** `solid` (default) is an opaque surface shell with a default border; `glass` is the thick glass shell. @default "solid" */
|
|
29
42
|
surface?: "solid" | "glass";
|
|
43
|
+
/** Extra classes on the shell, merged after the built-in ones. */
|
|
30
44
|
className?: string;
|
|
31
45
|
};
|
|
32
46
|
|
|
33
47
|
/**
|
|
34
48
|
* Paper `List and detail` (1W6P-0): a 16px-radius shell, solid or thick glass, with a 250px list on an inset
|
|
35
49
|
* surface and a detail pane. Selecting a row fills it in 150ms and swaps the detail with a short rise.
|
|
50
|
+
*
|
|
51
|
+
* Selection is controlled or uncontrolled like a native input: pass `value` to
|
|
52
|
+
* own it, or `defaultValue` for the initial row. Below `sm` the two panes stack
|
|
53
|
+
* instead of shrinking the list. The list is a `role="listbox"` whose rows are
|
|
54
|
+
* `role="option"` buttons with `aria-selected`; they respond to clicks and the
|
|
55
|
+
* keyboard's normal button activation.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* <ListDetail
|
|
60
|
+
* groups={[
|
|
61
|
+
* {
|
|
62
|
+
* id: "sources",
|
|
63
|
+
* label: "Sources",
|
|
64
|
+
* items: [{ id: "readme", title: "README.md", meta: "2.1 KB" }],
|
|
65
|
+
* },
|
|
66
|
+
* ]}
|
|
67
|
+
* >
|
|
68
|
+
* {(item) => (
|
|
69
|
+
* <>
|
|
70
|
+
* <ListDetailHeader title={item.title} description="Indexed 4 minutes ago" />
|
|
71
|
+
* <ListDetailMeta items={[{ id: "chunks", label: "Chunks", value: 42 }]} />
|
|
72
|
+
* </>
|
|
73
|
+
* )}
|
|
74
|
+
* </ListDetail>
|
|
75
|
+
* ```
|
|
36
76
|
*/
|
|
37
77
|
export function ListDetail({
|
|
38
78
|
groups,
|
|
@@ -103,16 +143,24 @@ export function ListDetail({
|
|
|
103
143
|
);
|
|
104
144
|
}
|
|
105
145
|
|
|
146
|
+
/** Props for {@link ListDetailHeader}: a tile icon, title, description and a right-hand action cluster. */
|
|
106
147
|
export type ListDetailHeaderProps = Omit<ComponentProps<"div">, "className" | "title"> & {
|
|
148
|
+
/** Glyph on the 40px sky tile; the tile only renders when this is set. */
|
|
107
149
|
icon?: ReactNode;
|
|
150
|
+
/** Detail title, drawn in mono at `label-lg` semibold. */
|
|
108
151
|
title: ReactNode;
|
|
152
|
+
/** One line under the title, in body-sm secondary colour. */
|
|
109
153
|
description?: ReactNode;
|
|
110
154
|
/** Right-hand cluster: a state label, a `Switch`, an icon button. */
|
|
111
155
|
actions?: ReactNode;
|
|
156
|
+
/** Extra classes on the row, merged after the built-in ones. */
|
|
112
157
|
className?: string;
|
|
113
158
|
};
|
|
114
159
|
|
|
115
|
-
/** Detail header: a 40px sky tile with
|
|
160
|
+
/** Detail header: a 40px sky tile with a 20px icon, title and description, actions on the right.
|
|
161
|
+
*
|
|
162
|
+
* Part of {@link ListDetail}.
|
|
163
|
+
*/
|
|
116
164
|
export function ListDetailHeader({ icon, title, description, actions, className, ...props }: ListDetailHeaderProps) {
|
|
117
165
|
return (
|
|
118
166
|
<div className={cn("flex flex-wrap items-center gap-3", className)} {...props}>
|
|
@@ -130,12 +178,25 @@ export function ListDetailHeader({ icon, title, description, actions, className,
|
|
|
130
178
|
);
|
|
131
179
|
}
|
|
132
180
|
|
|
181
|
+
/** Props for {@link ListDetailMeta}: the label/value pairs of the meta box. */
|
|
133
182
|
export type ListDetailMetaProps = Omit<ComponentProps<"dl">, "className"> & {
|
|
134
|
-
|
|
183
|
+
/** Pairs rendered in a two-column grid, label over value. */
|
|
184
|
+
items: {
|
|
185
|
+
/** Stable identifier of the pair; its React key. */
|
|
186
|
+
id: string;
|
|
187
|
+
/** Term above the value, in `label-sm` secondary text. */
|
|
188
|
+
label: ReactNode;
|
|
189
|
+
/** Definition under the label, truncated to one line. */
|
|
190
|
+
value: ReactNode;
|
|
191
|
+
}[];
|
|
192
|
+
/** Extra classes on the `dl`, merged after the built-in ones. */
|
|
135
193
|
className?: string;
|
|
136
194
|
};
|
|
137
195
|
|
|
138
|
-
/** Meta box: a subtle-fill 12px-radius box with equal columns of a label over a 14/20 value.
|
|
196
|
+
/** Meta box: a subtle-fill 12px-radius box with equal columns of a label over a 14/20 value.
|
|
197
|
+
*
|
|
198
|
+
* Part of {@link ListDetail}.
|
|
199
|
+
*/
|
|
139
200
|
export function ListDetailMeta({ items, className, ...props }: ListDetailMetaProps) {
|
|
140
201
|
return (
|
|
141
202
|
<dl
|
|
@@ -152,13 +213,21 @@ export function ListDetailMeta({ items, className, ...props }: ListDetailMetaPro
|
|
|
152
213
|
);
|
|
153
214
|
}
|
|
154
215
|
|
|
216
|
+
/** Props for {@link ListDetailInstructions}: the block heading, the text and the edit callback. */
|
|
155
217
|
export type ListDetailInstructionsProps = Omit<ComponentProps<"div">, "className" | "onChange"> & {
|
|
218
|
+
/** Heading above the panel.
|
|
219
|
+
* @default "Instructions" */
|
|
156
220
|
title?: ReactNode;
|
|
157
221
|
/** Small chip beside the title, such as the file name. */
|
|
158
222
|
tag?: ReactNode;
|
|
223
|
+
/** Controlled text. Leave unset to let the component hold the draft itself. */
|
|
159
224
|
value?: string;
|
|
225
|
+
/** Initial text when uncontrolled.
|
|
226
|
+
* @default "" */
|
|
160
227
|
defaultValue?: string;
|
|
228
|
+
/** Fires with the full textarea value on every keystroke in edit mode. */
|
|
161
229
|
onValueChange?: (value: string) => void;
|
|
230
|
+
/** Extra classes on the wrapper, merged after the built-in ones. */
|
|
162
231
|
className?: string;
|
|
163
232
|
};
|
|
164
233
|
|
|
@@ -244,14 +313,21 @@ export function ListDetailInstructions({
|
|
|
244
313
|
);
|
|
245
314
|
}
|
|
246
315
|
|
|
316
|
+
/** Props for {@link ListDetailFiles}: the row label and the chips it wraps. */
|
|
247
317
|
export type ListDetailFilesProps = Omit<ComponentProps<"div">, "className"> & {
|
|
318
|
+
/** Heading above the chips.
|
|
319
|
+
* @default "Files" */
|
|
248
320
|
label?: ReactNode;
|
|
249
321
|
/** Chips, usually a small `Button` with an icon. */
|
|
250
322
|
children: ReactNode;
|
|
323
|
+
/** Extra classes on the wrapper, merged after the built-in ones. */
|
|
251
324
|
className?: string;
|
|
252
325
|
};
|
|
253
326
|
|
|
254
|
-
/** Files row: a 13/18 label over a wrap of 32px glass chips, 6px apart.
|
|
327
|
+
/** Files row: a 13/18 label over a wrap of 32px glass chips, 6px apart.
|
|
328
|
+
*
|
|
329
|
+
* Part of {@link ListDetail}.
|
|
330
|
+
*/
|
|
255
331
|
export function ListDetailFiles({ label = "Files", children, className, ...props }: ListDetailFilesProps) {
|
|
256
332
|
return (
|
|
257
333
|
<div className={cn("flex flex-col gap-2", className)} {...props}>
|
|
@@ -261,11 +337,17 @@ export function ListDetailFiles({ label = "Files", children, className, ...props
|
|
|
261
337
|
);
|
|
262
338
|
}
|
|
263
339
|
|
|
264
|
-
/** Footer: a 1px top rule and 8px-spaced glass buttons; use `<span className="flex-1" />` as the spacer.
|
|
340
|
+
/** Footer: a 1px top rule and 8px-spaced glass buttons; use `<span className="flex-1" />` as the spacer.
|
|
341
|
+
*
|
|
342
|
+
* Part of {@link ListDetail}.
|
|
343
|
+
*/
|
|
265
344
|
export function ListDetailFooter({
|
|
266
345
|
className,
|
|
267
346
|
...props
|
|
268
|
-
}: Omit<ComponentProps<"div">, "className"> & {
|
|
347
|
+
}: Omit<ComponentProps<"div">, "className"> & {
|
|
348
|
+
/** Extra classes on the row, merged after the built-in ones. */
|
|
349
|
+
className?: string;
|
|
350
|
+
}) {
|
|
269
351
|
return (
|
|
270
352
|
<div
|
|
271
353
|
className={cn("flex flex-wrap items-center gap-2 border-t border-border-default pt-3.5", className)}
|
|
@@ -5,23 +5,59 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { Button } from "./button";
|
|
6
6
|
import { LiquidGlass } from "./liquid-glass";
|
|
7
7
|
|
|
8
|
+
/** Props for {@link MarketplaceCard}: identity, copy, capability chips and the install action. */
|
|
8
9
|
export type MarketplaceCardProps = Omit<ComponentProps<"div">, "children"> & {
|
|
9
10
|
/** Two-letter initials shown on the 40px ink tile. */
|
|
10
11
|
initials: string;
|
|
12
|
+
/** Card title on the row beside the tile; truncated in one line. */
|
|
11
13
|
name: ReactNode;
|
|
14
|
+
/** Publisher line under the name, in caption colour. */
|
|
12
15
|
author?: ReactNode;
|
|
16
|
+
/** Body copy under the header; any length, wrapped by the paragraph. */
|
|
13
17
|
description?: ReactNode;
|
|
14
|
-
/** Capability chips, each with an optional
|
|
15
|
-
chips?: {
|
|
18
|
+
/** Capability chips, each with an optional 12px icon. */
|
|
19
|
+
chips?: {
|
|
20
|
+
/** Stable key for this chip; also its React key. */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Chip text. */
|
|
23
|
+
label: ReactNode;
|
|
24
|
+
/** Optional leading icon, rendered at 12px by the chip's `[&_svg]:size-3`. */
|
|
25
|
+
icon?: ReactNode;
|
|
26
|
+
}[];
|
|
27
|
+
/** Rating value shown after a filled star, such as "4.8". Hides the star when unset. */
|
|
16
28
|
score?: ReactNode;
|
|
29
|
+
/** Install-count caption, separated from the score by a middle dot when both are set. */
|
|
17
30
|
installs?: ReactNode;
|
|
31
|
+
/** Renders the Install button in the footer when set; receives the native click event. */
|
|
18
32
|
onInstall?: () => void;
|
|
33
|
+
/** Text of the Install button.
|
|
34
|
+
* @default "Install" */
|
|
19
35
|
installLabel?: string;
|
|
20
|
-
/** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. */
|
|
36
|
+
/** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. @default "solid" */
|
|
21
37
|
surface?: "solid" | "glass";
|
|
22
38
|
};
|
|
23
39
|
|
|
24
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Paper `Marketplace card` (Settings 1VMY-0): solid or thick glass column with tile header, description, chips and a footer with rating and Install.
|
|
42
|
+
*
|
|
43
|
+
* The card is a plain `div`, so any extra native attribute (`data-*`, `id`,
|
|
44
|
+
* `onClick`) passes through. With `surface="glass"` it paints a `LiquidGlass`
|
|
45
|
+
* layer and every child is positioned to stay above it.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <MarketplaceCard
|
|
50
|
+
* initials="GH"
|
|
51
|
+
* name="GitHub"
|
|
52
|
+
* author="by Horon"
|
|
53
|
+
* description="Read repositories and open pull requests."
|
|
54
|
+
* chips={[{ id: "official", label: "Official" }]}
|
|
55
|
+
* score="4.8"
|
|
56
|
+
* installs="12k installs"
|
|
57
|
+
* onInstall={() => install("github")}
|
|
58
|
+
* />
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
25
61
|
export function MarketplaceCard({
|
|
26
62
|
initials,
|
|
27
63
|
name,
|
|
@@ -8,9 +8,24 @@ import { Menu, MenuContent, MenuTrigger } from "./menu";
|
|
|
8
8
|
|
|
9
9
|
const pillClass = "inline-flex h-5.5 items-center rounded-full px-2 text-label-sm";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Colour of an MCP server's status pill.
|
|
13
|
+
*
|
|
14
|
+
* - `connected` — muted pill with a live dot, the healthy default.
|
|
15
|
+
* - `warning` — danger-tinted pill with a danger dot, for a server that needs attention.
|
|
16
|
+
* - `neutral` — muted pill with a placeholder-coloured dot, for an idle or unknown state.
|
|
17
|
+
*/
|
|
11
18
|
export type McpServerStatusTone = "connected" | "warning" | "neutral";
|
|
12
19
|
|
|
13
|
-
const statusToneClass: Record<
|
|
20
|
+
const statusToneClass: Record<
|
|
21
|
+
McpServerStatusTone,
|
|
22
|
+
{
|
|
23
|
+
/** Classes of the pill itself: background tint and text colour. */
|
|
24
|
+
pill: string;
|
|
25
|
+
/** Classes of the 6px dot inside the pill: its fill colour. */
|
|
26
|
+
dot: string;
|
|
27
|
+
}
|
|
28
|
+
> = {
|
|
14
29
|
connected: { pill: "bg-overlay-ink-6 text-text-primary", dot: "bg-status-live" },
|
|
15
30
|
warning: { pill: "bg-accent-red-500-14 text-text-danger", dot: "bg-text-danger" },
|
|
16
31
|
neutral: { pill: "bg-overlay-ink-6 text-text-secondary", dot: "bg-text-placeholder" },
|
|
@@ -26,29 +41,60 @@ const withKeys = (values: string[] | undefined) => {
|
|
|
26
41
|
});
|
|
27
42
|
};
|
|
28
43
|
|
|
44
|
+
/** Props for {@link McpServerRow}: the server identity, its tags and status, and the two actions. */
|
|
29
45
|
export type McpServerRowProps = Omit<ComponentProps<"div">, "className" | "title"> & {
|
|
30
46
|
/** Server name; its first two letters fill the 40px tile unless `initials` is set. */
|
|
31
47
|
name: string;
|
|
48
|
+
/** Overrides the two-letter tile text derived from `name`; up to two characters read best. */
|
|
32
49
|
initials?: string;
|
|
33
50
|
/** Tags next to the name, such as "Official". */
|
|
34
51
|
tags?: string[];
|
|
35
52
|
/** Tags tinted brand, such as "All devices". */
|
|
36
53
|
brandTags?: string[];
|
|
54
|
+
/** Body line under the tags, in body-sm secondary colour. */
|
|
37
55
|
description?: ReactNode;
|
|
38
56
|
/** Meta line, joined by "·". */
|
|
39
57
|
meta?: string[];
|
|
58
|
+
/** Text inside the status pill.
|
|
59
|
+
* @default "Connected" */
|
|
40
60
|
statusLabel?: ReactNode;
|
|
41
|
-
/** Colour of the status pill: a live dot by default, a danger tint for a warning, a muted dot for a neutral state.
|
|
61
|
+
/** Colour of the status pill: a live dot by default, a danger tint for a warning, a muted dot for a neutral state.
|
|
62
|
+
* @default "connected" */
|
|
42
63
|
statusTone?: McpServerStatusTone;
|
|
64
|
+
/** Text of the Configure button.
|
|
65
|
+
* @default "Configure" */
|
|
43
66
|
configureLabel?: ReactNode;
|
|
67
|
+
/** Fires when the Configure button is clicked; the button always renders, so pass this to make it do anything. */
|
|
44
68
|
onConfigure?: () => void;
|
|
45
69
|
/** Overflow menu items (`MenuItem`). Hides the overflow button when absent. */
|
|
46
70
|
menu?: ReactNode;
|
|
71
|
+
/** Accessible label of the overflow button.
|
|
72
|
+
* @default "More" */
|
|
47
73
|
menuLabel?: string;
|
|
74
|
+
/** Extra classes on the row `div`, merged after the built-in ones. */
|
|
48
75
|
className?: string;
|
|
49
76
|
};
|
|
50
77
|
|
|
51
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Paper `MCP server row` (1VHS-0): a divided row with a 40px tile, tags, a status pill and two actions.
|
|
80
|
+
*
|
|
81
|
+
* A row of a settings list rather than a card: it draws only a bottom border and
|
|
82
|
+
* drops it on the last child. The main cluster wraps (`basis-40`) so the row stays
|
|
83
|
+
* usable in a narrow column; its overflow menu opens from the row and portals to
|
|
84
|
+
* `document.body`.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* <McpServerRow
|
|
89
|
+
* name="Linear"
|
|
90
|
+
* tags={["Official"]}
|
|
91
|
+
* description="Read issues and projects."
|
|
92
|
+
* meta={["12 tools", "OAuth"]}
|
|
93
|
+
* onConfigure={() => openConfig("linear")}
|
|
94
|
+
* menu={<MenuItem>Reconnect</MenuItem>}
|
|
95
|
+
* />
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
52
98
|
export function McpServerRow({
|
|
53
99
|
name,
|
|
54
100
|
initials,
|
package/src/components/menu.tsx
CHANGED
|
@@ -3,15 +3,38 @@ import { createContext, useContext, type ComponentProps, type ReactNode } from "
|
|
|
3
3
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
|
|
6
|
-
type WithClass<T> = Omit<T, "className"> & {
|
|
6
|
+
type WithClass<T> = Omit<T, "className"> & {
|
|
7
|
+
/** Extra classes merged onto the element the part renders. */
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
7
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Visual treatment of the popup.
|
|
13
|
+
*
|
|
14
|
+
* - `glass` — the Figma `Menu` on the veil: 20px radius, 40px rows, 220px wide.
|
|
15
|
+
* - `solid` — the Account / Workspace menu that opens over glass chrome: opaque fill, 18px radius, `shadow-popover`, 36px rows, 256px wide.
|
|
16
|
+
* - `paper` — the Paper workspace switcher: default fill, 1px default border, 14px radius, `0 12 32` shadow, 304px wide, 36px rows.
|
|
17
|
+
*/
|
|
8
18
|
export type MenuSurface = "glass" | "solid" | "paper";
|
|
9
19
|
|
|
10
20
|
const SurfaceContext = createContext<MenuSurface>("glass");
|
|
11
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Root of a menu; owns open state and the trigger/popup pair. Re-exported from
|
|
24
|
+
* Base UI's `Menu.Root`, so it accepts `open`, `defaultOpen`, `onOpenChange` and
|
|
25
|
+
* `modal`.
|
|
26
|
+
*/
|
|
12
27
|
export const Menu = BaseMenu.Root;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The element that opens the menu. Base UI gives it `aria-haspopup` and the
|
|
31
|
+
* expanded state; use `render` to make an existing button the trigger.
|
|
32
|
+
*
|
|
33
|
+
* Part of {@link MenuContent}.
|
|
34
|
+
*/
|
|
13
35
|
export const MenuTrigger = BaseMenu.Trigger;
|
|
14
36
|
|
|
37
|
+
/** Props for {@link MenuContent}: the surface variant and the Base UI positioner offsets. */
|
|
15
38
|
export type MenuContentProps = WithClass<ComponentProps<typeof BaseMenu.Popup>> & {
|
|
16
39
|
/**
|
|
17
40
|
* `glass` is the Figma `Menu` on the veil (20px radius, 40px rows).
|
|
@@ -19,21 +42,58 @@ export type MenuContentProps = WithClass<ComponentProps<typeof BaseMenu.Popup>>
|
|
|
19
42
|
* (opaque fill, 18px radius, soft shadow, 36px rows).
|
|
20
43
|
* `paper` is the Paper workspace switcher: default fill, 1px default border, 14px radius,
|
|
21
44
|
* 0 12 32 shadow, 304px wide, 36px rows.
|
|
45
|
+
* @default "solid"
|
|
22
46
|
*/
|
|
23
47
|
surface?: MenuSurface;
|
|
48
|
+
/** Preferred side of the anchor the popup opens on; flips when it does not fit. */
|
|
24
49
|
side?: ComponentProps<typeof BaseMenu.Positioner>["side"];
|
|
50
|
+
/** Alignment of the popup against the anchor.
|
|
51
|
+
* @default "start" */
|
|
25
52
|
align?: ComponentProps<typeof BaseMenu.Positioner>["align"];
|
|
53
|
+
/** Gap in px between the anchor and the popup.
|
|
54
|
+
* @default 8 */
|
|
26
55
|
sideOffset?: number;
|
|
56
|
+
/** Offset in px along the alignment axis. */
|
|
27
57
|
alignOffset?: number;
|
|
28
58
|
};
|
|
29
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Radius, border, fill and shadow of the popup per {@link MenuSurface}. Width is
|
|
62
|
+
* not part of this map; `MenuContent` applies it directly (`w-55` glass, `w-76`
|
|
63
|
+
* paper, `w-64` solid).
|
|
64
|
+
*/
|
|
30
65
|
export const menuPopupClass = {
|
|
31
66
|
glass: "rounded-xl veil-overlay",
|
|
32
67
|
solid: "rounded-[18px] bg-glass-fill-solid shadow-popover",
|
|
33
68
|
paper: "rounded-[14px] border border-border-default bg-bg-default shadow-[0_12px_32px_#0A142424]",
|
|
34
69
|
} as const;
|
|
35
70
|
|
|
36
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Figma `Menus and popovers` (88:495): `Menu`, `Account menu` and `Workspace menu`.
|
|
73
|
+
*
|
|
74
|
+
* The popup portals to `document.body` through `BaseMenu.Portal`, so it escapes
|
|
75
|
+
* any `overflow: hidden` ancestor and always paints above the page at `z-50`.
|
|
76
|
+
* Base UI handles keyboard navigation, focus return to the trigger and dismissal;
|
|
77
|
+
* the surface prop only changes the look. Its width is capped at the viewport
|
|
78
|
+
* minus 32px so the 304px `paper` menu still fits a 320px screen.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* <Menu>
|
|
83
|
+
* <MenuTrigger render={<Button variant="solid" />}>Account</MenuTrigger>
|
|
84
|
+
* <MenuContent align="end">
|
|
85
|
+
* <MenuLabel>Workspaces</MenuLabel>
|
|
86
|
+
* <MenuItem icon={<GearSix />} trailing="⌘," onClick={openSettings}>
|
|
87
|
+
* Settings
|
|
88
|
+
* </MenuItem>
|
|
89
|
+
* <MenuSeparator />
|
|
90
|
+
* <MenuItem icon={<Trash />} danger onClick={remove}>
|
|
91
|
+
* Delete
|
|
92
|
+
* </MenuItem>
|
|
93
|
+
* </MenuContent>
|
|
94
|
+
* </Menu>
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
37
97
|
export function MenuContent({
|
|
38
98
|
surface = "solid",
|
|
39
99
|
side,
|
|
@@ -72,18 +132,27 @@ export function MenuContent({
|
|
|
72
132
|
);
|
|
73
133
|
}
|
|
74
134
|
|
|
135
|
+
/** Props for {@link MenuItem}: optional leading icon, description line and trailing shortcut. */
|
|
75
136
|
export type MenuItemProps = WithClass<ComponentProps<typeof BaseMenu.Item>> & {
|
|
76
137
|
/** 18px leading icon, or a 28px workspace tile. */
|
|
77
138
|
icon?: ReactNode;
|
|
78
|
-
/** Caption under the label, such as "3 members". */
|
|
139
|
+
/** Caption under the label, such as "3 members". Grows the row to 48px. */
|
|
79
140
|
description?: ReactNode;
|
|
80
141
|
/** Right side: a shortcut such as "⌘C", or a check. */
|
|
81
142
|
trailing?: ReactNode;
|
|
143
|
+
/** Paints the label and icon in `text-text-danger`; pair it with `onClick` or `disabled` for a destructive command. */
|
|
82
144
|
danger?: boolean;
|
|
83
145
|
/** Current choice drawn on the brand tint (Paper workspace switcher). */
|
|
84
146
|
selected?: boolean;
|
|
85
147
|
};
|
|
86
148
|
|
|
149
|
+
/**
|
|
150
|
+
* One row of a {@link MenuContent}: icon, label, optional description and trailing
|
|
151
|
+
* shortcut. It is a Base UI `Menu.Item`, so it closes the menu on click, is
|
|
152
|
+
* disabled through `disabled` and highlighted with the arrow keys.
|
|
153
|
+
*
|
|
154
|
+
* Part of {@link MenuContent}.
|
|
155
|
+
*/
|
|
87
156
|
export function MenuItem({
|
|
88
157
|
icon,
|
|
89
158
|
description,
|
|
@@ -131,6 +200,17 @@ export function MenuItem({
|
|
|
131
200
|
);
|
|
132
201
|
}
|
|
133
202
|
|
|
203
|
+
/**
|
|
204
|
+
* A 1px rule between groups of items, drawn full width inside the popup's own
|
|
205
|
+
* `p-1.5` inset. Its wrapper adds `px-1.5` on glass and `px-2.5` on solid and
|
|
206
|
+
* paper, so the line starts 12px from the popup edge on glass and 16px on the
|
|
207
|
+
* others: on solid and paper it lines up with the row padding (`px-2.5`), while
|
|
208
|
+
* the glass rows are inset further (`px-3`) and the rule runs wider than they do.
|
|
209
|
+
*
|
|
210
|
+
* It renders as a Base UI `Menu.Separator`, so it is hidden from the keyboard.
|
|
211
|
+
*
|
|
212
|
+
* Part of {@link MenuContent}.
|
|
213
|
+
*/
|
|
134
214
|
export function MenuSeparator({ className, ...props }: WithClass<ComponentProps<typeof BaseMenu.Separator>>) {
|
|
135
215
|
const surface = useContext(SurfaceContext);
|
|
136
216
|
return (
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { Copy, FileText, X } from "@phosphor-icons/react";
|
|
2
|
-
import {
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
3
|
|
|
4
|
+
import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard";
|
|
4
5
|
import { cn } from "../lib/cn";
|
|
5
6
|
import { IconButton } from "./icon-button";
|
|
6
7
|
import { LiquidGlass } from "./liquid-glass";
|
|
7
8
|
|
|
8
9
|
/** Figma `Date separator` (Message extras 109:667): caption between two 10% ink lines. */
|
|
9
|
-
|
|
10
|
+
// prettier-ignore
|
|
11
|
+
export function DateSeparator({ children, className }: {
|
|
12
|
+
/** Caption text between the rules, such as "Today" or "Mar 4". */
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
/** Extra classes on the row, merged after the built-in ones. */
|
|
15
|
+
className?: string;
|
|
16
|
+
}) {
|
|
10
17
|
return (
|
|
11
18
|
<div role="separator" className={cn("flex items-center gap-3", className)}>
|
|
12
19
|
<span className="h-px flex-1 bg-overlay-ink-10" />
|
|
@@ -16,17 +23,43 @@ export function DateSeparator({ children, className }: { children: ReactNode; cl
|
|
|
16
23
|
);
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
/** One button of a {@link MessageActions} pill. */
|
|
27
|
+
export type MessageAction = {
|
|
28
|
+
/** Accessible label of the button; also its React key, so keep labels unique within a pill. */
|
|
29
|
+
label: string;
|
|
30
|
+
/** 16px glyph; sized by the pill's `[&>svg]:size-4`. */
|
|
31
|
+
icon: ReactNode;
|
|
32
|
+
/** Fires on click. */
|
|
33
|
+
onClick?: () => void;
|
|
34
|
+
};
|
|
20
35
|
|
|
21
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Figma `Message actions` (109:667): a pill of 30px icon buttons with 16px icons, solid or regular glass.
|
|
38
|
+
*
|
|
39
|
+
* Hovering a button fills a circle behind the glyph; each button carries its
|
|
40
|
+
* label as `aria-label` and the pill exposes `role="toolbar"`. With
|
|
41
|
+
* `surface="glass"` it paints a `LiquidGlass` layer and the buttons sit above it.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <MessageActions
|
|
46
|
+
* actions={[
|
|
47
|
+
* { label: "Copy", icon: <Copy />, onClick: () => copy(text) },
|
|
48
|
+
* { label: "Regenerate", icon: <ArrowsClockwise />, onClick: regenerate },
|
|
49
|
+
* ]}
|
|
50
|
+
* />
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
22
53
|
export function MessageActions({
|
|
23
54
|
actions,
|
|
24
55
|
surface = "solid",
|
|
25
56
|
className,
|
|
26
57
|
}: {
|
|
58
|
+
/** Buttons of the pill, rendered in order; each needs a unique label. */
|
|
27
59
|
actions: MessageAction[];
|
|
28
|
-
/** `solid` (default) is an opaque surface pill with a default border; `glass` is the regular glass pill. */
|
|
60
|
+
/** `solid` (default) is an opaque surface pill with a default border; `glass` is the regular glass pill. @default "solid" */
|
|
29
61
|
surface?: "solid" | "glass";
|
|
62
|
+
/** Extra classes on the pill, merged after the built-in ones. */
|
|
30
63
|
className?: string;
|
|
31
64
|
}) {
|
|
32
65
|
return (
|
|
@@ -54,14 +87,20 @@ export function MessageActions({
|
|
|
54
87
|
);
|
|
55
88
|
}
|
|
56
89
|
|
|
90
|
+
/** Props for {@link Attachment}: what the file is, how it looks and whether it can be removed. */
|
|
57
91
|
export type AttachmentProps = {
|
|
92
|
+
/** File name, truncated to one line. */
|
|
58
93
|
name: ReactNode;
|
|
59
94
|
/** Caption, such as "PDF · 2.4 MB". */
|
|
60
95
|
detail?: ReactNode;
|
|
61
96
|
/** Image thumbnail URL; files show a document icon on a sky tile. */
|
|
62
97
|
thumbnail?: string;
|
|
98
|
+
/** Renders the 32px remove button when set. */
|
|
63
99
|
onRemove?: () => void;
|
|
100
|
+
/** Accessible label of the remove button.
|
|
101
|
+
* @default "Remove" */
|
|
64
102
|
removeLabel?: string;
|
|
103
|
+
/** Extra classes on the card, merged after the built-in ones. */
|
|
65
104
|
className?: string;
|
|
66
105
|
};
|
|
67
106
|
|
|
@@ -102,11 +141,19 @@ export function Attachment({ name, detail, thumbnail, onRemove, removeLabel = "R
|
|
|
102
141
|
);
|
|
103
142
|
}
|
|
104
143
|
|
|
144
|
+
/** Props for {@link CodeBlock}: the code text, its language caption and the copy-button labels. */
|
|
105
145
|
export type CodeBlockProps = Omit<ComponentProps<"div">, "className" | "children"> & {
|
|
146
|
+
/** Source text, rendered verbatim inside a `<code>` in a horizontal scroller. */
|
|
106
147
|
code: string;
|
|
148
|
+
/** Caption in the header bar, such as "tsx"; the space is kept even when empty. */
|
|
107
149
|
language?: string;
|
|
150
|
+
/** Copy-button text before the copy succeeds.
|
|
151
|
+
* @default "Copy" */
|
|
108
152
|
copyLabel?: string;
|
|
153
|
+
/** Copy-button text for 1500ms after a successful copy.
|
|
154
|
+
* @default "Copied" */
|
|
109
155
|
copiedLabel?: string;
|
|
156
|
+
/** Extra classes on the outer `div`, merged after the built-in ones. */
|
|
110
157
|
className?: string;
|
|
111
158
|
};
|
|
112
159
|
|
|
@@ -119,25 +166,14 @@ export function CodeBlock({
|
|
|
119
166
|
className,
|
|
120
167
|
...props
|
|
121
168
|
}: CodeBlockProps) {
|
|
122
|
-
const
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
if (!copied) return undefined;
|
|
125
|
-
const timer = setTimeout(() => setCopied(false), 1500);
|
|
126
|
-
return () => clearTimeout(timer);
|
|
127
|
-
}, [copied]);
|
|
128
|
-
const copy = () => {
|
|
129
|
-
navigator.clipboard?.writeText(code).then(
|
|
130
|
-
() => setCopied(true),
|
|
131
|
-
() => undefined,
|
|
132
|
-
);
|
|
133
|
-
};
|
|
169
|
+
const { copied, copy } = useCopyToClipboard(1500);
|
|
134
170
|
return (
|
|
135
171
|
<div className={cn("flex flex-col overflow-clip rounded-lg bg-surface-ink", className)} {...props}>
|
|
136
172
|
<div className="flex items-center gap-2 bg-bg-surface-6 py-2 pr-3.5 pl-3.5 text-caption text-text-on-ink">
|
|
137
173
|
<span className="min-w-0 flex-1 opacity-70">{language}</span>
|
|
138
174
|
<button
|
|
139
175
|
type="button"
|
|
140
|
-
onClick={copy}
|
|
176
|
+
onClick={() => copy(code)}
|
|
141
177
|
className="flex shrink-0 motion-press cursor-pointer items-center gap-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand"
|
|
142
178
|
>
|
|
143
179
|
<Copy aria-hidden className="size-3.5 shrink-0" />
|