@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,42 +5,112 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { ChatBubble } from "./chat-bubble";
|
|
6
6
|
import { Spinner } from "./spinner";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* One query row in the list under the status line.
|
|
10
|
+
*/
|
|
8
11
|
export type ResearchQuery = {
|
|
12
|
+
/** Stable key for the row. */
|
|
9
13
|
id: string;
|
|
10
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* `searching` shows a spinner and the muted text; `done` a check. Anything else (omitted, and
|
|
16
|
+
* any other value) renders as `done`.
|
|
17
|
+
* @default "done"
|
|
18
|
+
*/
|
|
11
19
|
state?: "done" | "searching";
|
|
20
|
+
/** The query text, truncated with an ellipsis when the row runs out of width. */
|
|
12
21
|
text: ReactNode;
|
|
13
22
|
/** Result count shown at the end of the row, such as "8 results". */
|
|
14
23
|
results?: ReactNode;
|
|
15
24
|
};
|
|
16
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Props for {@link ResearchRunPanel}. Extra `section` props (`id`, `data-*`, handlers) land on the
|
|
28
|
+
* outer panel element.
|
|
29
|
+
*/
|
|
17
30
|
export type ResearchRunPanelProps = Omit<ComponentProps<"section">, "title"> & {
|
|
31
|
+
/**
|
|
32
|
+
* Header title next to the magnifying glass.
|
|
33
|
+
* @default "Research"
|
|
34
|
+
*/
|
|
18
35
|
title?: ReactNode;
|
|
36
|
+
/**
|
|
37
|
+
* Text of the header button that starts a new run.
|
|
38
|
+
* @default "New research"
|
|
39
|
+
*/
|
|
19
40
|
newLabel?: ReactNode;
|
|
41
|
+
/** Fires when the header button is clicked; the button is still rendered without it. */
|
|
20
42
|
onNewResearch?: () => void;
|
|
21
43
|
/** The question bubble, right-aligned. */
|
|
22
44
|
question: ReactNode;
|
|
23
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Shows the spinner, status line and caret while the run works; when false the status block
|
|
47
|
+
* collapses and the submit button becomes a send arrow.
|
|
48
|
+
* @default true
|
|
49
|
+
*/
|
|
24
50
|
running?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Label in front of the caret in the status line.
|
|
53
|
+
* @default "Researching"
|
|
54
|
+
*/
|
|
25
55
|
statusLabel?: ReactNode;
|
|
56
|
+
/** First meta value after the status label, such as "12s"; joins `read`/`total` with a middot. */
|
|
26
57
|
elapsed?: ReactNode;
|
|
58
|
+
/** Pages read so far; with `total` it renders "N of M read" and drives the sources pill label. */
|
|
27
59
|
read?: number;
|
|
60
|
+
/** Pages in the run; must be set together with `read` for the count to appear. */
|
|
28
61
|
total?: number;
|
|
62
|
+
/** Queries resolved so far, drawn under a 1.5px rail with a 30ms stagger per row. */
|
|
29
63
|
queries?: ResearchQuery[];
|
|
30
64
|
/** Initials for the stacked source avatars under the queries. */
|
|
31
65
|
sources?: string[];
|
|
66
|
+
/** Fires from the round button while `running`, which then shows a stop glyph instead of an arrow. */
|
|
32
67
|
onStop?: () => void;
|
|
33
68
|
/** Makes the sources pill actionable; without it the pill is a plain label. */
|
|
34
69
|
onOpenSources?: () => void;
|
|
35
70
|
/** Makes the running status line expandable; without it the row carries no caret. */
|
|
36
71
|
onToggleStatus?: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Placeholder and accessible name of the follow-up input.
|
|
74
|
+
* @default "Ask a follow-up"
|
|
75
|
+
*/
|
|
37
76
|
followUpPlaceholder?: string;
|
|
77
|
+
/** Fires with the raw input value on submit; the field is cleared afterwards. */
|
|
38
78
|
onFollowUp?: (value: string) => void;
|
|
39
79
|
};
|
|
40
80
|
|
|
41
81
|
const stagger = (index: number) => ({ "--i": index, animationDelay: `calc(var(--i) * 30ms)` }) as CSSProperties;
|
|
42
82
|
|
|
43
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Paper `Research run` (Agents N10): a 24px-radius panel with a header, the question, a live status line and the queries read so far.
|
|
85
|
+
*
|
|
86
|
+
* The panel is presentational: it holds no run state of its own. Submitted follow-ups are handed
|
|
87
|
+
* to `onFollowUp` and the input is cleared, so the caller appends the new question. While
|
|
88
|
+
* `running` the status line is announced politely and the round button stops the run instead of
|
|
89
|
+
* submitting.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```tsx
|
|
93
|
+
* import { ResearchRunPanel } from "@horon-ai/react";
|
|
94
|
+
*
|
|
95
|
+
* function Example() {
|
|
96
|
+
* return (
|
|
97
|
+
* <ResearchRunPanel
|
|
98
|
+
* question="What changed in the EU AI Act this year?"
|
|
99
|
+
* statusLabel="Reading sources"
|
|
100
|
+
* read={4}
|
|
101
|
+
* total={12}
|
|
102
|
+
* queries={[
|
|
103
|
+
* { id: "1", text: "EU AI Act amendments", results: "8 results" },
|
|
104
|
+
* { id: "2", text: "Enforcement timeline", state: "searching" },
|
|
105
|
+
* ]}
|
|
106
|
+
* sources={["EU", "CE"]}
|
|
107
|
+
* onStop={() => {}}
|
|
108
|
+
* onFollowUp={(value) => console.log(value)}
|
|
109
|
+
* />
|
|
110
|
+
* );
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
44
114
|
export function ResearchRunPanel({
|
|
45
115
|
title = "Research",
|
|
46
116
|
newLabel = "New research",
|
|
@@ -8,23 +8,46 @@ import { cn } from "../lib/cn";
|
|
|
8
8
|
import type { ControlSize } from "../lib/size";
|
|
9
9
|
import { LiquidGlass } from "./liquid-glass";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Field size of a picker, on the shared control scale: `sm` 32px, `md` 40px, `lg` 48px.
|
|
13
|
+
*/
|
|
11
14
|
export type PickerFieldSize = ControlSize;
|
|
12
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Props for {@link PickerField}. The type is closed: it declares no `disabled` or `name` (and no
|
|
18
|
+
* other Base UI `Select.Root` prop), so TypeScript rejects them before they reach the root; `items`
|
|
19
|
+
* is the label list the trigger reads.
|
|
20
|
+
*/
|
|
13
21
|
export type PickerFieldProps = {
|
|
14
|
-
|
|
22
|
+
/** Choices in the popup; `value` is what `onValueChange` returns, `label` is what the row and trigger show. */
|
|
23
|
+
items: {
|
|
24
|
+
/** Value reported when the item is picked. */
|
|
25
|
+
value: string;
|
|
26
|
+
/** Text shown on the trigger and in the popup row. */
|
|
27
|
+
label: ReactNode;
|
|
28
|
+
}[];
|
|
29
|
+
/** Selected value in controlled mode. */
|
|
15
30
|
value?: string;
|
|
31
|
+
/** Starting value in uncontrolled mode. */
|
|
16
32
|
defaultValue?: string;
|
|
33
|
+
/** Fires with the newly selected value, or `null` when the choice is cleared, after a pick. */
|
|
17
34
|
onValueChange?: (value: string | null) => void;
|
|
18
|
-
/** Leading 16px
|
|
35
|
+
/** Leading icon, 16px on `sm` and `md` and 20px on `lg`; the Time field uses a clock. */
|
|
19
36
|
icon?: ReactNode;
|
|
20
37
|
/**
|
|
21
38
|
* `solid` (default) is the opaque surface field with a default border, `glass` the veil field,
|
|
22
39
|
* `inset` the 4% ink field inside the schedule card.
|
|
40
|
+
* @default "solid"
|
|
23
41
|
*/
|
|
24
42
|
surface?: "solid" | "glass" | "inset";
|
|
25
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Field size: `sm` 32px, `md` 40px (default), `lg` 48px; scales width, icon and menu rows.
|
|
45
|
+
* @default "md"
|
|
46
|
+
*/
|
|
26
47
|
size?: PickerFieldSize;
|
|
48
|
+
/** Accessible name of the trigger when no visible label is associated with it. */
|
|
27
49
|
"aria-label"?: string;
|
|
50
|
+
/** Classes merged onto the trigger. */
|
|
28
51
|
className?: string;
|
|
29
52
|
};
|
|
30
53
|
|
|
@@ -73,7 +96,34 @@ const itemIndicatorSizeClass: Record<PickerFieldSize, string> = {
|
|
|
73
96
|
lg: "[&_svg]:size-5",
|
|
74
97
|
};
|
|
75
98
|
|
|
76
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Figma `Time field` (Routine schedule 110:765): a 160px picker on the same 32/40/48px and 14/16/18 text rungs as `Input`.
|
|
101
|
+
*
|
|
102
|
+
* The popup is portalled to the body and aligned under the trigger with an 8px offset, so it can
|
|
103
|
+
* escape a scrolling card; the caret rotates while it is open. The field never grows past its
|
|
104
|
+
* rung width but shrinks with a narrow parent.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```tsx
|
|
108
|
+
* import { useState } from "react";
|
|
109
|
+
* import { PickerField } from "@horon-ai/react";
|
|
110
|
+
*
|
|
111
|
+
* function Example() {
|
|
112
|
+
* const [time, setTime] = useState("09:00");
|
|
113
|
+
* return (
|
|
114
|
+
* <PickerField
|
|
115
|
+
* aria-label="Start time"
|
|
116
|
+
* value={time}
|
|
117
|
+
* onValueChange={(next) => setTime(next ?? time)}
|
|
118
|
+
* items={[
|
|
119
|
+
* { value: "09:00", label: "9:00 AM" },
|
|
120
|
+
* { value: "10:00", label: "10:00 AM" },
|
|
121
|
+
* ]}
|
|
122
|
+
* />
|
|
123
|
+
* );
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
77
127
|
export function PickerField({
|
|
78
128
|
items,
|
|
79
129
|
icon,
|
|
@@ -145,17 +195,39 @@ export function PickerField({
|
|
|
145
195
|
);
|
|
146
196
|
}
|
|
147
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Day circle size: `sm` 32px, `md` 36px, `lg` 44px.
|
|
200
|
+
*/
|
|
148
201
|
export type WeekdayPickerSize = ControlSize;
|
|
149
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Props for {@link WeekdayPicker}. It is a multiple-selection toggle group, so `value` and
|
|
205
|
+
* `onValueChange` carry an array of day values.
|
|
206
|
+
*/
|
|
150
207
|
export type WeekdayPickerProps = {
|
|
151
208
|
/** One entry per day, in display order. */
|
|
152
|
-
days: {
|
|
209
|
+
days: {
|
|
210
|
+
/** Value that goes into `value` and `onValueChange`. */
|
|
211
|
+
value: string;
|
|
212
|
+
/** Short text on the circle, such as "M". */
|
|
213
|
+
label: string;
|
|
214
|
+
/** Accessible name of the toggle, such as "Monday". */
|
|
215
|
+
name: string;
|
|
216
|
+
}[];
|
|
217
|
+
/** Selected day values in controlled mode. */
|
|
153
218
|
value?: string[];
|
|
219
|
+
/** Selected day values at first render in uncontrolled mode. */
|
|
154
220
|
defaultValue?: string[];
|
|
221
|
+
/** Fires with the complete next selection whenever a day is toggled on or off. */
|
|
155
222
|
onValueChange?: (value: string[]) => void;
|
|
156
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* Day circle size: `sm` 32px, `md` 36px (default), `lg` 44px.
|
|
225
|
+
* @default "md"
|
|
226
|
+
*/
|
|
157
227
|
size?: WeekdayPickerSize;
|
|
228
|
+
/** Accessible name of the group. */
|
|
158
229
|
"aria-label"?: string;
|
|
230
|
+
/** Classes merged onto the wrapping flex row. */
|
|
159
231
|
className?: string;
|
|
160
232
|
};
|
|
161
233
|
|
|
@@ -172,7 +244,32 @@ const weekdayToggleSizeClass: Record<WeekdayPickerSize, string> = {
|
|
|
172
244
|
lg: "size-11 text-label-lg",
|
|
173
245
|
};
|
|
174
246
|
|
|
175
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Figma `Weekday picker` (110:765): 36px round toggles, ink when on and 6% ink when off.
|
|
249
|
+
*
|
|
250
|
+
* Toggling one day does not close the group; each press reports the whole next selection. The
|
|
251
|
+
* group wraps instead of shrinking its circles, so touch targets stay at least 32px. It is
|
|
252
|
+
* multiple-selection, so several days can be on at once.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```tsx
|
|
256
|
+
* import { WeekdayPicker } from "@horon-ai/react";
|
|
257
|
+
*
|
|
258
|
+
* function Example() {
|
|
259
|
+
* return (
|
|
260
|
+
* <WeekdayPicker
|
|
261
|
+
* aria-label="Repeat on"
|
|
262
|
+
* defaultValue={["mon", "wed", "fri"]}
|
|
263
|
+
* days={[
|
|
264
|
+
* { value: "mon", label: "M", name: "Monday" },
|
|
265
|
+
* { value: "tue", label: "T", name: "Tuesday" },
|
|
266
|
+
* { value: "wed", label: "W", name: "Wednesday" },
|
|
267
|
+
* ]}
|
|
268
|
+
* />
|
|
269
|
+
* );
|
|
270
|
+
* }
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
176
273
|
export function WeekdayPicker({
|
|
177
274
|
days,
|
|
178
275
|
value,
|
|
@@ -210,15 +307,28 @@ export function WeekdayPicker({
|
|
|
210
307
|
);
|
|
211
308
|
}
|
|
212
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Props for {@link ScheduleCard}. The card is a plain section: it lays out what you pass and
|
|
312
|
+
* draws the frame, header and next-run strip around it.
|
|
313
|
+
*/
|
|
213
314
|
export type ScheduleCardProps = {
|
|
315
|
+
/** Header title next to the repeat icon. */
|
|
214
316
|
title: ReactNode;
|
|
215
|
-
/** The "Next run" line at the bottom. */
|
|
317
|
+
/** The "Next run" line at the bottom; without it the strip is not rendered. */
|
|
216
318
|
nextRun?: ReactNode;
|
|
319
|
+
/** Body of the card, usually a stack of `ScheduleSection` blocks and pickers. */
|
|
217
320
|
children: ReactNode;
|
|
218
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* Card size: `sm` 340px, `md` 380px (default), `lg` 420px; scales padding, gaps and text.
|
|
323
|
+
* @default "md"
|
|
324
|
+
*/
|
|
219
325
|
size?: ControlSize;
|
|
220
|
-
/**
|
|
326
|
+
/**
|
|
327
|
+
* `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card.
|
|
328
|
+
* @default "solid"
|
|
329
|
+
*/
|
|
221
330
|
surface?: "solid" | "glass";
|
|
331
|
+
/** Classes merged onto the card. */
|
|
222
332
|
className?: string;
|
|
223
333
|
};
|
|
224
334
|
|
|
@@ -259,7 +369,37 @@ const nextRunIconSizeClass: Record<ControlSize, string> = {
|
|
|
259
369
|
lg: "size-5",
|
|
260
370
|
};
|
|
261
371
|
|
|
262
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Figma `Routine schedule card` (110:765): a 380px card, 20px padding, 16px gaps, solid or thick glass.
|
|
374
|
+
*
|
|
375
|
+
* Compose it from `ScheduleSection` blocks holding `WeekdayPicker`, `PickerField` or `Input`
|
|
376
|
+
* controls. On a narrow column the card folds to the available width instead of overflowing.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```tsx
|
|
380
|
+
* import { PickerField, ScheduleCard, ScheduleSection, WeekdayPicker } from "@horon-ai/react";
|
|
381
|
+
*
|
|
382
|
+
* function Example() {
|
|
383
|
+
* return (
|
|
384
|
+
* <ScheduleCard title="Morning digest" nextRun="Next run tomorrow at 9:00 AM">
|
|
385
|
+
* <ScheduleSection label="Repeat">
|
|
386
|
+
* <PickerField
|
|
387
|
+
* aria-label="Repeat"
|
|
388
|
+
* surface="inset"
|
|
389
|
+
* items={[
|
|
390
|
+
* { value: "daily", label: "Daily" },
|
|
391
|
+
* { value: "weekly", label: "Weekly" },
|
|
392
|
+
* ]}
|
|
393
|
+
* />
|
|
394
|
+
* </ScheduleSection>
|
|
395
|
+
* <ScheduleSection label="Days">
|
|
396
|
+
* <WeekdayPicker days={[{ value: "mon", label: "M", name: "Monday" }]} />
|
|
397
|
+
* </ScheduleSection>
|
|
398
|
+
* </ScheduleCard>
|
|
399
|
+
* );
|
|
400
|
+
* }
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
263
403
|
export function ScheduleCard({
|
|
264
404
|
title,
|
|
265
405
|
nextRun,
|
|
@@ -298,7 +438,7 @@ export function ScheduleCard({
|
|
|
298
438
|
);
|
|
299
439
|
}
|
|
300
440
|
|
|
301
|
-
/**
|
|
441
|
+
/** Vertical gap between a section's caption and its control, per size. */
|
|
302
442
|
const sectionSizeClass: Record<ControlSize, string> = {
|
|
303
443
|
sm: "gap-1",
|
|
304
444
|
md: "gap-2",
|
|
@@ -311,6 +451,10 @@ const sectionLabelSizeClass: Record<ControlSize, string> = {
|
|
|
311
451
|
lg: "text-label-sm",
|
|
312
452
|
};
|
|
313
453
|
|
|
454
|
+
/**
|
|
455
|
+
* A captioned block inside the schedule card, such as "Repeat" or "Days". Renders the caption in
|
|
456
|
+
* secondary text above its children, so a card body becomes a stack of labelled controls.
|
|
457
|
+
*/
|
|
314
458
|
export function ScheduleSection({
|
|
315
459
|
label,
|
|
316
460
|
children,
|
|
@@ -318,9 +462,16 @@ export function ScheduleSection({
|
|
|
318
462
|
size = "md",
|
|
319
463
|
className,
|
|
320
464
|
}: {
|
|
465
|
+
/** Caption above the control, in secondary text. */
|
|
321
466
|
label: ReactNode;
|
|
467
|
+
/** The control the caption belongs to. */
|
|
322
468
|
children: ReactNode;
|
|
469
|
+
/**
|
|
470
|
+
* Matches the size of the controls inside the section; scales the caption rung and the caption gap.
|
|
471
|
+
* @default "md"
|
|
472
|
+
*/
|
|
323
473
|
size?: ControlSize;
|
|
474
|
+
/** Classes merged onto the block. */
|
|
324
475
|
className?: string;
|
|
325
476
|
}) {
|
|
326
477
|
return (
|
|
@@ -4,23 +4,48 @@ import { type ReactNode, useRef } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import type { ControlSize } from "../lib/size";
|
|
7
|
-
import { useActiveIndicator } from "../
|
|
7
|
+
import { useActiveIndicator } from "../hooks/use-active-indicator";
|
|
8
8
|
import { LiquidGlass } from "./liquid-glass";
|
|
9
9
|
|
|
10
10
|
/** `tab` keeps Paper's page-tab rung beside the three control sizes. */
|
|
11
11
|
export type SegmentedControlSize = ControlSize | "tab";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Track treatment of a segmented control.
|
|
15
|
+
*
|
|
16
|
+
* - `solid` — opaque surface track with a default border and a 10% ink selection.
|
|
17
|
+
* - `glass` — regular glass; the selection is a thick glass pill over it.
|
|
18
|
+
* - `inset` — 5% ink track, for use inside a routine card.
|
|
19
|
+
* - `inset-faint` — 4% ink track, for the header filter row.
|
|
20
|
+
* - `subtle` — `bg-subtle` page-tab tray whose selection is a plain surface pill.
|
|
21
|
+
*/
|
|
12
22
|
export type SegmentedControlSurface = "solid" | "glass" | "inset" | "inset-faint" | "subtle";
|
|
13
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Props for {@link SegmentedControl}. The control is single-choice: only one item value is selected.
|
|
26
|
+
*/
|
|
14
27
|
export type SegmentedControlProps = {
|
|
15
|
-
|
|
28
|
+
/** Segments in display order; `value` is what `onValueChange` returns and `disabled` dims one segment. */
|
|
29
|
+
items: {
|
|
30
|
+
/** Value reported when this segment is chosen. */
|
|
31
|
+
value: string;
|
|
32
|
+
/** Text on the segment. */
|
|
33
|
+
label: ReactNode;
|
|
34
|
+
/** Disables only this segment; it cannot be selected or focused. */
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
}[];
|
|
37
|
+
/** Selected value in controlled mode. */
|
|
16
38
|
value?: string;
|
|
39
|
+
/** Selected value at first render in uncontrolled mode. */
|
|
17
40
|
defaultValue?: string;
|
|
41
|
+
/** Fires with the chosen value when the selection changes. */
|
|
18
42
|
onValueChange?: (value: string) => void;
|
|
19
43
|
/**
|
|
20
44
|
* `md`: Segmented control (62:379), 36px track, 30px segments, 16px padding.
|
|
21
45
|
* `sm`: one rung down, a 32px track with 26px segments and 10px padding.
|
|
22
46
|
* `lg`: Segmented filter (88:723), 44px track, 36px segments.
|
|
23
47
|
* `tab`: Paper page tabs (Projects Active/Archived), 4px track padding and gap, 32px segments with 14px padding.
|
|
48
|
+
* @default "md"
|
|
24
49
|
*/
|
|
25
50
|
size?: SegmentedControlSize;
|
|
26
51
|
/**
|
|
@@ -28,11 +53,17 @@ export type SegmentedControlProps = {
|
|
|
28
53
|
* `glass` is regular glass. `inset` is the 5% ink track inside the routine card,
|
|
29
54
|
* `inset-faint` the 4% ink track inside the header filter. `subtle` is the bg-subtle page-tab tray whose
|
|
30
55
|
* selection is a plain white pill.
|
|
56
|
+
* @default "solid"
|
|
31
57
|
*/
|
|
32
58
|
surface?: SegmentedControlSurface;
|
|
33
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Segments share the width equally instead of hugging their labels.
|
|
61
|
+
* @default size === "md"
|
|
62
|
+
*/
|
|
34
63
|
fill?: boolean;
|
|
64
|
+
/** Accessible name of the toggle group. */
|
|
35
65
|
"aria-label"?: string;
|
|
66
|
+
/** Classes merged onto the track. */
|
|
36
67
|
className?: string;
|
|
37
68
|
};
|
|
38
69
|
|
|
@@ -64,6 +95,30 @@ const surfaceClass: Record<SegmentedControlSurface, string> = {
|
|
|
64
95
|
* thick fill and a 0 1 3 8% shadow, the iOS tab bar selection; dark mode keeps a
|
|
65
96
|
* 10% fill. On the solid and inset tracks it is 10% ink. The pill is the same on
|
|
66
97
|
* every rung, as in `Tabs`.
|
|
98
|
+
*
|
|
99
|
+
* The track scrolls horizontally when the segments do not fit, so a long label set never spills.
|
|
100
|
+
* Pass `aria-label` because the track has no visible name of its own.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* import { useState } from "react";
|
|
105
|
+
* import { SegmentedControl } from "@horon-ai/react";
|
|
106
|
+
*
|
|
107
|
+
* function Example() {
|
|
108
|
+
* const [view, setView] = useState("active");
|
|
109
|
+
* return (
|
|
110
|
+
* <SegmentedControl
|
|
111
|
+
* aria-label="Projects"
|
|
112
|
+
* value={view}
|
|
113
|
+
* onValueChange={setView}
|
|
114
|
+
* items={[
|
|
115
|
+
* { value: "active", label: "Active" },
|
|
116
|
+
* { value: "archived", label: "Archived" },
|
|
117
|
+
* ]}
|
|
118
|
+
* />
|
|
119
|
+
* );
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
67
122
|
*/
|
|
68
123
|
export function SegmentedControl({
|
|
69
124
|
items,
|
|
@@ -7,28 +7,65 @@ import type { ControlSize } from "../lib/size";
|
|
|
7
7
|
import { fieldBoxClass, fieldSizeClass, fieldTextClass, type InputSurface } from "./input";
|
|
8
8
|
import { inputGroupSegmentClass, useInputGroup } from "./input-group";
|
|
9
9
|
|
|
10
|
+
/** Field height and text rung of a `field` select: 32/40/48px with 14/16/18px text. */
|
|
10
11
|
export type SelectSize = ControlSize;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One choice in the list.
|
|
15
|
+
*/
|
|
11
16
|
export type SelectItemData = {
|
|
17
|
+
/** Value reported on selection. */
|
|
12
18
|
value: string;
|
|
19
|
+
/** Text shown both in the trigger and in the popup row. */
|
|
13
20
|
label: ReactNode;
|
|
14
21
|
/** Shorter text for the trigger once chosen, such as `+82` for "+82 South Korea". */
|
|
15
22
|
selectedLabel?: ReactNode;
|
|
23
|
+
/** Dims the row and blocks picking it. */
|
|
16
24
|
disabled?: boolean;
|
|
17
25
|
};
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Props for {@link Select}. Selection props (`value`, `defaultValue`, `onValueChange`, `name`) pass
|
|
29
|
+
* through to Base UI's select root.
|
|
30
|
+
*/
|
|
19
31
|
export type SelectProps = Omit<ComponentProps<typeof BaseSelect.Root>, "items" | "children"> & {
|
|
32
|
+
/** Choices in the popup, in display order. */
|
|
20
33
|
items: SelectItemData[];
|
|
34
|
+
/**
|
|
35
|
+
* Text shown in the trigger before a choice is made.
|
|
36
|
+
* @default "Select"
|
|
37
|
+
*/
|
|
21
38
|
placeholder?: ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Field height and text rung for `variant="field"`; a `chip` trigger keeps its fixed 32px,
|
|
41
|
+
* `label-md` size and still uses `size` for its popup rows, and a `segment` takes the enclosing
|
|
42
|
+
* `InputGroup` size instead.
|
|
43
|
+
* @default "md"
|
|
44
|
+
*/
|
|
22
45
|
size?: SelectSize;
|
|
46
|
+
/**
|
|
47
|
+
* Fill of the field and of its popup; `glass` also switches the popup to the veil. A `chip` uses
|
|
48
|
+
* it for the popup only, since the chip has its own subtle fill.
|
|
49
|
+
* @default "solid"
|
|
50
|
+
*/
|
|
23
51
|
surface?: InputSurface;
|
|
52
|
+
/** Paints the danger border and sets `aria-invalid` on the trigger. */
|
|
24
53
|
invalid?: boolean;
|
|
25
54
|
/**
|
|
26
55
|
* `chip` is the Paper filter chip: a 32px bg-subtle pill, 12px padding, 13/18 medium, sized to its label.
|
|
27
56
|
* `segment` is the tinted prefix or suffix of an `InputGroup`; it takes the group's size and disabled state.
|
|
57
|
+
* @default "field"
|
|
28
58
|
*/
|
|
29
59
|
variant?: "field" | "chip" | "segment";
|
|
60
|
+
/**
|
|
61
|
+
* Draws the caret at the right of the trigger; set it to false when the trigger supplies its own
|
|
62
|
+
* icon.
|
|
63
|
+
* @default true
|
|
64
|
+
*/
|
|
30
65
|
showIndicator?: boolean;
|
|
66
|
+
/** Classes merged onto the trigger. */
|
|
31
67
|
className?: string;
|
|
68
|
+
/** Accessible name of the trigger. */
|
|
32
69
|
"aria-label"?: string;
|
|
33
70
|
};
|
|
34
71
|
|
|
@@ -53,6 +90,29 @@ const triggerFocusClass = "focus-visible:outline-2 focus-visible:outline-offset-
|
|
|
53
90
|
* text size. The popup is the veil (`veil-overlay`), 20px radius, 40px rows; the chosen row
|
|
54
91
|
* is 6% ink with a check. `variant="chip"` keeps its single 32px chip size;
|
|
55
92
|
* `variant="segment"` follows its `InputGroup`.
|
|
93
|
+
*
|
|
94
|
+
* The popup is portalled and positioned under the trigger; rows are highlighted with the keyboard
|
|
95
|
+
* and marked with a check when chosen. The trigger is a button, not a native `select`, so give it an
|
|
96
|
+
* `aria-label` or associate a visible label yourself; a wrapping `<label>` does not name it.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```tsx
|
|
100
|
+
* import { Select } from "@horon-ai/react";
|
|
101
|
+
*
|
|
102
|
+
* function Example() {
|
|
103
|
+
* return (
|
|
104
|
+
* <Select
|
|
105
|
+
* aria-label="Country"
|
|
106
|
+
* defaultValue="kr"
|
|
107
|
+
* items={[
|
|
108
|
+
* { value: "kr", label: "South Korea", selectedLabel: "+82" },
|
|
109
|
+
* { value: "jp", label: "Japan", selectedLabel: "+81" },
|
|
110
|
+
* { value: "us", label: "United States", disabled: true },
|
|
111
|
+
* ]}
|
|
112
|
+
* />
|
|
113
|
+
* );
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
56
116
|
*/
|
|
57
117
|
export function Select({
|
|
58
118
|
items,
|