@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.
- package/package.json +2 -2
- package/src/components/agent-activity.tsx +75 -7
- package/src/components/agent-lane.tsx +32 -3
- package/src/components/agent-lanes-timeline.tsx +85 -7
- package/src/components/agent-panel.tsx +46 -4
- package/src/components/agent-steering-panel.tsx +53 -3
- 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 +86 -7
- 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 +65 -1
- 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 +38 -1
- package/src/components/flue-background.tsx +88 -4
- 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 +28 -2
- 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 +50 -4
- 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 +57 -2
- 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 +47 -2
- 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 +87 -4
- 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/use-active-indicator.ts +44 -1
- package/src/hooks/use-color-scheme.ts +24 -1
- package/src/hooks/use-copy-to-clipboard.ts +23 -0
- package/src/hooks/use-device.ts +29 -1
- package/src/hooks/use-hotkey.ts +21 -0
- package/src/hooks/use-media-query.ts +18 -0
- package/src/hooks/use-platform.ts +30 -1
- package/src/hooks/use-reduced-motion.ts +18 -1
- package/src/index.ts +31 -0
- package/src/lib/cn.ts +13 -1
- package/src/lib/cursor-ghost.ts +16 -6
- package/src/lib/platform.ts +71 -4
package/src/components/kbd.tsx
CHANGED
|
@@ -5,13 +5,16 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { formatShortcut } from "../lib/platform";
|
|
6
6
|
import type { ControlSize } from "../lib/size";
|
|
7
7
|
|
|
8
|
+
/** Height rung of the chip, shared with the other sized controls: `sm`, `md`, `lg`. */
|
|
8
9
|
export type KbdSize = ControlSize;
|
|
9
10
|
|
|
11
|
+
/** Props of {@link Kbd}: the native `kbd` attributes plus the chip size. */
|
|
10
12
|
export type KbdProps = Omit<ComponentProps<"kbd">, "className"> & {
|
|
11
|
-
/** Chip size: `sm` tighter caption, `md` the Command palette chip (default), `lg` label-sm with more padding. */
|
|
13
|
+
/** Chip size: `sm` tighter caption, `md` the Command palette chip (default), `lg` label-sm with more padding. @default "md" */
|
|
12
14
|
size?: KbdSize;
|
|
13
15
|
/** Shortcut such as `Mod+Enter`, drawn with the OS's key labels (`⌘↵` / `Ctrl+Enter`) instead of children. */
|
|
14
16
|
keys?: string;
|
|
17
|
+
/** Extra classes for the `kbd` element. */
|
|
15
18
|
className?: string;
|
|
16
19
|
};
|
|
17
20
|
|
|
@@ -22,7 +25,30 @@ const sizeClass: Record<KbdSize, string> = {
|
|
|
22
25
|
lg: "h-6 gap-1 px-2 text-label-sm [&_svg]:size-3.5",
|
|
23
26
|
};
|
|
24
27
|
|
|
25
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Figma `Kbd` (Command palette 110:676): a 6% ink chip with caption text.
|
|
30
|
+
*
|
|
31
|
+
* A `<kbd>` for a keyboard shortcut: inline-flex, `rounded-xs`, secondary text, centred on a trimmed
|
|
32
|
+
* line box so the glyph sits optically centred in the chip. Pass `keys` as a `+`-joined shortcut such as
|
|
33
|
+
* `Mod+Enter` and the chip draws the OS's labels — `⌘↵` on macOS and iOS, `Ctrl+Enter` on Windows, Linux
|
|
34
|
+
* and Android, and the Mac symbols whenever the OS is still unknown (server render, first hydration pass).
|
|
35
|
+
* Without `keys` it renders `children`, so several chips can be composed by hand
|
|
36
|
+
* (`<Kbd>⌘</Kbd><Kbd>K</Kbd>`). Sizes are 16/20/24px tall, and any icon inside is resized with the chip
|
|
37
|
+
* (`2.5`/`3`/`3.5`), so pass a bare Phosphor icon or glyph.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* import { Kbd } from "@horon-ai/react";
|
|
42
|
+
*
|
|
43
|
+
* function Hint() {
|
|
44
|
+
* return (
|
|
45
|
+
* <span className="flex items-center gap-1 text-caption">
|
|
46
|
+
* Search <Kbd keys="Mod+K" />
|
|
47
|
+
* </span>
|
|
48
|
+
* );
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
26
52
|
export function Kbd({ size = "md", keys, className, children, ...props }: KbdProps) {
|
|
27
53
|
const { os } = usePlatform();
|
|
28
54
|
return (
|
|
@@ -5,32 +5,51 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { PanelPillTabs } from "./bot-instructions-panel";
|
|
6
6
|
import { Spinner } from "./spinner";
|
|
7
7
|
|
|
8
|
+
/** One pill tab of {@link KnowledgeBasePanel}, such as "All" or "Documents". */
|
|
8
9
|
export type KnowledgeBaseTab = {
|
|
10
|
+
/** Stable identifier; it is the tab's value and React key. */
|
|
9
11
|
id: string;
|
|
12
|
+
/** Tab text. */
|
|
10
13
|
label: ReactNode;
|
|
14
|
+
/** Optional count drawn in caption colour right of the label. */
|
|
11
15
|
count?: number;
|
|
12
16
|
};
|
|
13
17
|
|
|
18
|
+
/** Props for {@link KnowledgeBasePanel}: header, tabs, indexing state and the dropzone handler. */
|
|
14
19
|
export type KnowledgeBasePanelProps = Omit<ComponentProps<"section">, "className" | "title" | "onChange"> & {
|
|
20
|
+
/** Panel heading, rendered as the `h3`. */
|
|
15
21
|
title: ReactNode;
|
|
16
22
|
/** The 12/18 line under the title: sources, chunks, bots. */
|
|
17
23
|
meta?: ReactNode;
|
|
24
|
+
/** Pills of the tab tray, in display order. */
|
|
18
25
|
tabs: KnowledgeBaseTab[];
|
|
26
|
+
/** Controlled selected tab id. Leave unset to let the component hold the selection itself. */
|
|
19
27
|
value?: string;
|
|
28
|
+
/** Initial selected tab when uncontrolled. Falls back to the first tab. */
|
|
20
29
|
defaultValue?: string;
|
|
30
|
+
/** Fires with the new tab id when a pill is clicked. */
|
|
21
31
|
onValueChange?: (id: string) => void;
|
|
22
|
-
/** Shows the spinner and "N indexing" at the right of the tabs when above zero.
|
|
32
|
+
/** Shows the spinner and "N indexing" at the right of the tabs when above zero.
|
|
33
|
+
* @default 0 */
|
|
23
34
|
indexing?: number;
|
|
35
|
+
/** Fires when the round settings button is clicked; the button always renders. */
|
|
24
36
|
onSettings?: () => void;
|
|
37
|
+
/** Fires when "Add source" is clicked; the button always renders. */
|
|
25
38
|
onAddSource?: () => void;
|
|
26
39
|
/** Called with the files dropped on the dropzone or picked with Browse. Omit to hide the dropzone. */
|
|
27
40
|
onFiles?: (files: File[]) => void;
|
|
41
|
+
/** Headline inside the dropzone.
|
|
42
|
+
* @default "Drop files here, or browse" */
|
|
28
43
|
dropTitle?: ReactNode;
|
|
44
|
+
/** Supporting line under the dropzone headline.
|
|
45
|
+
* @default "PDF, DOCX, Markdown or TXT · Notion, Google Drive and websites from Add source" */
|
|
29
46
|
dropHint?: ReactNode;
|
|
30
|
-
/** Pipeline column labels shown above the list; hidden on narrow widths.
|
|
47
|
+
/** Pipeline column labels shown above the list; hidden on narrow widths.
|
|
48
|
+
* @default ["Parse", "Chunk", "Embed", "Index"] */
|
|
31
49
|
stages?: string[];
|
|
32
50
|
/** The list rows, usually `SourceRow variant="list"`. */
|
|
33
51
|
children?: ReactNode;
|
|
52
|
+
/** Extra classes on the panel `section`, merged after the built-in ones. */
|
|
34
53
|
className?: string;
|
|
35
54
|
};
|
|
36
55
|
|
|
@@ -40,6 +59,25 @@ const defaultStages = ["Parse", "Chunk", "Embed", "Index"];
|
|
|
40
59
|
* Paper `Knowledge base` (XVM-1): a 24px-radius surface panel with a header, a pill tab tray, a table head, a
|
|
41
60
|
* flat divided source list and a dashed dropzone. The selected tab pill travels in 280ms, the dropzone border and
|
|
42
61
|
* fill change in 150ms while a file is over it, and indexing progress fills are animated by the rows.
|
|
62
|
+
*
|
|
63
|
+
* The dropzone is a `<label>` wrapping a visually hidden multi-file input, so
|
|
64
|
+
* clicking it opens the file picker as well as accepting a drag; the input is
|
|
65
|
+
* cleared after each pick so the same file can be chosen twice. Passing `onFiles`
|
|
66
|
+
* is what makes the dropzone appear.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* <KnowledgeBasePanel
|
|
71
|
+
* title="Knowledge base"
|
|
72
|
+
* meta="3 sources · 128 chunks"
|
|
73
|
+
* tabs={[{ id: "all", label: "All", count: 3 }]}
|
|
74
|
+
* indexing={2}
|
|
75
|
+
* onAddSource={openSourcePicker}
|
|
76
|
+
* onFiles={upload}
|
|
77
|
+
* >
|
|
78
|
+
* <SourceRow variant="list" status="ready" icon={<FileText />} name="README.md" detail="24 chunks" />
|
|
79
|
+
* </KnowledgeBasePanel>
|
|
80
|
+
* ```
|
|
43
81
|
*/
|
|
44
82
|
export function KnowledgeBasePanel({
|
|
45
83
|
title,
|
|
@@ -18,6 +18,22 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Without backdrop-filter support, or with reduced transparency requested, the
|
|
20
20
|
* fill turns to `glass-fill-frost` and the rim stays (see `globals.css`).
|
|
21
|
+
*
|
|
22
|
+
* It takes no props and renders a single `aria-hidden` span; every visual
|
|
23
|
+
* decision comes from the host's class list, so the same element serves buttons,
|
|
24
|
+
* headers, cards and sheets.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* function GlassCard() {
|
|
29
|
+
* return (
|
|
30
|
+
* <div className="relative isolate overflow-clip rounded-xl bg-glass-regular p-4">
|
|
31
|
+
* <LiquidGlass />
|
|
32
|
+
* <span className="relative">Floating chrome over the backdrop</span>
|
|
33
|
+
* </div>
|
|
34
|
+
* );
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
21
37
|
*/
|
|
22
38
|
export function LiquidGlass() {
|
|
23
39
|
return <span aria-hidden className="liquid-layers" />;
|
|
@@ -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 (
|