@horon-ai/react 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/components/agent-activity.tsx +81 -11
- package/src/components/agent-lane.tsx +32 -3
- package/src/components/agent-lanes-timeline.tsx +85 -7
- package/src/components/agent-panel.tsx +53 -7
- package/src/components/agent-steering-panel.tsx +73 -21
- package/src/components/approval-card.tsx +34 -2
- package/src/components/avatar.tsx +20 -4
- package/src/components/badge.tsx +24 -3
- package/src/components/banner.tsx +19 -3
- package/src/components/bot-instructions-panel.tsx +92 -9
- package/src/components/bot-list-row.tsx +13 -1
- package/src/components/bot-memory-panel.tsx +58 -3
- package/src/components/bottom-sheet.tsx +61 -7
- package/src/components/breadcrumb-header.tsx +32 -2
- package/src/components/breadcrumb.tsx +31 -1
- package/src/components/button.tsx +46 -1
- package/src/components/calendar.tsx +76 -6
- package/src/components/card.tsx +30 -1
- package/src/components/chat-bubble.tsx +32 -3
- package/src/components/chat-controls.tsx +60 -7
- package/src/components/checkbox.tsx +22 -1
- package/src/components/chip.tsx +29 -2
- package/src/components/choice-card.tsx +25 -1
- package/src/components/code-panel.tsx +28 -3
- package/src/components/combobox.tsx +52 -2
- package/src/components/command-palette.tsx +89 -10
- package/src/components/composer.tsx +28 -1
- package/src/components/data-table.tsx +198 -16
- package/src/components/date-field.tsx +31 -2
- package/src/components/dialog.tsx +45 -3
- package/src/components/directory-card.tsx +23 -2
- package/src/components/document-extract-panel.tsx +83 -5
- package/src/components/empty-state.tsx +21 -2
- package/src/components/field.tsx +49 -3
- package/src/components/file-drop.tsx +39 -4
- package/src/components/filter-bar.tsx +39 -2
- package/src/components/flue-background.tsx +91 -21
- package/src/components/horon-icon.tsx +50 -3
- package/src/components/horon-wordmark.tsx +26 -1
- package/src/components/icon-button.tsx +38 -1
- package/src/components/input-group.tsx +57 -6
- package/src/components/input.tsx +48 -2
- package/src/components/integration-row.tsx +43 -2
- package/src/components/kbd.tsx +37 -4
- package/src/components/knowledge-base-panel.tsx +40 -2
- package/src/components/liquid-glass.tsx +16 -0
- package/src/components/list-detail.tsx +89 -7
- package/src/components/marketplace-card.tsx +40 -4
- package/src/components/mcp-server-row.tsx +49 -3
- package/src/components/menu.tsx +83 -3
- package/src/components/message-extras.tsx +54 -18
- package/src/components/modal.tsx +71 -4
- package/src/components/orchestrator-panel.tsx +48 -4
- package/src/components/page-header.tsx +26 -3
- package/src/components/plan-card.tsx +28 -1
- package/src/components/plan-step.tsx +41 -1
- package/src/components/plugin-row.tsx +61 -3
- package/src/components/progress.tsx +32 -2
- package/src/components/radio-group.tsx +55 -2
- package/src/components/research-run-panel.tsx +73 -3
- package/src/components/schedule.tsx +163 -12
- package/src/components/segmented-control.tsx +58 -3
- package/src/components/select.tsx +60 -0
- package/src/components/settings-list.tsx +66 -8
- package/src/components/sidebar.tsx +214 -23
- package/src/components/skeleton.tsx +61 -4
- package/src/components/slider.tsx +30 -0
- package/src/components/source-row.tsx +43 -4
- package/src/components/spinner.tsx +29 -2
- package/src/components/split-button.tsx +37 -3
- package/src/components/switch.tsx +31 -1
- package/src/components/tab-bar.tsx +48 -3
- package/src/components/tabs.tsx +59 -5
- package/src/components/task-row.tsx +49 -3
- package/src/components/textarea-field.tsx +36 -2
- package/src/components/textarea.tsx +35 -2
- package/src/components/toast.tsx +52 -3
- package/src/components/tool-call.tsx +91 -17
- package/src/components/tool-calls-header.tsx +32 -1
- package/src/components/tool-permissions.tsx +50 -2
- package/src/components/tooltip.tsx +33 -1
- package/src/components/top-bar.tsx +35 -2
- package/src/components/usage-meter.tsx +28 -2
- package/src/components/workspace-menu.tsx +46 -1
- package/src/hooks/index.ts +8 -0
- package/src/{lib → hooks}/use-active-indicator.ts +44 -1
- package/src/hooks/use-color-scheme.ts +50 -0
- package/src/hooks/use-copy-to-clipboard.ts +55 -0
- package/src/hooks/use-device.ts +69 -0
- package/src/hooks/use-hotkey.ts +56 -0
- package/src/hooks/use-media-query.ts +43 -0
- package/src/hooks/use-platform.ts +68 -0
- package/src/hooks/use-reduced-motion.ts +25 -0
- package/src/index.ts +44 -0
- package/src/lib/cn.ts +13 -1
- package/src/lib/cursor-ghost.ts +16 -6
- package/src/lib/platform.ts +236 -0
|
@@ -5,17 +5,28 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { PanelPillTabs } from "./bot-instructions-panel";
|
|
6
6
|
import { Switch } from "./switch";
|
|
7
7
|
|
|
8
|
-
type WithClass<T> = Omit<T, "className"> & {
|
|
8
|
+
type WithClass<T> = Omit<T, "className"> & {
|
|
9
|
+
/** Extra classes merged onto the root element. */
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
9
12
|
|
|
13
|
+
/** One switch row of {@link BotMemoryPanel}: a label, an optional description and the Switch state. */
|
|
10
14
|
export type BotMemorySetting = {
|
|
15
|
+
/** Stable key of the row, passed back to `onSettingChange`. */
|
|
11
16
|
id: string;
|
|
17
|
+
/** Row title, also used as the switch's `aria-label`. */
|
|
12
18
|
label: string;
|
|
19
|
+
/** Optional second line under the label, e.g. what the setting does. */
|
|
13
20
|
description?: string;
|
|
21
|
+
/** Initial switch state; the switch is uncontrolled after mount. */
|
|
14
22
|
defaultChecked?: boolean;
|
|
15
23
|
};
|
|
16
24
|
|
|
25
|
+
/** One remembered item of {@link BotMemoryPanel}, grouped under its `group` heading. */
|
|
17
26
|
export type BotMemoryItem = {
|
|
27
|
+
/** Stable key of the memory. */
|
|
18
28
|
id: string;
|
|
29
|
+
/** The remembered text, on the first line of the row. */
|
|
19
30
|
text: string;
|
|
20
31
|
/** Where the memory came from, for example `From a chat · Oct 8`. */
|
|
21
32
|
source: string;
|
|
@@ -25,25 +36,47 @@ export type BotMemoryItem = {
|
|
|
25
36
|
fresh?: boolean;
|
|
26
37
|
};
|
|
27
38
|
|
|
28
|
-
|
|
39
|
+
/** The transient "Memory updated" pill of {@link BotMemoryPanel}: a title and an optional second line. */
|
|
40
|
+
export type BotMemoryToast = {
|
|
41
|
+
/** Bold pill text, e.g. "Memory updated". */
|
|
42
|
+
title: string;
|
|
43
|
+
/** Optional detail shown after the title, e.g. what was saved. */
|
|
44
|
+
message?: string;
|
|
45
|
+
};
|
|
29
46
|
|
|
47
|
+
/** Props of {@link BotMemoryPanel}. */
|
|
30
48
|
export type BotMemoryPanelProps = WithClass<Omit<ComponentProps<"section">, "title">> & {
|
|
49
|
+
/**
|
|
50
|
+
* Heading of the panel.
|
|
51
|
+
* @default "Memory"
|
|
52
|
+
*/
|
|
31
53
|
title?: ReactNode;
|
|
54
|
+
/** Muted second line under the heading, e.g. how many memories are stored. */
|
|
32
55
|
subtitle?: ReactNode;
|
|
33
56
|
/** Fill of the memory meter, 0 to 100. */
|
|
34
57
|
usage: number;
|
|
58
|
+
/** Switch rows shown under the header, in order. */
|
|
35
59
|
settings?: BotMemorySetting[];
|
|
60
|
+
/** All memories; they are grouped by `group` and filtered by the pill tabs. */
|
|
36
61
|
memories: BotMemoryItem[];
|
|
37
62
|
/** The "Memory updated" pill; it enters with a pop and leaves through its close button. */
|
|
38
63
|
toast?: BotMemoryToast;
|
|
64
|
+
/** Fires once the closing pill has finished its exit animation. */
|
|
39
65
|
onToastDismiss?: () => void;
|
|
66
|
+
/** "Add memory" at the bottom of the panel. */
|
|
40
67
|
onAdd?: () => void;
|
|
68
|
+
/** Fires when a settings switch is flipped, with the setting `id` and the new state. */
|
|
41
69
|
onSettingChange?: (id: string, checked: boolean) => void;
|
|
42
70
|
/** Makes the search button actionable; without it the icon is a plain decoration. */
|
|
43
71
|
onSearch?: () => void;
|
|
44
72
|
};
|
|
45
73
|
|
|
46
|
-
|
|
74
|
+
/** Hairline separator between panel rows. */
|
|
75
|
+
// prettier-ignore
|
|
76
|
+
function Divider({ inset }: {
|
|
77
|
+
/** Indents the rule by 20px to match the surrounding rows. */
|
|
78
|
+
inset?: boolean;
|
|
79
|
+
}) {
|
|
47
80
|
return (
|
|
48
81
|
<div className={cn("flex self-stretch", inset && "px-5")}>
|
|
49
82
|
<div role="separator" className="h-px flex-1 bg-overlay-ink-8" />
|
|
@@ -54,6 +87,28 @@ function Divider({ inset }: { inset?: boolean }) {
|
|
|
54
87
|
/**
|
|
55
88
|
* Bot memory panel (Paper `ZO2-1` / `HPY-0`): a 24px-radius surface with a memory meter, two switch rows,
|
|
56
89
|
* a filter pill track, a transient "Memory updated" pill and the grouped memories.
|
|
90
|
+
*
|
|
91
|
+
* The meter clamps `usage` to 0–100 and exposes it as a `progressbar`. The filter tabs are built from the
|
|
92
|
+
* memories' `group` values plus an "All" tab and are component state; each row rises in with a 30ms
|
|
93
|
+
* stagger. The toast pill shows while a `toast` is passed, plays its exit animation when its close button
|
|
94
|
+
* is pressed, re-enters with a pop for a new toast object, and calls `onToastDismiss` when the exit
|
|
95
|
+
* animation ends.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* import { BotMemoryPanel } from "@horon-ai/react";
|
|
100
|
+
*
|
|
101
|
+
* function Example() {
|
|
102
|
+
* return (
|
|
103
|
+
* <BotMemoryPanel
|
|
104
|
+
* usage={42}
|
|
105
|
+
* settings={[{ id: "auto", label: "Memory", description: "Let the assistant remember chats", defaultChecked: true }]}
|
|
106
|
+
* memories={[{ id: "m1", text: "Prefers Korean replies", source: "From a chat · Oct 8", group: "Preference" }]}
|
|
107
|
+
* onAdd={() => {}}
|
|
108
|
+
* />
|
|
109
|
+
* );
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
57
112
|
*/
|
|
58
113
|
export function BotMemoryPanel({
|
|
59
114
|
title = "Memory",
|
|
@@ -4,23 +4,42 @@ import type { ComponentProps, ReactNode } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Root of the bottom sheet: owns the open state and the drag/Escape dismissal. Wrap a
|
|
9
|
+
* {@link BottomSheetTrigger} and a {@link BottomSheetContent}; the sheet is only mounted while open.
|
|
10
|
+
*/
|
|
7
11
|
export const BottomSheet = Drawer.Root;
|
|
12
|
+
/** The element that opens the sheet, forwarded to Base UI's trigger unchanged. */
|
|
8
13
|
export const BottomSheetTrigger = Drawer.Trigger;
|
|
14
|
+
/** A close control for the sheet body; it also dismisses on scrim, Escape and a downward drag. */
|
|
9
15
|
export const BottomSheetClose = Drawer.Close;
|
|
10
16
|
|
|
17
|
+
/** Props of {@link BottomSheetContent}. */
|
|
11
18
|
export type BottomSheetContentProps = Omit<ComponentProps<typeof Drawer.Popup>, "className" | "title"> & {
|
|
19
|
+
/** Sheet title, rendered in the drawer's title slot above the children. */
|
|
12
20
|
title: ReactNode;
|
|
13
21
|
/**
|
|
14
22
|
* `panel` is the Figma panel sheet (374px max, label-lg title). `detail` is the Paper detail sheet:
|
|
15
23
|
* full width, 18px rhythm, a caller-built header (tile, title, description) set flush left.
|
|
24
|
+
* @default "panel"
|
|
16
25
|
*/
|
|
17
26
|
variant?: "panel" | "detail";
|
|
18
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* `glass` is the veil sheet (`veil-overlay`); `solid` is the Paper workspace sheet: opaque default fill, 20px top corners, 8/24/16 padding.
|
|
29
|
+
* @default "solid"
|
|
30
|
+
*/
|
|
19
31
|
surface?: "glass" | "solid";
|
|
20
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Draws the dimmed, blurred scrim behind the sheet. Paper's floating panel sheet has none.
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
21
36
|
backdrop?: boolean;
|
|
22
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Insets the sheet 16px from the sides and bottom edge (Paper's 358px panel sheet) instead of docking it full width.
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
23
41
|
floating?: boolean;
|
|
42
|
+
/** Extra classes merged onto the sheet popup. */
|
|
24
43
|
className?: string;
|
|
25
44
|
};
|
|
26
45
|
|
|
@@ -28,6 +47,29 @@ export type BottomSheetContentProps = Omit<ComponentProps<typeof Drawer.Popup>,
|
|
|
28
47
|
* Figma `Bottom sheet` (Panels 88:808): the veil with 32px top corners, a 36x5 grabber and a
|
|
29
48
|
* label-lg title. There is no close button: the sheet closes on the scrim, on Escape, on a
|
|
30
49
|
* downward drag of the grabber, or through the caller's own bottom action row.
|
|
50
|
+
*
|
|
51
|
+
* Portal-mounted above the page (z-50). The popup slides up on open and down on close; the body
|
|
52
|
+
* scrolls on its own with `overscroll-contain`, and the sheet is capped at `100dvh` minus 2rem.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* import { BottomSheet, BottomSheetAction, BottomSheetActions, BottomSheetContent, BottomSheetTrigger } from "@horon-ai/react";
|
|
57
|
+
*
|
|
58
|
+
* function Example() {
|
|
59
|
+
* return (
|
|
60
|
+
* <BottomSheet>
|
|
61
|
+
* <BottomSheetTrigger>Open sheet</BottomSheetTrigger>
|
|
62
|
+
* <BottomSheetContent title="Reminder" floating>
|
|
63
|
+
* <p className="text-body-sm text-text-secondary">Every weekday at 9:00</p>
|
|
64
|
+
* <BottomSheetActions>
|
|
65
|
+
* <BottomSheetAction>Cancel</BottomSheetAction>
|
|
66
|
+
* <BottomSheetAction variant="primary">Save</BottomSheetAction>
|
|
67
|
+
* </BottomSheetActions>
|
|
68
|
+
* </BottomSheetContent>
|
|
69
|
+
* </BottomSheet>
|
|
70
|
+
* );
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
31
73
|
*/
|
|
32
74
|
export function BottomSheetContent({
|
|
33
75
|
title,
|
|
@@ -82,14 +124,17 @@ export function BottomSheetContent({
|
|
|
82
124
|
);
|
|
83
125
|
}
|
|
84
126
|
|
|
127
|
+
/** Props of {@link BottomSheetRow}. */
|
|
85
128
|
export type BottomSheetRowProps = Omit<ComponentProps<"button">, "className"> & {
|
|
129
|
+
/** Leading icon rendered at 20px in secondary text color. */
|
|
86
130
|
icon?: ReactNode;
|
|
87
131
|
/** Current value on the right, such as "On" or "Weekdays". */
|
|
88
132
|
value?: ReactNode;
|
|
133
|
+
/** Extra classes merged onto the row button. */
|
|
89
134
|
className?: string;
|
|
90
135
|
};
|
|
91
136
|
|
|
92
|
-
/** A 48px settings row: 20px icon, label, value and a 16px caret. */
|
|
137
|
+
/** A 48px settings row: 20px icon, label, value and a 16px caret. Part of {@link BottomSheetContent}. */
|
|
93
138
|
export function BottomSheetRow({ icon, value, className, children, ...props }: BottomSheetRowProps) {
|
|
94
139
|
return (
|
|
95
140
|
<button
|
|
@@ -108,22 +153,31 @@ export function BottomSheetRow({ icon, value, className, children, ...props }: B
|
|
|
108
153
|
);
|
|
109
154
|
}
|
|
110
155
|
|
|
111
|
-
/** The action row: secondary first, the primary action at the right edge, 8px apart. */
|
|
156
|
+
/** The action row: secondary first, the primary action at the right edge, 8px apart. Wraps onto a second line when the sheet is too narrow. Part of {@link BottomSheetContent}. */
|
|
112
157
|
export function BottomSheetActions({
|
|
113
158
|
className,
|
|
114
159
|
...props
|
|
115
|
-
}: Omit<ComponentProps<"div">, "className"> & {
|
|
160
|
+
}: Omit<ComponentProps<"div">, "className"> & {
|
|
161
|
+
/** Extra classes merged onto the action row. */
|
|
162
|
+
className?: string;
|
|
163
|
+
}) {
|
|
116
164
|
return (
|
|
117
165
|
<div className={cn("relative flex w-full flex-wrap items-center justify-end gap-2 pt-3", className)} {...props} />
|
|
118
166
|
);
|
|
119
167
|
}
|
|
120
168
|
|
|
169
|
+
/** Props of {@link BottomSheetAction}. */
|
|
121
170
|
export type BottomSheetActionProps = Omit<ComponentProps<"button">, "className"> & {
|
|
171
|
+
/**
|
|
172
|
+
* `primary` is the filled brand pill, `secondary` the 6% ink pill.
|
|
173
|
+
* @default "secondary"
|
|
174
|
+
*/
|
|
122
175
|
variant?: "primary" | "secondary";
|
|
176
|
+
/** Extra classes merged onto the action button. */
|
|
123
177
|
className?: string;
|
|
124
178
|
};
|
|
125
179
|
|
|
126
|
-
/** The sheet's 44px pill actions: `secondary` is 6% ink, `primary` is ink. */
|
|
180
|
+
/** The sheet's 44px pill actions: `secondary` is 6% ink, `primary` is ink. Part of {@link BottomSheetActions}. */
|
|
127
181
|
export function BottomSheetAction({ variant = "secondary", className, ...props }: BottomSheetActionProps) {
|
|
128
182
|
return (
|
|
129
183
|
<button
|
|
@@ -6,26 +6,56 @@ import { BreadcrumbItem } from "./breadcrumb";
|
|
|
6
6
|
import { IconButton } from "./icon-button";
|
|
7
7
|
import { LiquidGlass } from "./liquid-glass";
|
|
8
8
|
|
|
9
|
+
/** One crumb of {@link BreadcrumbHeader}; the last crumb in `crumbs` is the current page. */
|
|
9
10
|
export type BreadcrumbHeaderCrumb = {
|
|
11
|
+
/** Crumb text; also its React key, so crumbs should have distinct labels. */
|
|
10
12
|
label: string;
|
|
13
|
+
/** Makes the crumb a link; ignored for the last (current) crumb, which stays text. */
|
|
11
14
|
href?: string;
|
|
12
15
|
/** Leading 20px ink tile, such as the workspace initial. */
|
|
13
16
|
tile?: ReactNode;
|
|
14
17
|
};
|
|
15
18
|
|
|
19
|
+
/** Props of {@link BreadcrumbHeader}. */
|
|
16
20
|
export type BreadcrumbHeaderProps = Omit<ComponentProps<"header">, "className"> & {
|
|
17
21
|
/** Crumbs in order; the last one is the current page. */
|
|
18
22
|
crumbs: BreadcrumbHeaderCrumb[];
|
|
23
|
+
/** Adds the search icon button; without it the button is not rendered at all. */
|
|
19
24
|
onSearch?: () => void;
|
|
25
|
+
/** Adds the "more" (three dots) icon button; without it the button is not rendered at all. */
|
|
20
26
|
onMore?: () => void;
|
|
27
|
+
/** Accessible label of the search button. @default "Search" */
|
|
21
28
|
searchLabel?: string;
|
|
29
|
+
/** Accessible label of the more button. @default "More" */
|
|
22
30
|
moreLabel?: string;
|
|
23
|
-
/** `solid` (default) on the page, `glass` over imagery and glass panels. */
|
|
31
|
+
/** `solid` (default) on the page, `glass` over imagery and glass panels. @default "solid" */
|
|
24
32
|
surface?: "solid" | "glass";
|
|
33
|
+
/** Extra classes merged onto the header. */
|
|
25
34
|
className?: string;
|
|
26
35
|
};
|
|
27
36
|
|
|
28
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Paper `Header with breadcrumb`: a 52px regular-glass pill with a crumb row, search and more buttons.
|
|
39
|
+
*
|
|
40
|
+
* The crumb trail scrolls sideways inside the pill instead of truncating; only the last crumb truncates.
|
|
41
|
+
* The two icon buttons (36px) are only rendered when their handlers are passed. On `glass` a
|
|
42
|
+
* {@link LiquidGlass} layer sits under the content, so the header must be placed over imagery.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* import { BreadcrumbHeader } from "@horon-ai/react";
|
|
47
|
+
*
|
|
48
|
+
* function Example() {
|
|
49
|
+
* return (
|
|
50
|
+
* <BreadcrumbHeader
|
|
51
|
+
* crumbs={[{ label: "H", tile: "H", href: "/" }, { label: "Projects", href: "/projects" }, { label: "Horon" }]}
|
|
52
|
+
* onSearch={() => {}}
|
|
53
|
+
* onMore={() => {}}
|
|
54
|
+
* />
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
29
59
|
export function BreadcrumbHeader({
|
|
30
60
|
crumbs,
|
|
31
61
|
onSearch,
|
|
@@ -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
|