@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
|
@@ -4,15 +4,39 @@ import { Children, Fragment, type ComponentProps, type ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { LiquidGlass } from "./liquid-glass";
|
|
6
6
|
|
|
7
|
+
/** Tray material behind a {@link Breadcrumb} trail: `glass` is the regular glass tray, `solid` the opaque one. */
|
|
7
8
|
export type BreadcrumbSurface = "glass" | "solid";
|
|
8
9
|
|
|
10
|
+
/** Props of {@link Breadcrumb}. */
|
|
9
11
|
export type BreadcrumbProps = Omit<ComponentProps<"nav">, "className"> & {
|
|
10
12
|
/** `glass` wraps the crumbs in a regular-glass tray, `solid` in an opaque tray. Omitted keeps them bare. */
|
|
11
13
|
surface?: BreadcrumbSurface;
|
|
14
|
+
/** Extra classes merged onto the navigation element. */
|
|
12
15
|
className?: string;
|
|
13
16
|
};
|
|
14
17
|
|
|
15
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Figma `Breadcrumb` (110:948): crumbs 6px apart with 14px carets; the last crumb is the current page.
|
|
20
|
+
*
|
|
21
|
+
* Renders a `<nav aria-label>` (default `"Breadcrumb"`) holding an ordered list, and inserts an
|
|
22
|
+
* `aria-hidden` caret between entries. The tray is a grid box so it can shrink below its trail's
|
|
23
|
+
* min-content; an over-wide trail scrolls inside it rather than truncating labels.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* import { Breadcrumb, BreadcrumbItem } from "@horon-ai/react";
|
|
28
|
+
*
|
|
29
|
+
* function Example() {
|
|
30
|
+
* return (
|
|
31
|
+
* <Breadcrumb surface="solid">
|
|
32
|
+
* <BreadcrumbItem href="/" tile="H">Home</BreadcrumbItem>
|
|
33
|
+
* <BreadcrumbItem href="/projects">Projects</BreadcrumbItem>
|
|
34
|
+
* <BreadcrumbItem current>Horon</BreadcrumbItem>
|
|
35
|
+
* </Breadcrumb>
|
|
36
|
+
* );
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
16
40
|
export function Breadcrumb({
|
|
17
41
|
surface,
|
|
18
42
|
className,
|
|
@@ -53,14 +77,20 @@ export function Breadcrumb({
|
|
|
53
77
|
);
|
|
54
78
|
}
|
|
55
79
|
|
|
80
|
+
/** Props of {@link BreadcrumbItem}. */
|
|
56
81
|
export type BreadcrumbItemProps = Omit<ComponentProps<"a">, "className"> & {
|
|
57
82
|
/** Marks the current page: primary text and `aria-current`. */
|
|
58
83
|
current?: boolean;
|
|
59
84
|
/** Leading 20px ink tile, such as the workspace initial. */
|
|
60
85
|
tile?: ReactNode;
|
|
86
|
+
/** Extra classes merged onto the crumb element. */
|
|
61
87
|
className?: string;
|
|
62
88
|
};
|
|
63
89
|
|
|
90
|
+
/**
|
|
91
|
+
* One crumb of {@link Breadcrumb}: an anchor when `href` is set and the crumb is not current,
|
|
92
|
+
* otherwise a `<span>`. The current crumb renders primary text with `aria-current="page"`.
|
|
93
|
+
*/
|
|
64
94
|
export function BreadcrumbItem({ current, tile, href, className, children, ...props }: BreadcrumbItemProps) {
|
|
65
95
|
const Element = href && !current ? "a" : "span";
|
|
66
96
|
return (
|
|
@@ -4,14 +4,37 @@ import { Children, isValidElement, type ComponentProps, type ReactNode } from "r
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { LiquidGlass } from "./liquid-glass";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Visual treatment of {@link Button}.
|
|
9
|
+
*
|
|
10
|
+
* - `primary` — filled brand button, for the single main action of a screen or dialog.
|
|
11
|
+
* - `glass` — clear glass fill with a {@link LiquidGlass} layer, for chrome floating over imagery.
|
|
12
|
+
* - `solid` — bordered surface button, for secondary actions on a page.
|
|
13
|
+
* - `danger` — filled destructive action, such as delete.
|
|
14
|
+
* - `link` — size-independent text-only button; its `aria-current="page"` / `aria-current="true"` state
|
|
15
|
+
* colors it like primary text, which is how an active navigation link is marked.
|
|
16
|
+
*/
|
|
7
17
|
export type ButtonVariant = "primary" | "glass" | "solid" | "danger" | "link";
|
|
18
|
+
/**
|
|
19
|
+
* Height and text rung of a non-`link` {@link Button}, identical to {@link ControlSize}:
|
|
20
|
+
* `sm` 32px `label-md`, `md` 40px `label-lg`, `lg` 48px `label-lg`.
|
|
21
|
+
*/
|
|
8
22
|
export type ButtonSize = "sm" | "md" | "lg";
|
|
9
23
|
|
|
10
24
|
type ButtonCommonProps = {
|
|
25
|
+
/**
|
|
26
|
+
* Visual treatment; `link` ignores the size rungs.
|
|
27
|
+
* @default "primary"
|
|
28
|
+
*/
|
|
11
29
|
variant?: ButtonVariant;
|
|
30
|
+
/**
|
|
31
|
+
* Height and label rung of the control; `link` stays text-only.
|
|
32
|
+
* @default "md"
|
|
33
|
+
*/
|
|
12
34
|
size?: ButtonSize;
|
|
13
35
|
/** Leading icon; sized 16px (sm, md) or 20px (lg) and colored like the label. */
|
|
14
36
|
icon?: ReactNode;
|
|
37
|
+
/** Extra classes appended last, so a caller can add width, margins or overrides. */
|
|
15
38
|
className?: string;
|
|
16
39
|
};
|
|
17
40
|
|
|
@@ -49,7 +72,29 @@ const variantClass: Record<ButtonVariant, string> = {
|
|
|
49
72
|
link: "h-auto gap-1.5 rounded-sm px-1 text-label-sm text-text-secondary motion-colors hover:not-data-disabled:text-text-primary aria-[current=page]:text-text-primary aria-[current=true]:text-text-primary [&_svg]:size-3.5",
|
|
50
73
|
};
|
|
51
74
|
|
|
52
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Figma `Button` (55:339): Primary, Glass, Solid and Danger in three sizes, plus a text-only `link` variant (size-independent).
|
|
77
|
+
*
|
|
78
|
+
* Renders a Base UI button, or a real `<a>` once `href` is set (button-only attributes such as `type`,
|
|
79
|
+
* `disabled` and `form*` are then dropped instead of reaching the DOM). Sizes `sm` and `md` paint an
|
|
80
|
+
* invisible 44px hit area around the 32/40px control without moving it. A non-`link` button also accepts
|
|
81
|
+
* an `icon` before the label and, on `glass`, a {@link LiquidGlass} layer beneath its content.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* import { Button } from "@horon-ai/react";
|
|
86
|
+
*
|
|
87
|
+
* function Example() {
|
|
88
|
+
* return (
|
|
89
|
+
* <div className="flex items-center gap-2">
|
|
90
|
+
* <Button onClick={() => save()}>Save changes</Button>
|
|
91
|
+
* <Button variant="solid" size="sm">Cancel</Button>
|
|
92
|
+
* <Button variant="link" href="/settings">Settings</Button>
|
|
93
|
+
* </div>
|
|
94
|
+
* );
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
53
98
|
export function Button({
|
|
54
99
|
variant = "primary",
|
|
55
100
|
size = "md",
|
|
@@ -4,31 +4,52 @@ import { useMemo, useRef, useState, type KeyboardEvent } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import type { ControlSize } from "../lib/size";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* An inclusive date range for {@link Calendar} in `range` mode: both `start` and `end` are selected days,
|
|
9
|
+
* only those two endpoints are stored and the days between them are derived on render. Both ends are
|
|
10
|
+
* `null` before the first click, and `end` stays `null` while the user is picking the second day.
|
|
11
|
+
*/
|
|
12
|
+
export type DateRange = {
|
|
13
|
+
/** First selected day, or `null` when the range is empty. */
|
|
14
|
+
start: Date | null;
|
|
15
|
+
/** Last selected day; stays `null` until the second click completes the range. */
|
|
16
|
+
end: Date | null;
|
|
17
|
+
};
|
|
18
|
+
/** Day-cell rung of {@link Calendar}; identical to {@link ControlSize} (`sm` 32px, `md` 40px, `lg` 48px cells). */
|
|
8
19
|
export type CalendarSize = ControlSize;
|
|
9
20
|
|
|
10
21
|
type CommonProps = {
|
|
11
22
|
/** First visible month; defaults to the selected date or today. */
|
|
12
23
|
defaultMonth?: Date;
|
|
24
|
+
/**
|
|
25
|
+
* Controlled visible month. While set, the arrows and keyboard month changes only report
|
|
26
|
+
* through `onMonthChange` and the component keeps no month state of its own.
|
|
27
|
+
*/
|
|
13
28
|
month?: Date;
|
|
29
|
+
/** Fires with the first day of the month the user navigated to (arrows, or keyboard focus crossing a boundary). */
|
|
14
30
|
onMonthChange?: (month: Date) => void;
|
|
31
|
+
/** Days for which this returns true render disabled and cannot be selected. */
|
|
15
32
|
isDateDisabled?: (date: Date) => boolean;
|
|
16
33
|
/** BCP 47 locale for the month title, weekdays and day labels. */
|
|
17
34
|
locale?: string;
|
|
18
|
-
/** 0 = Sunday (the Figma calendar), 1 = Monday. */
|
|
35
|
+
/** 0 = Sunday (the Figma calendar), 1 = Monday. @default 0 */
|
|
19
36
|
weekStartsOn?: 0 | 1;
|
|
37
|
+
/** Extra classes merged onto the calendar frame. */
|
|
20
38
|
className?: string;
|
|
39
|
+
/** The day drawn as today, with the brand text color and the live dot; defaults to the real date at mount. */
|
|
21
40
|
today?: Date;
|
|
22
41
|
/**
|
|
23
42
|
* `default` is the day picker calendar (60:245): centered title-sm month.
|
|
24
43
|
* `popover` is the `Calendar popover` (88:495): 24px radius, left label-lg
|
|
25
44
|
* month and 32px round arrow buttons on the right.
|
|
45
|
+
* @default "default"
|
|
26
46
|
*/
|
|
27
47
|
variant?: "default" | "popover";
|
|
28
48
|
/**
|
|
29
49
|
* `solid` (default) is an opaque surface with a default border. `glass` puts it on the veil (`veil-overlay`),
|
|
30
50
|
* the material of the `DateField` popup; `flat` keeps the 80% white fill of the old thick glass
|
|
31
51
|
* without it (Paper Schedule mini calendar).
|
|
52
|
+
* @default "solid"
|
|
32
53
|
*/
|
|
33
54
|
surface?: "glass" | "flat" | "solid";
|
|
34
55
|
/** Days drawn as subtle ink circles in the day cell size, e.g. the week the schedule shows; selection still draws on top. */
|
|
@@ -36,11 +57,20 @@ type CommonProps = {
|
|
|
36
57
|
/**
|
|
37
58
|
* Day cell size: `sm` 32px, `md` 40px (default, the Figma size), `lg` 48px.
|
|
38
59
|
* Scales the header, padding, weekday row and day text with the cells.
|
|
60
|
+
* @default "md"
|
|
39
61
|
*/
|
|
40
62
|
size?: CalendarSize;
|
|
41
63
|
};
|
|
42
64
|
|
|
43
|
-
const containerSizeClass: Record<
|
|
65
|
+
const containerSizeClass: Record<
|
|
66
|
+
CalendarSize,
|
|
67
|
+
{
|
|
68
|
+
/** Classes for the `default` variant: fills the parent up to the Figma width. */
|
|
69
|
+
default: string;
|
|
70
|
+
/** Classes for the `popover` variant: fixed Figma width, capped by the viewport. */
|
|
71
|
+
popover: string;
|
|
72
|
+
}
|
|
73
|
+
> = {
|
|
44
74
|
// The default calendar fills its parent up to the Figma width (250/314/374px: seven cells plus padding),
|
|
45
75
|
// so it lands on that size on desktop and folds to the column on a narrow screen. `w-full` rather than a
|
|
46
76
|
// fixed `w-*` keeps its min-content at zero, which is what lets a flex parent shrink it. The popover is
|
|
@@ -140,10 +170,31 @@ const popoverDayTextClass: Record<CalendarSize, string> = {
|
|
|
140
170
|
lg: "[&_[role=gridcell]]:text-label-lg",
|
|
141
171
|
};
|
|
142
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Props of {@link Calendar}. `mode` switches the value shape: `single` (the default) works with one
|
|
175
|
+
* `Date`, `range` with a {@link DateRange}; the callback always receives the same shape.
|
|
176
|
+
*/
|
|
143
177
|
export type CalendarProps = CommonProps &
|
|
144
178
|
(
|
|
145
|
-
| {
|
|
146
|
-
|
|
179
|
+
| {
|
|
180
|
+
/** Pick one day. @default "single" */
|
|
181
|
+
mode?: "single";
|
|
182
|
+
/** Selected day, or `null` when nothing is selected. */
|
|
183
|
+
value?: Date | null;
|
|
184
|
+
/** Fires when a day is clicked, with the newly selected day. */
|
|
185
|
+
onValueChange?: (value: Date) => void;
|
|
186
|
+
}
|
|
187
|
+
| {
|
|
188
|
+
/** Pick a start and an end day. */
|
|
189
|
+
mode: "range";
|
|
190
|
+
/** Current range; omitted means both ends are empty. */
|
|
191
|
+
value?: DateRange;
|
|
192
|
+
/**
|
|
193
|
+
* Fires on every click: the first click sets `start` and clears `end`, the second sets `end`,
|
|
194
|
+
* and a click before `start` restarts the range at that day.
|
|
195
|
+
*/
|
|
196
|
+
onValueChange?: (value: DateRange) => void;
|
|
197
|
+
}
|
|
147
198
|
);
|
|
148
199
|
|
|
149
200
|
const startOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
@@ -162,7 +213,26 @@ function monthGrid(month: Date, weekStartsOn: 0 | 1): Date[] {
|
|
|
162
213
|
return Array.from({ length: weeks * 7 }, (_, index) => addDays(start, index));
|
|
163
214
|
}
|
|
164
215
|
|
|
165
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Figma `Day` (60:125) inside the `Calendar` of the day picker example (60:245).
|
|
218
|
+
*
|
|
219
|
+
* A one-month grid with `grid`/`row`/`gridcell` roles and an `aria-label` on every day. The month starts
|
|
220
|
+
* from `defaultMonth`, else the selected day, else today, and slides between months with the arrow
|
|
221
|
+
* buttons; `month` makes that controlled. Arrow keys move focus by a day, a week vertically, and bring
|
|
222
|
+
* the focused month into view; only the focused day is tabbable. The variant controls the frame
|
|
223
|
+
* (`default` centered header, `popover` left title with round arrows) and `surface` its material.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```tsx
|
|
227
|
+
* import { useState } from "react";
|
|
228
|
+
* import { Calendar, type DateRange } from "@horon-ai/react";
|
|
229
|
+
*
|
|
230
|
+
* function Example() {
|
|
231
|
+
* const [range, setRange] = useState<DateRange>({ start: null, end: null });
|
|
232
|
+
* return <Calendar mode="range" value={range} onValueChange={setRange} weekStartsOn={1} />;
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
166
236
|
export function Calendar(props: CalendarProps) {
|
|
167
237
|
const {
|
|
168
238
|
defaultMonth,
|
package/src/components/card.tsx
CHANGED
|
@@ -5,8 +5,11 @@ import type { ComponentProps, ReactElement, ReactNode } from "react";
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import { LiquidGlass } from "./liquid-glass";
|
|
7
7
|
|
|
8
|
+
/** Props of {@link Card}. */
|
|
8
9
|
export type CardProps = Omit<ComponentProps<"div">, "className" | "title"> & {
|
|
10
|
+
/** Card heading on the title row, wrapping inside the card instead of truncating; the caret joins this row. */
|
|
9
11
|
title?: ReactNode;
|
|
12
|
+
/** Body line under the title, in body-sm secondary text. */
|
|
10
13
|
description?: ReactNode;
|
|
11
14
|
/** Shows the trailing caret, for cards that open something. */
|
|
12
15
|
interactive?: boolean;
|
|
@@ -17,12 +20,38 @@ export type CardProps = Omit<ComponentProps<"div">, "className" | "title"> & {
|
|
|
17
20
|
/**
|
|
18
21
|
* `solid` (default) is an opaque surface with a default border. `glass` is the thick glass.
|
|
19
22
|
* `frost` is the Paper home board card: the same glass with the denser frost fill and a 0 8 24 6% drop.
|
|
23
|
+
* @default "solid"
|
|
20
24
|
*/
|
|
21
25
|
surface?: "glass" | "frost" | "solid";
|
|
26
|
+
/** Extra classes merged onto the rendered root. */
|
|
22
27
|
className?: string;
|
|
23
28
|
};
|
|
24
29
|
|
|
25
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Figma `Card` (63:310): a 320px thick-glass card, 20px padding and radius, Default / Interactive / Selected.
|
|
32
|
+
*
|
|
33
|
+
* `render` swaps the root element through Base UI's `useRender` (e.g. `<button />` or `<a />`); the glass
|
|
34
|
+
* layer is isolated behind the content, so the card never needs a stacking-context patch from the caller.
|
|
35
|
+
* `interactive` adds the caret, hover rim and press scale, `selected` the brand rim
|
|
36
|
+
* (`[data-selected]` is set too), and children render in a full-width block under the description.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* import { Card } from "@horon-ai/react";
|
|
41
|
+
*
|
|
42
|
+
* function Example() {
|
|
43
|
+
* return (
|
|
44
|
+
* <Card
|
|
45
|
+
* interactive
|
|
46
|
+
* surface="frost"
|
|
47
|
+
* title="Weekly digest"
|
|
48
|
+
* description="A summary of everything the assistant saved this week."
|
|
49
|
+
* render={<button type="button" />}
|
|
50
|
+
* />
|
|
51
|
+
* );
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
26
55
|
export function Card({
|
|
27
56
|
title,
|
|
28
57
|
description,
|
|
@@ -3,13 +3,22 @@ import type { ComponentProps } from "react";
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import type { ControlSize } from "../lib/size";
|
|
5
5
|
|
|
6
|
+
/** Padding and text rung of {@link ChatBubble}; identical to {@link ControlSize}. */
|
|
6
7
|
export type ChatBubbleSize = ControlSize;
|
|
7
8
|
|
|
9
|
+
/** Props of {@link ChatBubble}. */
|
|
8
10
|
export type ChatBubbleProps = Omit<ComponentProps<"div">, "className"> & {
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* `me` is the ink bubble, `bot` the flat subtle bubble. Neither uses glass.
|
|
13
|
+
* @default "me"
|
|
14
|
+
*/
|
|
10
15
|
from?: "me" | "bot";
|
|
11
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Bubble size: `sm` 12/8px padding with body-sm, `md` 16/12px with body-md (default), `lg` 20/14px with body-lg.
|
|
18
|
+
* @default "md"
|
|
19
|
+
*/
|
|
12
20
|
size?: ChatBubbleSize;
|
|
21
|
+
/** Extra classes merged onto the bubble. */
|
|
13
22
|
className?: string;
|
|
14
23
|
};
|
|
15
24
|
|
|
@@ -19,7 +28,27 @@ const sizeClass: Record<ChatBubbleSize, string> = {
|
|
|
19
28
|
lg: "px-5 py-3.5 text-body-lg",
|
|
20
29
|
};
|
|
21
30
|
|
|
22
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Figma `Chat bubble` (64:359): 16/12px padding, 20px radius, body-md text.
|
|
33
|
+
*
|
|
34
|
+
* `me` bubbles push to the right edge with the ink fill, `bot` bubbles stay left on the subtle fill;
|
|
35
|
+
* both cap at 560px or the parent's width, wrap long words and rise in with `motion-rise`. The `from`
|
|
36
|
+
* value is mirrored to `data-from` for styling hooks.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* import { ChatBubble } from "@horon-ai/react";
|
|
41
|
+
*
|
|
42
|
+
* function Example() {
|
|
43
|
+
* return (
|
|
44
|
+
* <div className="flex flex-col gap-2">
|
|
45
|
+
* <ChatBubble from="bot">What should I look up?</ChatBubble>
|
|
46
|
+
* <ChatBubble from="me">The Q3 usage report, please.</ChatBubble>
|
|
47
|
+
* </div>
|
|
48
|
+
* );
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
23
52
|
export function ChatBubble({ from = "me", size = "md", className, ...props }: ChatBubbleProps) {
|
|
24
53
|
return (
|
|
25
54
|
<div
|
|
@@ -4,11 +4,16 @@ import type { ComponentProps, ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { LiquidGlass } from "./liquid-glass";
|
|
6
6
|
|
|
7
|
+
/** Material of the chat controls: `solid` on raised surfaces, `glass` over imagery and glass panels. */
|
|
7
8
|
export type ChatControlSurface = "solid" | "glass";
|
|
8
9
|
|
|
9
10
|
type ButtonProps = Omit<ComponentProps<"button">, "className"> & {
|
|
11
|
+
/** Extra classes merged onto the control button. */
|
|
10
12
|
className?: string;
|
|
11
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* `solid` (default) on raised surfaces, `glass` over imagery and glass panels.
|
|
15
|
+
* @default "solid"
|
|
16
|
+
*/
|
|
12
17
|
surface?: ChatControlSurface;
|
|
13
18
|
};
|
|
14
19
|
|
|
@@ -20,7 +25,22 @@ const surfaceClass: Record<ChatControlSurface, string> = {
|
|
|
20
25
|
solid: "border border-border-default bg-bg-surface hover:bg-bg-subtle active:bg-interactive-secondary-hover",
|
|
21
26
|
};
|
|
22
27
|
|
|
23
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Figma `Jump to latest` (Chat controls 88:659): a 44px clear-glass circle with a 20px caret.
|
|
30
|
+
*
|
|
31
|
+
* A square icon button with `aria-label` (default `"Jump to latest"`) that surfaces with the
|
|
32
|
+
* `motion-rise` entrance and forwards `onClick`; the caller's handler performs the scroll to the
|
|
33
|
+
* newest message.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* import { JumpToLatest } from "@horon-ai/react";
|
|
38
|
+
*
|
|
39
|
+
* function Example() {
|
|
40
|
+
* return <JumpToLatest surface="glass" onClick={() => scrollToLatest()} />;
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
24
44
|
export function JumpToLatest({
|
|
25
45
|
surface = "solid",
|
|
26
46
|
className,
|
|
@@ -40,7 +60,12 @@ export function JumpToLatest({
|
|
|
40
60
|
);
|
|
41
61
|
}
|
|
42
62
|
|
|
43
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Figma `New messages` (88:659): a 40px clear-glass pill with a 16px caret and a label.
|
|
65
|
+
*
|
|
66
|
+
* A peer of {@link JumpToLatest} for the "there are unread messages above" case; the child is the label
|
|
67
|
+
* and all button attributes are forwarded.
|
|
68
|
+
*/
|
|
44
69
|
export function NewMessagesPill({ surface = "solid", className, children, ...props }: ButtonProps) {
|
|
45
70
|
return (
|
|
46
71
|
<button
|
|
@@ -58,21 +83,49 @@ export function NewMessagesPill({ surface = "solid", className, children, ...pro
|
|
|
58
83
|
/** Bar heights of the Figma waveform, in px. */
|
|
59
84
|
const WAVEFORM = [8, 14, 22, 12, 26, 18, 10, 20, 28, 16, 9, 18, 24, 12, 7, 15, 21, 11];
|
|
60
85
|
|
|
86
|
+
/** Props of {@link VoiceInput}. */
|
|
61
87
|
export type VoiceInputProps = {
|
|
62
88
|
/** Elapsed time, such as "0:07". */
|
|
63
89
|
elapsed: ReactNode;
|
|
64
|
-
/** 0-1 levels for the 18 bars; defaults to the Figma waveform. */
|
|
90
|
+
/** 0-1 levels for the 18 bars; defaults to the Figma waveform. Each level is scaled to 4–28px. */
|
|
65
91
|
levels?: number[];
|
|
66
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Animates the waveform while true; set false once recording stops.
|
|
94
|
+
* @default true
|
|
95
|
+
*/
|
|
67
96
|
recording?: boolean;
|
|
97
|
+
/** Fires when the red stop button is pressed. */
|
|
68
98
|
onStop?: () => void;
|
|
99
|
+
/**
|
|
100
|
+
* Accessible label of the stop button.
|
|
101
|
+
* @default "Stop recording"
|
|
102
|
+
*/
|
|
69
103
|
stopLabel?: string;
|
|
70
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* `solid` (default) on raised surfaces, `glass` over imagery and glass panels.
|
|
106
|
+
* @default "solid"
|
|
107
|
+
*/
|
|
71
108
|
surface?: ChatControlSurface;
|
|
109
|
+
/** Extra classes merged onto the pill. */
|
|
72
110
|
className?: string;
|
|
73
111
|
};
|
|
74
112
|
|
|
75
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Figma `Voice input` (88:659): a 52px pill with a red stop button, waveform and timer.
|
|
115
|
+
*
|
|
116
|
+
* The 18 bars keep the Figma heights unless `levels` is passed, in which case each 0-1 level maps to a
|
|
117
|
+
* 4–28px bar. With `recording` the bars run the `wave` animation on a 120ms stagger; the whole waveform
|
|
118
|
+
* is `aria-hidden`, so only the stop button and the elapsed text reach assistive technology.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```tsx
|
|
122
|
+
* import { VoiceInput } from "@horon-ai/react";
|
|
123
|
+
*
|
|
124
|
+
* function Example() {
|
|
125
|
+
* return <VoiceInput elapsed="0:07" recording onStop={() => stopRecording()} />;
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
76
129
|
export function VoiceInput({
|
|
77
130
|
elapsed,
|
|
78
131
|
levels,
|
|
@@ -5,15 +5,20 @@ import type { ComponentProps, ReactNode } from "react";
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import type { ControlSize } from "../lib/size";
|
|
7
7
|
|
|
8
|
+
/** Box size of {@link Checkbox}; identical to {@link ControlSize}. */
|
|
8
9
|
export type CheckboxSize = ControlSize;
|
|
9
10
|
|
|
11
|
+
/** Props of {@link Checkbox}. */
|
|
10
12
|
export type CheckboxProps = Omit<ComponentProps<typeof BaseCheckbox.Root>, "className"> & {
|
|
13
|
+
/** Label rendered after the box inside the same clickable `<label>`. */
|
|
11
14
|
label?: ReactNode;
|
|
12
15
|
/**
|
|
13
16
|
* `md` is the 20px Figma Checkbox, `sm` the 18px box of the Data table (192:9046),
|
|
14
17
|
* `lg` the 24px box.
|
|
18
|
+
* @default "md"
|
|
15
19
|
*/
|
|
16
20
|
size?: CheckboxSize;
|
|
21
|
+
/** Extra classes merged onto the label wrapper. */
|
|
17
22
|
className?: string;
|
|
18
23
|
};
|
|
19
24
|
|
|
@@ -36,7 +41,23 @@ const indicatorClass: Record<CheckboxSize, string> = {
|
|
|
36
41
|
lg: "[&_svg]:size-4",
|
|
37
42
|
};
|
|
38
43
|
|
|
39
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Figma `Checkbox` (58:217): a 20px box, 6px radius, with a 10px gap to its label.
|
|
46
|
+
*
|
|
47
|
+
* A Base UI checkbox: `checked` makes it controlled, otherwise it keeps its own state, and
|
|
48
|
+
* `defaultChecked` / `onCheckedChange` / `indeterminate` work as in Base UI. Checked and indeterminate
|
|
49
|
+
* boxes fill with the brand color and show a check or a dash; the wrapper `<label>` dims the whole control
|
|
50
|
+
* when the input is disabled.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* import { Checkbox } from "@horon-ai/react";
|
|
55
|
+
*
|
|
56
|
+
* function Example() {
|
|
57
|
+
* return <Checkbox label="Remember this chat" defaultChecked />;
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
40
61
|
export function Checkbox({ label, size = "md", className, ...props }: CheckboxProps) {
|
|
41
62
|
return (
|
|
42
63
|
<label
|
package/src/components/chip.tsx
CHANGED
|
@@ -5,11 +5,17 @@ import { Children, type ComponentProps, useState } from "react";
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import type { ControlSize } from "../lib/size";
|
|
7
7
|
|
|
8
|
+
/** Height and label rung of {@link Chip}; identical to {@link ControlSize} (`sm` 28px, `md` 32px, `lg` 36px). */
|
|
8
9
|
export type ChipSize = ControlSize;
|
|
9
10
|
|
|
11
|
+
/** Props of {@link Chip}. */
|
|
10
12
|
export type ChipProps = Omit<ComponentProps<typeof Toggle>, "className"> & {
|
|
11
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* `md` is the 32px Figma Chip; `sm` is 28px and `lg` 36px.
|
|
15
|
+
* @default "md"
|
|
16
|
+
*/
|
|
12
17
|
size?: ChipSize;
|
|
18
|
+
/** Extra classes merged onto the toggle button. */
|
|
13
19
|
className?: string;
|
|
14
20
|
};
|
|
15
21
|
|
|
@@ -32,7 +38,28 @@ const checkIconClass: Record<ChipSize, string> = {
|
|
|
32
38
|
lg: "size-4",
|
|
33
39
|
};
|
|
34
40
|
|
|
35
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Figma `Chip` (61:302): a 32px solid toggle, no glass; selected shows a check, the brand border and brand text.
|
|
43
|
+
*
|
|
44
|
+
* A Base UI toggle: `pressed` makes it controlled, otherwise it keeps its own state and reports through
|
|
45
|
+
* `onPressedChange`. The check is in a `0`-width slot that opens on press, so an unselected chip never
|
|
46
|
+
* reserves the icon space; the slot only animates after the first toggle, so a chip that mounts pressed
|
|
47
|
+
* does not flash it. An element child is laid out in a row at the label gap.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* import { Chip } from "@horon-ai/react";
|
|
52
|
+
*
|
|
53
|
+
* function Example() {
|
|
54
|
+
* return (
|
|
55
|
+
* <div className="flex flex-wrap gap-2">
|
|
56
|
+
* <Chip defaultPressed>Documents</Chip>
|
|
57
|
+
* <Chip>Images</Chip>
|
|
58
|
+
* </div>
|
|
59
|
+
* );
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
36
63
|
export function Chip({ size = "md", className, children, onPressedChange, ...props }: ChipProps) {
|
|
37
64
|
// The exit animation only plays after the first toggle, so a chip that mounts unpressed never flashes the check.
|
|
38
65
|
const [toggled, setToggled] = useState(false);
|
|
@@ -2,13 +2,20 @@ import { Radio as BaseRadio } from "@base-ui/react/radio";
|
|
|
2
2
|
import type { ComponentProps, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
/**
|
|
6
|
+
* Props for {@link ChoiceCard}: a radio root plus the card's own title, description and badge slots.
|
|
7
|
+
* `value`, `disabled` and the other radio props pass straight through to the Base UI radio root.
|
|
8
|
+
*/
|
|
5
9
|
export type ChoiceCardProps = Omit<ComponentProps<typeof BaseRadio.Root>, "className" | "children" | "title"> & {
|
|
10
|
+
/** Card heading on the title line; `label-md` semibold primary text, wrapped onto as many lines as the card width allows. */
|
|
6
11
|
title: ReactNode;
|
|
12
|
+
/** Optional second line under the title, rendered as `caption` secondary text. */
|
|
7
13
|
description?: ReactNode;
|
|
8
14
|
/** Pill at the end of the title line, e.g. "Recommended": 22px, 8px padding, 18% sky with brand text (Paper Mobile connection). */
|
|
9
15
|
badge?: ReactNode;
|
|
10
|
-
/** `brand` (default) is 18% sky with brand text; `neutral` is 6% ink with secondary text. */
|
|
16
|
+
/** `brand` (default) is 18% sky with brand text; `neutral` is 6% ink with secondary text. @default "brand" */
|
|
11
17
|
badgeTone?: "brand" | "neutral";
|
|
18
|
+
/** Extra classes on the card root, merged after the layout, border and state classes. */
|
|
12
19
|
className?: string;
|
|
13
20
|
};
|
|
14
21
|
|
|
@@ -16,6 +23,23 @@ export type ChoiceCardProps = Omit<ComponentProps<typeof BaseRadio.Root>, "class
|
|
|
16
23
|
* Paper `Choice card` (1VXM-0 selected, 1VXR-0 default): a 252px solid card, padding 14, radius 16, gap 12,
|
|
17
24
|
* with a 1px default border that turns brand when selected; no glass.
|
|
18
25
|
* Use inside `RadioGroup`. The 18px radio is a ring: 1.5px grey when off, a 5px ink ring around an 8px core when on.
|
|
26
|
+
*
|
|
27
|
+
* The card is a Base UI radio root, so it belongs to a single-select group: arrow keys move the selection,
|
|
28
|
+
* `value` / `defaultValue` on the group pick the card, and `disabled` drops the card to 40% opacity and blocks
|
|
29
|
+
* the click. The radio mark is decorative; the accessible name is the card's own text.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <RadioGroup defaultValue="team">
|
|
34
|
+
* <ChoiceCard value="solo" title="Solo" description="One workspace for yourself." />
|
|
35
|
+
* <ChoiceCard
|
|
36
|
+
* value="team"
|
|
37
|
+
* title="Team"
|
|
38
|
+
* description="Shared bots, memory and billing."
|
|
39
|
+
* badge="Recommended"
|
|
40
|
+
* />
|
|
41
|
+
* </RadioGroup>
|
|
42
|
+
* ```
|
|
19
43
|
*/
|
|
20
44
|
export function ChoiceCard({ title, description, badge, badgeTone = "brand", className, ...props }: ChoiceCardProps) {
|
|
21
45
|
return (
|
|
@@ -2,12 +2,26 @@ import type { ComponentProps } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Props for {@link CodePanel}: the `<pre>` attributes (`children` carries the code) plus a merged `className`. */
|
|
6
|
+
export type CodePanelProps = Omit<ComponentProps<"pre">, "className"> & {
|
|
7
|
+
/** Extra classes on the `<pre>`, merged after the panel surface and type classes. */
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Paper `Code block` (1VV9-0), the light variant: a 10px-radius neutral-50 panel with a 1px hairline,
|
|
9
13
|
* Geist Mono 12px on a 21px line pitch (Paper rows are 19px with a 2px gap), no header bar or copy button. Wrap keys in `CodeKey`; plain text reads as the string color.
|
|
10
14
|
* Chat surfaces keep the ink `CodeBlock` from message-extras.
|
|
15
|
+
*
|
|
16
|
+
* The `<pre>` scrolls horizontally instead of wrapping (`whitespace-pre`), and the dark theme swaps the
|
|
17
|
+
* panel to neutral-800 with a stronger border.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <CodePanel>
|
|
22
|
+
* {"---\ntitle: Weekly digest\n---"}
|
|
23
|
+
* </CodePanel>
|
|
24
|
+
* ```
|
|
11
25
|
*/
|
|
12
26
|
export function CodePanel({ className, ...props }: CodePanelProps) {
|
|
13
27
|
return (
|
|
@@ -21,7 +35,18 @@ export function CodePanel({ className, ...props }: CodePanelProps) {
|
|
|
21
35
|
);
|
|
22
36
|
}
|
|
23
37
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
38
|
+
/**
|
|
39
|
+
* A frontmatter key or other highlighted token inside a `CodePanel`.
|
|
40
|
+
*
|
|
41
|
+
* Part of {@link CodePanel}. Renders an inline `span` in the key colour, so it only changes the colour of
|
|
42
|
+
* the text it wraps; extra classes can override or add weight.
|
|
43
|
+
*/
|
|
44
|
+
export function CodeKey({
|
|
45
|
+
className,
|
|
46
|
+
...props
|
|
47
|
+
}: Omit<ComponentProps<"span">, "className"> & {
|
|
48
|
+
/** Extra classes on the span, merged after the key colour. */
|
|
49
|
+
className?: string;
|
|
50
|
+
}) {
|
|
26
51
|
return <span className={cn("text-syntax-key", className)} {...props} />;
|
|
27
52
|
}
|