@horon-ai/react 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- 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
|
@@ -4,14 +4,44 @@ import { cn } from "../lib/cn";
|
|
|
4
4
|
import { LiquidGlass } from "./liquid-glass";
|
|
5
5
|
import { Switch, type SwitchProps } from "./switch";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** A component's own prop set with `className` replaced by a plain optional string, so names never collide with the DOM attribute. */
|
|
8
|
+
type WithClass<T> = Omit<T, "className"> & {
|
|
9
|
+
/** Classes merged onto the rendered element. */
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Props for {@link SettingsList}. Other `div` props (`id`, `data-*`, handlers) land on the card.
|
|
15
|
+
*/
|
|
9
16
|
export type SettingsListProps = WithClass<ComponentProps<"div">> & {
|
|
10
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card.
|
|
19
|
+
* @default "solid"
|
|
20
|
+
*/
|
|
11
21
|
surface?: "solid" | "glass";
|
|
12
22
|
};
|
|
13
23
|
|
|
14
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Paper `Settings list` (1VD8-0): a 16px-radius card of divided rows, 20px inline padding, solid or thick glass.
|
|
26
|
+
*
|
|
27
|
+
* Put only `SettingsRow` children inside: each row draws its own bottom divider, and the card clips
|
|
28
|
+
* the corners, so grouping rows here is enough to get the divided list. Glass keeps the rows above
|
|
29
|
+
* the liquid layer.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* import { SettingsList, SettingsRow, SettingsSwitch } from "@horon-ai/react";
|
|
34
|
+
*
|
|
35
|
+
* function Example() {
|
|
36
|
+
* return (
|
|
37
|
+
* <SettingsList>
|
|
38
|
+
* <SettingsRow title="Sync on Wi-Fi" description="Only use Wi-Fi for uploads" control={<SettingsSwitch defaultChecked />} />
|
|
39
|
+
* <SettingsRow title="Version" control={<span>2.4.1</span>} />
|
|
40
|
+
* </SettingsList>
|
|
41
|
+
* );
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
15
45
|
export function SettingsList({ surface = "solid", className, children, ...props }: SettingsListProps) {
|
|
16
46
|
return (
|
|
17
47
|
<div
|
|
@@ -28,17 +58,27 @@ export function SettingsList({ surface = "solid", className, children, ...props
|
|
|
28
58
|
);
|
|
29
59
|
}
|
|
30
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Props for {@link SettingsRow}. Extra `div` props (`id`, `data-*`, handlers) land on the row.
|
|
63
|
+
*/
|
|
31
64
|
export type SettingsRowProps = Omit<ComponentProps<"div">, "className" | "title"> & {
|
|
65
|
+
/** Primary line of the row. */
|
|
32
66
|
title: ReactNode;
|
|
67
|
+
/** Secondary line under the title, in secondary text. */
|
|
33
68
|
description?: ReactNode;
|
|
34
69
|
/** Leading slot before the title, usually a `SettingsRowIcon`; the row tightens to 12px gap and 13px block padding. */
|
|
35
70
|
leading?: ReactNode;
|
|
36
71
|
/** Trailing control slot: a `Switch`, a `SettingsKeys` pair, a button. */
|
|
37
72
|
control?: ReactNode;
|
|
73
|
+
/** Classes merged onto the row. */
|
|
38
74
|
className?: string;
|
|
39
75
|
};
|
|
40
76
|
|
|
41
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* One row: title and subtitle on the left, control on the right, 1px divider except on the last row.
|
|
79
|
+
*
|
|
80
|
+
* Part of {@link SettingsList}.
|
|
81
|
+
*/
|
|
42
82
|
export function SettingsRow({ title, description, leading, control, className, ...props }: SettingsRowProps) {
|
|
43
83
|
return (
|
|
44
84
|
<div
|
|
@@ -59,7 +99,12 @@ export function SettingsRow({ title, description, leading, control, className, .
|
|
|
59
99
|
);
|
|
60
100
|
}
|
|
61
101
|
|
|
62
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Paper settings row icon tile: 32px, 9px radius, subtle fill, 16px glyph centered. Decorative,
|
|
104
|
+
* so it is hidden from assistive technology; the row title carries the meaning.
|
|
105
|
+
*
|
|
106
|
+
* Part of {@link SettingsList}.
|
|
107
|
+
*/
|
|
63
108
|
export function SettingsRowIcon({ className, ...props }: WithClass<ComponentProps<"span">>) {
|
|
64
109
|
return (
|
|
65
110
|
<span
|
|
@@ -73,12 +118,21 @@ export function SettingsRowIcon({ className, ...props }: WithClass<ComponentProp
|
|
|
73
118
|
);
|
|
74
119
|
}
|
|
75
120
|
|
|
76
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* A `Switch` pre-wired for a row's control slot (md, 44x26).
|
|
123
|
+
*
|
|
124
|
+
* Part of {@link SettingsList}.
|
|
125
|
+
*/
|
|
77
126
|
export function SettingsSwitch(props: SwitchProps) {
|
|
78
127
|
return <Switch size="md" {...props} />;
|
|
79
128
|
}
|
|
80
129
|
|
|
81
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* One keycap: 26px high, 6px radius, subtle fill with a 1px hairline (replaces `Kbd` in settings rows).
|
|
132
|
+
* It renders a `<kbd>`, so screen readers announce it as keyboard input rather than body text.
|
|
133
|
+
*
|
|
134
|
+
* Part of {@link SettingsList}.
|
|
135
|
+
*/
|
|
82
136
|
export function SettingsKey({ className, children, ...props }: WithClass<ComponentProps<"kbd">>) {
|
|
83
137
|
return (
|
|
84
138
|
<kbd
|
|
@@ -93,7 +147,11 @@ export function SettingsKey({ className, children, ...props }: WithClass<Compone
|
|
|
93
147
|
);
|
|
94
148
|
}
|
|
95
149
|
|
|
96
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* A keycap pair or chord, 4px apart; a plain flex wrapper, so it takes its children as-is.
|
|
152
|
+
*
|
|
153
|
+
* Part of {@link SettingsList}.
|
|
154
|
+
*/
|
|
97
155
|
export function SettingsKeys({ className, ...props }: WithClass<ComponentProps<"span">>) {
|
|
98
156
|
return <span className={cn("inline-flex items-center gap-1", className)} {...props} />;
|
|
99
157
|
}
|
|
@@ -15,28 +15,57 @@ import {
|
|
|
15
15
|
import { cn } from "../lib/cn";
|
|
16
16
|
import { LiquidGlass } from "./liquid-glass";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
type
|
|
18
|
+
/** A `div` prop set with an optional `className`, used by the parts that render a plain `div`. */
|
|
19
|
+
type DivProps = Omit<ComponentProps<"div">, "className"> & {
|
|
20
|
+
/** Classes merged onto the element. */
|
|
21
|
+
className?: string;
|
|
22
|
+
};
|
|
23
|
+
/** A `button` prop set with an optional `className`, used by the parts that render a `button`. */
|
|
24
|
+
type ButtonProps = Omit<ComponentProps<"button">, "className"> & {
|
|
25
|
+
/** Classes merged onto the button. */
|
|
26
|
+
className?: string;
|
|
27
|
+
};
|
|
20
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Props for {@link Sidebar}. Other `nav` props (`aria-label`, `id`, handlers) land on the column.
|
|
31
|
+
*/
|
|
21
32
|
export type SidebarProps = Omit<ComponentProps<"nav">, "className"> & {
|
|
33
|
+
/** Classes merged onto the column. */
|
|
22
34
|
className?: string;
|
|
23
35
|
/**
|
|
24
36
|
* `glass` (default) is the thick-glass column. `photo` is the Paper app sidebar: the Horon photo
|
|
25
37
|
* on #EEF3F8 under a 55% white veil, with the thick glass kept for dark mode. `plain` paints no
|
|
26
38
|
* fill or glass, so the column takes the color of the layout behind it, divided by a 1px border.
|
|
39
|
+
* @default "glass"
|
|
27
40
|
*/
|
|
28
41
|
surface?: "glass" | "photo" | "plain";
|
|
29
42
|
/**
|
|
30
43
|
* Drag the right edge to change the width, from the `md` breakpoint (768px) up.
|
|
31
44
|
* Below `md` the handle is hidden and the sidebar keeps its fixed 280px width.
|
|
45
|
+
* @default false
|
|
32
46
|
*/
|
|
33
47
|
resizable?: boolean;
|
|
34
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Starting width in px when `resizable`. Double-clicking the handle returns to it.
|
|
50
|
+
* @default 280
|
|
51
|
+
*/
|
|
35
52
|
defaultWidth?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Narrowest width in px the handle can drag to.
|
|
55
|
+
* @default 240
|
|
56
|
+
*/
|
|
36
57
|
minWidth?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Widest width in px the handle can drag to.
|
|
60
|
+
* @default 400
|
|
61
|
+
*/
|
|
37
62
|
maxWidth?: number;
|
|
38
63
|
/** Called with the clamped width as it changes, during a drag too. */
|
|
39
64
|
onWidthChange?: (width: number) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Accessible name of the resize handle.
|
|
67
|
+
* @default "Resize sidebar"
|
|
68
|
+
*/
|
|
40
69
|
resizeLabel?: string;
|
|
41
70
|
};
|
|
42
71
|
|
|
@@ -49,6 +78,27 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
49
78
|
/**
|
|
50
79
|
* Figma `Sidebar` (88:848): a 280px thick-glass column with a 1px right border, 16/12/12px padding.
|
|
51
80
|
* With `resizable`, an 8px handle on the right edge drags the width between `minWidth` and `maxWidth`.
|
|
81
|
+
*
|
|
82
|
+
* Width is uncontrolled: the column keeps its own state, starts at `defaultWidth` and reports every
|
|
83
|
+
* clamped change through `onWidthChange`. The handle is keyboard operable (arrow keys step 16px,
|
|
84
|
+
* Home/End jump to the limits) and sits behind `md`; the column never grows past a narrower parent.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* import { Sidebar, SidebarGroup, SidebarHeader, SidebarItem } from "@horon-ai/react";
|
|
89
|
+
*
|
|
90
|
+
* function Example() {
|
|
91
|
+
* return (
|
|
92
|
+
* <Sidebar resizable onWidthChange={(width) => console.log(width)}>
|
|
93
|
+
* <SidebarHeader initial="H" name="Horon" onCollapse={() => {}} />
|
|
94
|
+
* <SidebarGroup label="Workspace">
|
|
95
|
+
* <SidebarItem current>Home</SidebarItem>
|
|
96
|
+
* <SidebarItem count={3}>Inbox</SidebarItem>
|
|
97
|
+
* </SidebarGroup>
|
|
98
|
+
* </Sidebar>
|
|
99
|
+
* );
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
52
102
|
*/
|
|
53
103
|
export function Sidebar({
|
|
54
104
|
className,
|
|
@@ -65,7 +115,12 @@ export function Sidebar({
|
|
|
65
115
|
}: SidebarProps) {
|
|
66
116
|
const [width, setWidth] = useState(() => clamp(defaultWidth, minWidth, maxWidth));
|
|
67
117
|
const [dragging, setDragging] = useState(false);
|
|
68
|
-
const drag = useRef<{
|
|
118
|
+
const drag = useRef<{
|
|
119
|
+
/** Pointer x (px, client coordinate) where the drag started. */
|
|
120
|
+
x: number;
|
|
121
|
+
/** Column width (px) at the moment the drag started, so every move applies to a fixed base. */
|
|
122
|
+
width: number;
|
|
123
|
+
} | null>(null);
|
|
69
124
|
|
|
70
125
|
const resize = (next: number) => {
|
|
71
126
|
const clamped = clamp(next, minWidth, maxWidth);
|
|
@@ -165,22 +220,46 @@ export function Sidebar({
|
|
|
165
220
|
);
|
|
166
221
|
}
|
|
167
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Props for {@link SidebarHeader}. Renders the workspace switch and an optional collapse button in
|
|
225
|
+
* one 36px row.
|
|
226
|
+
*/
|
|
168
227
|
export type SidebarHeaderProps = {
|
|
169
228
|
/** Workspace initial shown on the 28px ink tile. */
|
|
170
229
|
initial: ReactNode;
|
|
230
|
+
/** Workspace name next to the tile; truncated when the column is narrow. */
|
|
171
231
|
name: ReactNode;
|
|
172
232
|
/** Props for the workspace switch button, such as a `MenuTrigger` render target. */
|
|
173
233
|
switchProps?: ButtonProps;
|
|
174
234
|
/** Replaces the default switch, such as a `WorkspaceMenu` whose trigger is a `SidebarWorkspaceSwitch`. */
|
|
175
235
|
switchSlot?: ReactNode;
|
|
236
|
+
/** Shows the collapse button when set; it receives the click and the caller hides the column. */
|
|
176
237
|
onCollapse?: () => void;
|
|
238
|
+
/**
|
|
239
|
+
* Accessible name of the collapse button.
|
|
240
|
+
* @default "Collapse sidebar"
|
|
241
|
+
*/
|
|
177
242
|
collapseLabel?: string;
|
|
243
|
+
/** Classes merged onto the header row. */
|
|
178
244
|
className?: string;
|
|
179
245
|
};
|
|
180
246
|
|
|
181
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Props for {@link SidebarWorkspaceSwitch}: every button prop except `name`, which is the visible
|
|
249
|
+
* workspace label instead.
|
|
250
|
+
*/
|
|
251
|
+
export type SidebarWorkspaceSwitchProps = Omit<ButtonProps, "name"> & {
|
|
252
|
+
/** Workspace initial on the 28px ink tile. */
|
|
253
|
+
initial: ReactNode;
|
|
254
|
+
/** Workspace name, truncated in the row. */
|
|
255
|
+
name: ReactNode;
|
|
256
|
+
};
|
|
182
257
|
|
|
183
|
-
/**
|
|
258
|
+
/**
|
|
259
|
+
* The workspace switch button: 28px ink tile, name and up-down caret. Highlights while its menu is open.
|
|
260
|
+
*
|
|
261
|
+
* Part of {@link Sidebar}.
|
|
262
|
+
*/
|
|
184
263
|
export function SidebarWorkspaceSwitch({ initial, name, className, ...props }: SidebarWorkspaceSwitchProps) {
|
|
185
264
|
return (
|
|
186
265
|
<button
|
|
@@ -201,7 +280,12 @@ export function SidebarWorkspaceSwitch({ initial, name, className, ...props }: S
|
|
|
201
280
|
);
|
|
202
281
|
}
|
|
203
282
|
|
|
204
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* Header: workspace switch (tile, name, up-down caret) and an optional 28px collapse button. 10px gap below.
|
|
285
|
+
*
|
|
286
|
+
* Part of {@link Sidebar}. Pass `switchSlot` when the switch must open a menu; otherwise the built-in
|
|
287
|
+
* `SidebarWorkspaceSwitch` is rendered with `switchProps` spread onto it.
|
|
288
|
+
*/
|
|
205
289
|
export function SidebarHeader({
|
|
206
290
|
initial,
|
|
207
291
|
name,
|
|
@@ -228,25 +312,41 @@ export function SidebarHeader({
|
|
|
228
312
|
);
|
|
229
313
|
}
|
|
230
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Props every sidebar row shares, whichever element it renders as.
|
|
317
|
+
*/
|
|
231
318
|
type SidebarItemCommonProps = {
|
|
232
319
|
/** 18px icon, or a `SidebarProjectTile`. */
|
|
233
320
|
icon?: ReactNode;
|
|
234
321
|
/** Count on the right, such as unread items. */
|
|
235
322
|
count?: ReactNode;
|
|
236
|
-
/** Trailing 7px red dot for attention. */
|
|
323
|
+
/** Trailing 7px red dot marking the row for attention; the dot itself carries `aria-label="Needs attention"` but no role. */
|
|
237
324
|
attention?: boolean;
|
|
325
|
+
/** Marks the active row: 6% ink fill and `aria-current="page"`. */
|
|
238
326
|
current?: boolean;
|
|
327
|
+
/** Classes merged onto the row. */
|
|
239
328
|
className?: string;
|
|
240
329
|
};
|
|
241
330
|
|
|
242
|
-
/**
|
|
331
|
+
/**
|
|
332
|
+
* With `href`, the row is a real link and takes anchor props; without it, a `type="button"` button.
|
|
333
|
+
* The `current` flag adds `aria-current="page"` to either.
|
|
334
|
+
*/
|
|
243
335
|
export type SidebarItemProps =
|
|
244
|
-
| (SidebarItemCommonProps & {
|
|
245
|
-
|
|
336
|
+
| (SidebarItemCommonProps & {
|
|
337
|
+
/** Destination; setting it switches the row to an anchor. */
|
|
338
|
+
href: string;
|
|
339
|
+
} & Omit<ComponentProps<"a">, "className" | "href" | "type">)
|
|
340
|
+
| (SidebarItemCommonProps & {
|
|
341
|
+
/** Leave it out to render a button instead of a link. */
|
|
342
|
+
href?: undefined;
|
|
343
|
+
} & ButtonProps);
|
|
246
344
|
|
|
247
345
|
/**
|
|
248
346
|
* Figma sidebar `Item`: 32px row, 12px radius, 10px gap; the current item is 6% ink.
|
|
249
347
|
* Pass `href` to render an `<a>` with the same look.
|
|
348
|
+
*
|
|
349
|
+
* Part of {@link Sidebar}.
|
|
250
350
|
*/
|
|
251
351
|
export function SidebarItem({ icon, count, attention, current, className, children, ...props }: SidebarItemProps) {
|
|
252
352
|
const classes = cn(
|
|
@@ -286,11 +386,18 @@ export function SidebarItem({ icon, count, attention, current, className, childr
|
|
|
286
386
|
);
|
|
287
387
|
}
|
|
288
388
|
|
|
289
|
-
/**
|
|
389
|
+
/**
|
|
390
|
+
* A 22px rounded tile with a 14px inverse icon, for project rows. Set the fill with `className`.
|
|
391
|
+
*
|
|
392
|
+
* Part of {@link Sidebar}.
|
|
393
|
+
*/
|
|
290
394
|
export function SidebarProjectTile({
|
|
291
395
|
className,
|
|
292
396
|
...props
|
|
293
|
-
}: Omit<ComponentProps<"span">, "className"> & {
|
|
397
|
+
}: Omit<ComponentProps<"span">, "className"> & {
|
|
398
|
+
/** Classes merged onto the tile; the fill color comes from here. */
|
|
399
|
+
className?: string;
|
|
400
|
+
}) {
|
|
294
401
|
return (
|
|
295
402
|
<span
|
|
296
403
|
className={cn(
|
|
@@ -302,14 +409,29 @@ export function SidebarProjectTile({
|
|
|
302
409
|
);
|
|
303
410
|
}
|
|
304
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Props for {@link SidebarGroup}.
|
|
414
|
+
*/
|
|
305
415
|
export type SidebarGroupProps = {
|
|
416
|
+
/** Group caption; also the collapsible trigger's label. */
|
|
306
417
|
label: ReactNode;
|
|
418
|
+
/**
|
|
419
|
+
* Whether the group starts expanded.
|
|
420
|
+
* @default true
|
|
421
|
+
*/
|
|
307
422
|
defaultOpen?: boolean;
|
|
423
|
+
/** Rows or projects inside the group. */
|
|
308
424
|
children: ReactNode;
|
|
425
|
+
/** Classes merged onto the group. */
|
|
309
426
|
className?: string;
|
|
310
427
|
};
|
|
311
428
|
|
|
312
|
-
/**
|
|
429
|
+
/**
|
|
430
|
+
* A collapsible group: 28px label in secondary text with a 14px caret, 14px above.
|
|
431
|
+
*
|
|
432
|
+
* Part of {@link Sidebar}. The group is uncontrolled after mount; the caret rotates from pointing
|
|
433
|
+
* right (collapsed) to down (open) and the panel height animates.
|
|
434
|
+
*/
|
|
313
435
|
export function SidebarGroup({ label, defaultOpen = true, children, className }: SidebarGroupProps) {
|
|
314
436
|
return (
|
|
315
437
|
<Collapsible.Root defaultOpen={defaultOpen} className={cn("relative mt-3.5 flex flex-col gap-0.5", className)}>
|
|
@@ -340,8 +462,19 @@ function branchesPath(count: number): string {
|
|
|
340
462
|
return [trunk, ...branches].join("");
|
|
341
463
|
}
|
|
342
464
|
|
|
343
|
-
/**
|
|
344
|
-
|
|
465
|
+
/**
|
|
466
|
+
* Figma `Tree`: nested sidebar items with 16px icons, 20px in, joined to the parent by curved connectors.
|
|
467
|
+
*
|
|
468
|
+
* Part of {@link Sidebar}. The connector is one SVG path computed from the child count, so it
|
|
469
|
+
* assumes 32px rows with a 2px gap; pass the child rows directly rather than wrapping them.
|
|
470
|
+
*/
|
|
471
|
+
// prettier-ignore
|
|
472
|
+
export function SidebarTree({ children, className }: {
|
|
473
|
+
/** Child rows; their count and order decide the connector shape. */
|
|
474
|
+
children: ReactNode;
|
|
475
|
+
/** Classes merged onto the tree wrapper. */
|
|
476
|
+
className?: string;
|
|
477
|
+
}) {
|
|
345
478
|
const count = Children.count(children);
|
|
346
479
|
const height = ROW / 2 + (count - 1) * (ROW + GAP) + 0.75;
|
|
347
480
|
return (
|
|
@@ -361,18 +494,33 @@ export function SidebarTree({ children, className }: { children: ReactNode; clas
|
|
|
361
494
|
);
|
|
362
495
|
}
|
|
363
496
|
|
|
364
|
-
/**
|
|
497
|
+
/**
|
|
498
|
+
* Pushes the account card to the bottom.
|
|
499
|
+
*
|
|
500
|
+
* Part of {@link Sidebar}; it grows to fill the remaining column height.
|
|
501
|
+
*/
|
|
365
502
|
export function SidebarSpacer() {
|
|
366
503
|
return <div aria-hidden className="min-h-px flex-1" />;
|
|
367
504
|
}
|
|
368
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Props for {@link SidebarAccount}: every button prop plus the identity lines it shows.
|
|
508
|
+
*/
|
|
369
509
|
export type SidebarAccountProps = ButtonProps & {
|
|
510
|
+
/** Initials on the 36px sky avatar tile. */
|
|
370
511
|
initials: ReactNode;
|
|
512
|
+
/** Account name. */
|
|
371
513
|
name: ReactNode;
|
|
514
|
+
/** Email line under the name. */
|
|
372
515
|
email?: ReactNode;
|
|
373
516
|
};
|
|
374
517
|
|
|
375
|
-
/**
|
|
518
|
+
/**
|
|
519
|
+
* Figma sidebar `Account`: a 54px row with a 36px sky avatar tile and an up-down caret.
|
|
520
|
+
*
|
|
521
|
+
* Part of {@link Sidebar}. The row is a button, so pair it with a menu when it should open an
|
|
522
|
+
* account switcher.
|
|
523
|
+
*/
|
|
376
524
|
export function SidebarAccount({ initials, name, email, className, ...props }: SidebarAccountProps) {
|
|
377
525
|
return (
|
|
378
526
|
<button
|
|
@@ -396,13 +544,24 @@ export function SidebarAccount({ initials, name, email, className, ...props }: S
|
|
|
396
544
|
);
|
|
397
545
|
}
|
|
398
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Props for {@link SidebarProject}.
|
|
549
|
+
*/
|
|
399
550
|
export type SidebarProjectProps = {
|
|
400
551
|
/** A `SidebarProjectTile`. */
|
|
401
552
|
icon: ReactNode;
|
|
553
|
+
/** Project name; truncated in the row. */
|
|
402
554
|
label: ReactNode;
|
|
555
|
+
/** Marks the active project: 6% ink row and `aria-current="page"`. */
|
|
403
556
|
current?: boolean;
|
|
557
|
+
/**
|
|
558
|
+
* Whether the project starts expanded in uncontrolled mode.
|
|
559
|
+
* @default false
|
|
560
|
+
*/
|
|
404
561
|
defaultOpen?: boolean;
|
|
562
|
+
/** Expanded state in controlled mode; pass `onOpenChange` with it. */
|
|
405
563
|
open?: boolean;
|
|
564
|
+
/** Fires with the next expanded state when the row is toggled. */
|
|
406
565
|
onOpenChange?: (open: boolean) => void;
|
|
407
566
|
/**
|
|
408
567
|
* The project's "More" control, such as a `MenuTrigger` whose render target is
|
|
@@ -411,6 +570,7 @@ export type SidebarProjectProps = {
|
|
|
411
570
|
actions?: ReactNode;
|
|
412
571
|
/** Chats and documents in the project, shown in a `SidebarTree` when open. */
|
|
413
572
|
children?: ReactNode;
|
|
573
|
+
/** Classes merged onto the project block. */
|
|
414
574
|
className?: string;
|
|
415
575
|
};
|
|
416
576
|
|
|
@@ -418,6 +578,10 @@ export type SidebarProjectProps = {
|
|
|
418
578
|
* Figma sidebar project row (88:848, Menu=Project 314:12407): tile, name and a
|
|
419
579
|
* 14px chevron (down when open). An open project shows the 16px "More" button in
|
|
420
580
|
* place of the chevron, on a 6% ink row; a collapsed project keeps the chevron.
|
|
581
|
+
*
|
|
582
|
+
* Part of {@link Sidebar}. The expanded state works uncontrolled (`defaultOpen`) or controlled
|
|
583
|
+
* (`open` + `onOpenChange`). A project without children renders no panel and no chevron; the
|
|
584
|
+
* trigger still toggles the open state, and any `actions` stay visible.
|
|
421
585
|
*/
|
|
422
586
|
export function SidebarProject({
|
|
423
587
|
icon,
|
|
@@ -481,9 +645,23 @@ export function SidebarProject({
|
|
|
481
645
|
);
|
|
482
646
|
}
|
|
483
647
|
|
|
484
|
-
|
|
648
|
+
/**
|
|
649
|
+
* Props for {@link SidebarProjectMore}: every button prop plus the accessible label.
|
|
650
|
+
*/
|
|
651
|
+
export type SidebarProjectMoreProps = ButtonProps & {
|
|
652
|
+
/**
|
|
653
|
+
* Accessible name of the button.
|
|
654
|
+
* @default "Project actions"
|
|
655
|
+
*/
|
|
656
|
+
label?: string;
|
|
657
|
+
};
|
|
485
658
|
|
|
486
|
-
/**
|
|
659
|
+
/**
|
|
660
|
+
* The 16px "More" button of a project row.
|
|
661
|
+
*
|
|
662
|
+
* Part of {@link Sidebar}; its tap target is widened to 44px by a pseudo-element so the small glyph
|
|
663
|
+
* stays easy to hit.
|
|
664
|
+
*/
|
|
487
665
|
export function SidebarProjectMore({ label = "Project actions", className, ...props }: SidebarProjectMoreProps) {
|
|
488
666
|
return (
|
|
489
667
|
<button
|
|
@@ -500,15 +678,24 @@ export function SidebarProjectMore({ label = "Project actions", className, ...pr
|
|
|
500
678
|
);
|
|
501
679
|
}
|
|
502
680
|
|
|
503
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Props for {@link SidebarAddButton}: every button prop plus the trailing icon.
|
|
683
|
+
*/
|
|
684
|
+
export type SidebarAddButtonProps = ButtonProps & {
|
|
685
|
+
/** Icon after the label; defaults to a 16px plus circle. @default a 16px plus-circle (`PlusCircle`) icon */
|
|
686
|
+
icon?: ReactNode;
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
/** Default trailing icon of {@link SidebarAddButton}. */
|
|
690
|
+
const addIcon = <PlusCircle />;
|
|
504
691
|
|
|
505
692
|
/**
|
|
506
693
|
* Figma `Add project`: a 36px button with a 1px inside dashed border (4/3 dash,
|
|
507
694
|
* 20% ink), label then a 16px plus-circle, 12px above. The border is an SVG rect
|
|
508
695
|
* so the dash matches; its 2px stroke is clipped to the inner 1px.
|
|
696
|
+
*
|
|
697
|
+
* Part of {@link Sidebar}.
|
|
509
698
|
*/
|
|
510
|
-
const addIcon = <PlusCircle />;
|
|
511
|
-
|
|
512
699
|
export function SidebarAddButton({ icon = addIcon, className, children, ...props }: SidebarAddButtonProps) {
|
|
513
700
|
return (
|
|
514
701
|
<button
|
|
@@ -539,4 +726,8 @@ export function SidebarAddButton({ icon = addIcon, className, children, ...props
|
|
|
539
726
|
);
|
|
540
727
|
}
|
|
541
728
|
|
|
729
|
+
/**
|
|
730
|
+
* A `div` prop set with an optional `className`, exported so callers can type their own sidebar
|
|
731
|
+
* pieces the same way the internal parts do.
|
|
732
|
+
*/
|
|
542
733
|
export type { DivProps as SidebarDivProps };
|
|
@@ -3,13 +3,39 @@ import type { ComponentProps } from "react";
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { LiquidGlass } from "./liquid-glass";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Props for {@link Skeleton}. Other `div` props (`style`, `data-*`) land on the placeholder.
|
|
8
|
+
*/
|
|
6
9
|
export type SkeletonProps = Omit<ComponentProps<"div">, "className"> & {
|
|
7
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* `block` has the Figma 6px radius, `pill` is fully round (avatars, buttons).
|
|
12
|
+
* @default "block"
|
|
13
|
+
*/
|
|
8
14
|
shape?: "block" | "pill";
|
|
15
|
+
/** Classes that give the placeholder its size, such as `h-2 w-40`. */
|
|
9
16
|
className?: string;
|
|
10
17
|
};
|
|
11
18
|
|
|
12
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* A 7% ink placeholder; size it with classes.
|
|
21
|
+
*
|
|
22
|
+
* The block is `aria-hidden` and shimmers with a translucent highlight, so it shows layout while
|
|
23
|
+
* content loads without being read out.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* import { Skeleton } from "@horon-ai/react";
|
|
28
|
+
*
|
|
29
|
+
* function Example() {
|
|
30
|
+
* return (
|
|
31
|
+
* <div className="flex items-center gap-3">
|
|
32
|
+
* <Skeleton shape="pill" className="size-9" />
|
|
33
|
+
* <Skeleton className="h-3 w-48" />
|
|
34
|
+
* </div>
|
|
35
|
+
* );
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
13
39
|
export function Skeleton({ shape = "block", className, ...props }: SkeletonProps) {
|
|
14
40
|
return (
|
|
15
41
|
<div
|
|
@@ -24,14 +50,45 @@ export function Skeleton({ shape = "block", className, ...props }: SkeletonProps
|
|
|
24
50
|
);
|
|
25
51
|
}
|
|
26
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Props for {@link SkeletonCard}.
|
|
55
|
+
*/
|
|
27
56
|
export type SkeletonCardProps = {
|
|
57
|
+
/**
|
|
58
|
+
* Placeholder arrangement: `row` an avatar with two lines, `card` a heading, three lines and a
|
|
59
|
+
* button, `text` three lines only.
|
|
60
|
+
* @default "row"
|
|
61
|
+
*/
|
|
28
62
|
type?: "row" | "card" | "text";
|
|
29
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card.
|
|
65
|
+
* @default "solid"
|
|
66
|
+
*/
|
|
30
67
|
surface?: "solid" | "glass";
|
|
68
|
+
/** Classes merged onto the card. */
|
|
31
69
|
className?: string;
|
|
32
70
|
};
|
|
33
71
|
|
|
34
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Figma `Skeleton` (in Loading 109:733): Row, Card and Text placeholders on a solid or thick-glass card.
|
|
74
|
+
*
|
|
75
|
+
* Use it while a list, a detail card or a paragraph loads. The card announces itself once with
|
|
76
|
+
* `role="status"` and the label "Loading"; the individual bars stay hidden from assistive tech.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* import { SkeletonCard } from "@horon-ai/react";
|
|
81
|
+
*
|
|
82
|
+
* function Example() {
|
|
83
|
+
* return (
|
|
84
|
+
* <div className="flex flex-col gap-3">
|
|
85
|
+
* <SkeletonCard />
|
|
86
|
+
* <SkeletonCard type="text" />
|
|
87
|
+
* </div>
|
|
88
|
+
* );
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
35
92
|
export function SkeletonCard({ type = "row", surface = "solid", className }: SkeletonCardProps) {
|
|
36
93
|
return (
|
|
37
94
|
<div
|
|
@@ -3,14 +3,44 @@ import type { ComponentProps } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Props of {@link Slider}: every Base UI `Slider.Root` prop (`value`, `defaultValue`,
|
|
8
|
+
* `onValueChange`, `min`, `max`, `step`, `disabled`, `orientation`) plus the two overrides below.
|
|
9
|
+
*/
|
|
6
10
|
export type SliderProps = Omit<ComponentProps<typeof BaseSlider.Root>, "className" | "children"> & {
|
|
11
|
+
/** Extra classes for the root, which is 220px wide (`w-55`) and caps at its container by default. */
|
|
7
12
|
className?: string;
|
|
13
|
+
/** Accessible name of the thumb, the only focusable part of the control. */
|
|
8
14
|
"aria-label"?: string;
|
|
9
15
|
};
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
18
|
* Paper settings slider (Voice / Speed): a 20px control, 6px track at 8% ink with an ink fill,
|
|
13
19
|
* and an 18px white thumb with a 1px hairline and a 0 1 3 12% shadow. Width comes from the caller.
|
|
20
|
+
*
|
|
21
|
+
* Wraps Base UI's slider: the whole 20px row is draggable and clickable, the thumb is the
|
|
22
|
+
* focusable element (focus ring on `text-brand`, it scales up on hover and press), and a
|
|
23
|
+
* disabled slider dims to 40% with a not-allowed cursor.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* import { useState } from "react";
|
|
28
|
+
* import { Slider } from "@horon-ai/react";
|
|
29
|
+
*
|
|
30
|
+
* function VoiceSpeed() {
|
|
31
|
+
* const [speed, setSpeed] = useState(1);
|
|
32
|
+
* return (
|
|
33
|
+
* <Slider
|
|
34
|
+
* aria-label="Voice speed"
|
|
35
|
+
* min={0.5}
|
|
36
|
+
* max={2}
|
|
37
|
+
* step={0.1}
|
|
38
|
+
* value={speed}
|
|
39
|
+
* onValueChange={(next) => setSpeed(next as number)}
|
|
40
|
+
* />
|
|
41
|
+
* );
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
14
44
|
*/
|
|
15
45
|
export function Slider({ className, "aria-label": ariaLabel, ...props }: SliderProps) {
|
|
16
46
|
return (
|