@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
|
@@ -7,9 +7,12 @@ 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
|
+
/** Height and text rung, shared with the other controls: `sm` 32px, `md` 40px, `lg` 48px. */
|
|
10
11
|
export type ComboboxSize = ControlSize;
|
|
11
12
|
|
|
13
|
+
/** One option in the list; the `value` is what the field reports and submits. */
|
|
12
14
|
export type ComboboxItemData = {
|
|
15
|
+
/** Stable identity, reported by `value` / `onValueChange` and submitted with the form. */
|
|
13
16
|
value: string;
|
|
14
17
|
/** Row text and the text the search matches. */
|
|
15
18
|
label: string;
|
|
@@ -19,46 +22,71 @@ export type ComboboxItemData = {
|
|
|
19
22
|
trailing?: ReactNode;
|
|
20
23
|
/** Extra search terms that are not shown, such as `KR` or `Korea`. */
|
|
21
24
|
keywords?: readonly string[];
|
|
25
|
+
/** Dims the row to 40% and makes it unselectable. */
|
|
22
26
|
disabled?: boolean;
|
|
23
27
|
};
|
|
24
28
|
|
|
29
|
+
/** Props shared by both value modes; `multiple` selects the mode in {@link ComboboxProps}. */
|
|
25
30
|
type ComboboxBaseProps = {
|
|
31
|
+
/** Options in list order, in the shape of {@link ComboboxItemData}. */
|
|
26
32
|
items: readonly ComboboxItemData[];
|
|
33
|
+
/** Text while nothing is chosen, and the field's `aria-label` in the `field` variant. @default "Search" */
|
|
27
34
|
placeholder?: string;
|
|
28
|
-
/** Placeholder of the search box inside the popup (`segment` variant). */
|
|
35
|
+
/** Placeholder of the search box inside the popup (`segment` variant). @default "Search" */
|
|
29
36
|
searchPlaceholder?: string;
|
|
30
|
-
/** Shown in the popup when nothing matches the search. */
|
|
37
|
+
/** Shown in the popup when nothing matches the search. @default "No results" */
|
|
31
38
|
emptyLabel?: ReactNode;
|
|
39
|
+
/** Rung of the field, rows and chips. @default "md" */
|
|
32
40
|
size?: ComboboxSize;
|
|
41
|
+
/** Surface of the field; the popup follows it. @default "solid" */
|
|
33
42
|
surface?: InputSurface;
|
|
34
43
|
/**
|
|
35
44
|
* `field` is a searchable field (the default). `segment` is the tinted prefix or suffix of an
|
|
36
45
|
* `InputGroup`: a button that opens a popup with the search box on top, taking the group's size.
|
|
46
|
+
* @default "field"
|
|
37
47
|
*/
|
|
38
48
|
variant?: "field" | "segment";
|
|
49
|
+
/** Turns the field or chips border danger red and sets `aria-invalid`; a `segment` trigger gets neither, because the wrapping `InputGroup` owns that border. */
|
|
39
50
|
invalid?: boolean;
|
|
51
|
+
/** Blocks the field; in a `segment` it also inherits `disabled` from the wrapping `InputGroup`. */
|
|
40
52
|
disabled?: boolean;
|
|
41
53
|
/** Form field name; the submitted value is the item `value`. */
|
|
42
54
|
name?: string;
|
|
55
|
+
/** Extra classes on the field or trigger, merged after the surface and size classes. */
|
|
43
56
|
className?: string;
|
|
57
|
+
/** Accessible name of the text field or segment trigger; the placeholder covers it in the `field` variant. */
|
|
44
58
|
"aria-label"?: string;
|
|
45
59
|
};
|
|
46
60
|
|
|
61
|
+
/** Single-select combobox: `value` / `onValueChange` carry one item `value` or `null`. */
|
|
47
62
|
export type ComboboxSingleProps = ComboboxBaseProps & {
|
|
63
|
+
/** Single-select mode; the default when the prop is omitted. */
|
|
48
64
|
multiple?: false;
|
|
65
|
+
/** Controlled choice, as an item `value`; `null` means nothing is chosen. */
|
|
49
66
|
value?: string | null;
|
|
67
|
+
/** Starting choice of the uncontrolled field. */
|
|
50
68
|
defaultValue?: string | null;
|
|
69
|
+
/** Fires with the picked item `value`, or `null` when the field is cleared. */
|
|
51
70
|
onValueChange?: (value: string | null) => void;
|
|
52
71
|
};
|
|
53
72
|
|
|
73
|
+
/** Multi-select combobox: the chosen items show as removable chips and the value is an array of item `value`s. */
|
|
54
74
|
export type ComboboxMultipleProps = ComboboxBaseProps & {
|
|
55
75
|
/** Chosen items show as removable chips in the field. */
|
|
56
76
|
multiple: true;
|
|
77
|
+
/** Controlled selection, in the order chosen. */
|
|
57
78
|
value?: readonly string[];
|
|
79
|
+
/** Starting selection of the uncontrolled field. */
|
|
58
80
|
defaultValue?: readonly string[];
|
|
81
|
+
/** Fires with the full next selection as item `value`s, in the order chosen. */
|
|
59
82
|
onValueChange?: (value: string[]) => void;
|
|
60
83
|
};
|
|
61
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Props for {@link Combobox}, a discriminated union on `multiple`: without it the field is single-select
|
|
87
|
+
* and its value is `string | null`; with `multiple: true` it is multi-select and its value is `readonly string[]`.
|
|
88
|
+
* Narrow on `props.multiple` before reading `value` or `onValueChange`.
|
|
89
|
+
*/
|
|
62
90
|
export type ComboboxProps = ComboboxSingleProps | ComboboxMultipleProps;
|
|
63
91
|
|
|
64
92
|
const itemClass: Record<ComboboxSize, string> = {
|
|
@@ -96,6 +124,28 @@ const shortLabel = (item: ComboboxItemData) => item.selectedLabel ?? item.label;
|
|
|
96
124
|
* `multiple`, several items shown as chips. The popup matches `Select`: the veil, rows with
|
|
97
125
|
* a check. `variant="segment"` puts it in an `InputGroup`, such as a country code before a
|
|
98
126
|
* phone number.
|
|
127
|
+
*
|
|
128
|
+
* Filtering is a case-insensitive substring match over each item's `label`, `value` and `keywords`,
|
|
129
|
+
* with the query trimmed first; a blank query keeps every item. The chosen item's `selectedLabel`
|
|
130
|
+
* (falling back to `label`) is what the field shows, while the form and `onValueChange` carry `value`.
|
|
131
|
+
* The popup portals to the body, opens 4px below the field and is at least as wide as it; the list scrolls
|
|
132
|
+
* past 256px and the panel caps at 320px. In `segment` form the popup is a 256px column with its own search
|
|
133
|
+
* box on top; on `multiple` the field grows and wraps as chips fill it instead of fixing its height.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```tsx
|
|
137
|
+
* const [country, setCountry] = useState<string | null>("+82");
|
|
138
|
+
*
|
|
139
|
+
* <Combobox
|
|
140
|
+
* aria-label="Country code"
|
|
141
|
+
* value={country}
|
|
142
|
+
* onValueChange={setCountry}
|
|
143
|
+
* items={[
|
|
144
|
+
* { value: "+82", label: "+82 South Korea", keywords: ["KR", "Korea"] },
|
|
145
|
+
* { value: "+1", label: "+1 United States", keywords: ["US"] },
|
|
146
|
+
* ]}
|
|
147
|
+
* />
|
|
148
|
+
* ```
|
|
99
149
|
*/
|
|
100
150
|
export function Combobox(props: ComboboxProps) {
|
|
101
151
|
const {
|
|
@@ -11,9 +11,13 @@ import { Kbd } from "./kbd";
|
|
|
11
11
|
const touchCloseClass =
|
|
12
12
|
"flex size-7 shrink-0 motion-press cursor-pointer items-center justify-center rounded-full text-text-secondary hover:bg-overlay-ink-6 hover:text-text-primary focus-visible:outline-2 focus-visible:outline-text-brand [&_svg]:size-4";
|
|
13
13
|
|
|
14
|
+
/** One selectable row of the palette. */
|
|
14
15
|
export type CommandItem = {
|
|
16
|
+
/** Stable identity and the React key of the row; keep it unique across the palette. */
|
|
15
17
|
value: string;
|
|
18
|
+
/** Row text; the query also matches it, together with `keywords`. */
|
|
16
19
|
label: string;
|
|
20
|
+
/** Extra search terms that are not shown, such as an abbreviation or a path. */
|
|
17
21
|
keywords?: string;
|
|
18
22
|
/** 28px leading tile: an initial or a 16px icon. */
|
|
19
23
|
tile?: ReactNode;
|
|
@@ -23,31 +27,76 @@ export type CommandItem = {
|
|
|
23
27
|
meta?: ReactNode;
|
|
24
28
|
/** Right-side hint, usually `<Kbd>`; hidden when the device shows no keyboard hints. */
|
|
25
29
|
hint?: ReactNode;
|
|
30
|
+
/** Runs when the row is clicked, before the palette closes itself. */
|
|
26
31
|
onSelect?: () => void;
|
|
27
32
|
};
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
/** A titled block of rows; groups are rendered in the order given. */
|
|
35
|
+
export type CommandGroup = {
|
|
36
|
+
/** Section heading, `caption` secondary text above the rows. */
|
|
37
|
+
label: string;
|
|
38
|
+
/** Rows of this section. */
|
|
39
|
+
items: CommandItem[];
|
|
40
|
+
};
|
|
30
41
|
|
|
42
|
+
/** Props for {@link CommandPalette}: the dialog state, the grouped rows and the footer hints. */
|
|
31
43
|
export type CommandPaletteProps = {
|
|
44
|
+
/** Controlled open state; leave it out to let the palette open from its `trigger`. */
|
|
32
45
|
open?: boolean;
|
|
46
|
+
/** Called with the next open state when the palette opens, closes on Escape, closes on select or its backdrop is pressed. */
|
|
33
47
|
onOpenChange?: (open: boolean) => void;
|
|
48
|
+
/** Sections of rows, in render order. */
|
|
34
49
|
groups: CommandGroup[];
|
|
50
|
+
/** Search field placeholder; it also names the dialog (`aria-label`). @default "Search" */
|
|
35
51
|
placeholder?: string;
|
|
36
52
|
/** Shown when the query has no matches; defaults to "No results". */
|
|
37
53
|
empty?: ReactNode;
|
|
38
54
|
/** Footer key hints, such as `[["↑↓", "Move"], ["↵", "Open"]]`; hidden when the device shows no keyboard hints. */
|
|
39
55
|
hints?: [ReactNode, string][];
|
|
56
|
+
/** Element rendered inside the dialog root as the opener, usually a `DialogTrigger`. */
|
|
40
57
|
trigger?: ReactNode;
|
|
41
58
|
/**
|
|
42
59
|
* `solid` (default) is an opaque palette with a default border. `glass` puts it on the veil (`veil-overlay`).
|
|
60
|
+
* @default "solid"
|
|
43
61
|
*/
|
|
44
62
|
surface?: "solid" | "glass";
|
|
63
|
+
/** Extra classes on the popup, merged after the surface, radius and motion classes. */
|
|
45
64
|
className?: string;
|
|
46
65
|
};
|
|
47
66
|
|
|
48
67
|
/**
|
|
49
68
|
* Figma `Command palette` (110:676): a 560px palette, 24px radius, with a 48px search row,
|
|
50
69
|
* grouped 44px results and a key-hint footer.
|
|
70
|
+
*
|
|
71
|
+
* A Base UI dialog on a 32% scrim with a 6px blur, anchored to the top of the viewport 96px down rather
|
|
72
|
+
* than centred. It always renders {@link CommandPaletteBody} with its own close control — an `esc` chip on
|
|
73
|
+
* devices that show keyboard hints, a 28px `X` tile on touch — and every row select reports
|
|
74
|
+
* `onOpenChange(false)`. Row hints and the footer hints disappear on those touch devices too. The body
|
|
75
|
+
* owns filtering and keyboard movement.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* const [open, setOpen] = useState(false);
|
|
80
|
+
*
|
|
81
|
+
* <CommandPalette
|
|
82
|
+
* open={open}
|
|
83
|
+
* onOpenChange={setOpen}
|
|
84
|
+
* trigger={<DialogTrigger render={<Button variant="solid" />}>Search</DialogTrigger>}
|
|
85
|
+
* groups={[
|
|
86
|
+
* {
|
|
87
|
+
* label: "Bots",
|
|
88
|
+
* items: [
|
|
89
|
+
* { value: "weekly", label: "Weekly digest", meta: "Product", hint: <Kbd>↵</Kbd> },
|
|
90
|
+
* { value: "inbox", label: "Inbox triage", meta: "Ops" },
|
|
91
|
+
* ],
|
|
92
|
+
* },
|
|
93
|
+
* ]}
|
|
94
|
+
* hints={[
|
|
95
|
+
* ["↑↓", "Move"],
|
|
96
|
+
* ["↵", "Open"],
|
|
97
|
+
* ]}
|
|
98
|
+
* />
|
|
99
|
+
* ```
|
|
51
100
|
*/
|
|
52
101
|
export function CommandPalette({
|
|
53
102
|
open,
|
|
@@ -108,17 +157,32 @@ export function CommandPalette({
|
|
|
108
157
|
);
|
|
109
158
|
}
|
|
110
159
|
|
|
160
|
+
/** Props for {@link CommandPaletteBody}: the rows plus the close control and the select callback the host owns. */
|
|
111
161
|
export type CommandPaletteBodyProps = {
|
|
162
|
+
/** Sections of rows, in render order. */
|
|
112
163
|
groups: CommandGroup[];
|
|
164
|
+
/** Search field placeholder. @default "Search" */
|
|
113
165
|
placeholder?: string;
|
|
166
|
+
/** Shown when the query has no matches. @default "No results" */
|
|
114
167
|
empty?: ReactNode;
|
|
168
|
+
/** Accessible name of the fallback `esc` button, used when no `closeControl` is passed. @default "Close" */
|
|
115
169
|
closeLabel?: string;
|
|
116
170
|
/** A ready close control; the dialog passes its own `Dialog.Close` so uncontrolled palettes still close. */
|
|
117
171
|
closeControl?: ReactNode;
|
|
172
|
+
/** Makes the fallback `esc` button close the palette; without both this and `closeControl` no button renders. */
|
|
118
173
|
onClose?: () => void;
|
|
174
|
+
/** Called after a row's own `onSelect`, so the host can close its dialog. */
|
|
119
175
|
onSelect?: () => void;
|
|
120
176
|
};
|
|
121
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The search row, groups and result rows of a command palette.
|
|
180
|
+
*
|
|
181
|
+
* Part of {@link CommandPalette}. Renders a Base UI `Autocomplete` that is always open and inline: the
|
|
182
|
+
* query filters on label plus keywords (case-insensitive), the first row is highlighted as soon as the
|
|
183
|
+
* list renders, and arrow keys move through the rows while Enter picks the highlighted one. Escape is left
|
|
184
|
+
* to the host's close control, which is why this part owns no dialog state.
|
|
185
|
+
*/
|
|
122
186
|
export function CommandPaletteBody({
|
|
123
187
|
groups,
|
|
124
188
|
placeholder = "Search",
|
|
@@ -5,21 +5,34 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import type { ControlSize } from "../lib/size";
|
|
6
6
|
import { IconButton } from "./icon-button";
|
|
7
7
|
|
|
8
|
+
/** Height and text rung of the composer: `sm` 32px, `md` 40px, `lg` 48px, shared with the other controls. */
|
|
8
9
|
export type ComposerSize = ControlSize;
|
|
10
|
+
/** `solid` is the opaque bordered pill; `glass` is the veil field for imagery and glass panels. */
|
|
9
11
|
export type ComposerSurface = "solid" | "glass";
|
|
10
12
|
|
|
13
|
+
/** Props for {@link Composer}: the message value, the send callback and the two side actions. */
|
|
11
14
|
export type ComposerProps = {
|
|
15
|
+
/** Rung of the field and its icon buttons. @default "md" */
|
|
12
16
|
size?: ComposerSize;
|
|
13
|
-
/** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. */
|
|
17
|
+
/** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. @default "solid" */
|
|
14
18
|
surface?: ComposerSurface;
|
|
19
|
+
/** Field placeholder; it doubles as the `aria-label` because the field has no visible label. @default "Message" */
|
|
15
20
|
placeholder?: string;
|
|
21
|
+
/** Controlled text. Leave it out to let the composer own the value and still report changes. */
|
|
16
22
|
value?: string;
|
|
23
|
+
/** Starting text of the uncontrolled composer. @default "" */
|
|
17
24
|
defaultValue?: string;
|
|
25
|
+
/** Fires on every keystroke with the next full text, controlled or not. */
|
|
18
26
|
onValueChange?: (value: string) => void;
|
|
27
|
+
/** Fires with the full (untrimmed) text when the form submits, from Enter or the send button; whitespace-only text blocks the submit. The field is cleared after. */
|
|
19
28
|
onSubmit?: (value: string) => void;
|
|
29
|
+
/** Fires when the plus button is pressed; the composer itself opens nothing. */
|
|
20
30
|
onAttach?: () => void;
|
|
31
|
+
/** Fires when the microphone button is pressed, i.e. while the field is empty. */
|
|
21
32
|
onVoice?: () => void;
|
|
33
|
+
/** Disables the field and both icon buttons without changing the layout. */
|
|
22
34
|
disabled?: boolean;
|
|
35
|
+
/** Extra classes on the form, merged after the surface and size classes. */
|
|
23
36
|
className?: string;
|
|
24
37
|
};
|
|
25
38
|
|
|
@@ -39,6 +52,20 @@ const textClass: Record<ComposerSize, string> = {
|
|
|
39
52
|
* Figma `Composer` (63:462): a regular-glass pill with an attach button, the
|
|
40
53
|
* message field, and a voice button that turns into the ink send button once
|
|
41
54
|
* there is text.
|
|
55
|
+
*
|
|
56
|
+
* A real `form`, so Enter submits and the browser handles implicit submission; a value that is only
|
|
57
|
+
* whitespace blocks the send and leaves the typed text in place. The last slot swaps between the voice
|
|
58
|
+
* button (empty field) and the primary send button (`motion-swap`), and only the visible one is focusable.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <Composer
|
|
63
|
+
* placeholder="Ask anything"
|
|
64
|
+
* onAttach={() => openFilePicker()}
|
|
65
|
+
* onVoice={() => startDictation()}
|
|
66
|
+
* onSubmit={(text) => sendMessage(text)}
|
|
67
|
+
* />
|
|
68
|
+
* ```
|
|
42
69
|
*/
|
|
43
70
|
export function Composer({
|
|
44
71
|
size = "md",
|
|
@@ -26,19 +26,51 @@ import { Checkbox } from "./checkbox";
|
|
|
26
26
|
import { IconButton } from "./icon-button";
|
|
27
27
|
import { Input } from "./input";
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/** Strips an element's own `className` so a wrapper can merge its classes with the caller's. */
|
|
30
|
+
type WithClass<T> = Omit<T, "className"> & {
|
|
31
|
+
/** Extra classes merged last onto the element, after the component's own classes. */
|
|
32
|
+
className?: string;
|
|
33
|
+
};
|
|
30
34
|
|
|
35
|
+
/** Viewport the table is designed for: `desktop` and `tablet` keep 24px card gutters, `mobile` narrows them to 18px. */
|
|
31
36
|
export type DataTableDevice = "desktop" | "tablet" | "mobile";
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* Figma `Data table`: a solid surface card, 20px radius, 1px default border.
|
|
35
40
|
* `device` sets the gutters: 24px on desktop and tablet, 18px on mobile (where the table scrolls sideways).
|
|
41
|
+
*
|
|
42
|
+
* The card is the context every part reads: it publishes `data-device`, which the header, cells and footer
|
|
43
|
+
* use to switch their padding, and `group/dt`, so the parts can be laid out in any order inside it.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* function BotsTable({ bots }: { bots: { id: string; name: string; owner: string }[] }) {
|
|
48
|
+
* const table = useDataTable({
|
|
49
|
+
* data: bots,
|
|
50
|
+
* columns: [
|
|
51
|
+
* { accessorKey: "name", header: "Name" },
|
|
52
|
+
* { accessorKey: "owner", header: "Owner" },
|
|
53
|
+
* ],
|
|
54
|
+
* pageSize: 10,
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* return (
|
|
58
|
+
* <DataTable>
|
|
59
|
+
* <DataTableView table={table} />
|
|
60
|
+
* <DataTablePagination table={table} />
|
|
61
|
+
* </DataTable>
|
|
62
|
+
* );
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
36
65
|
*/
|
|
37
66
|
export function DataTable({
|
|
38
67
|
device = "desktop",
|
|
39
68
|
className,
|
|
40
69
|
...props
|
|
41
|
-
}: WithClass<ComponentProps<"div">> & {
|
|
70
|
+
}: WithClass<ComponentProps<"div">> & {
|
|
71
|
+
/** Viewport the table is designed for. @default "desktop" */
|
|
72
|
+
device?: DataTableDevice;
|
|
73
|
+
}) {
|
|
42
74
|
return (
|
|
43
75
|
<div
|
|
44
76
|
data-device={device}
|
|
@@ -51,15 +83,24 @@ export function DataTable({
|
|
|
51
83
|
);
|
|
52
84
|
}
|
|
53
85
|
|
|
86
|
+
/** Props for {@link DataTableHeader}: an optional leading icon, the heading and trailing actions. */
|
|
54
87
|
export type DataTableHeaderProps = {
|
|
55
88
|
/** 16px icon on the 26px brand tile. */
|
|
56
89
|
icon?: ReactNode;
|
|
90
|
+
/** Card heading, `label-lg` primary text on one truncated line. */
|
|
57
91
|
title: ReactNode;
|
|
58
92
|
/** Trailing 16px icon buttons. */
|
|
59
93
|
actions?: ReactNode;
|
|
94
|
+
/** Extra classes on the header row, merged after the padding classes. */
|
|
60
95
|
className?: string;
|
|
61
96
|
};
|
|
62
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The card's top row: leading icon tile, title and trailing actions, 24px gutters (18px on mobile).
|
|
100
|
+
*
|
|
101
|
+
* Part of {@link DataTable}. The actions column is `text-secondary` with 16px icons and pulls 6px into the
|
|
102
|
+
* header's vertical padding, so an icon button keeps its own footprint without inflating the row.
|
|
103
|
+
*/
|
|
63
104
|
export function DataTableHeader({ icon, title, actions, className }: DataTableHeaderProps) {
|
|
64
105
|
return (
|
|
65
106
|
<div className={cn("flex items-center gap-3 px-6 py-4.5 group-data-[device=mobile]/dt:px-4.5", className)}>
|
|
@@ -76,6 +117,12 @@ export function DataTableHeader({ icon, title, actions, className }: DataTableHe
|
|
|
76
117
|
);
|
|
77
118
|
}
|
|
78
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The scroll container and the `<table>` inside it, with collapsed borders.
|
|
122
|
+
*
|
|
123
|
+
* Part of {@link DataTable}. On `device="mobile"` the table switches to its min-content width so the
|
|
124
|
+
* wrapper scrolls sideways instead of squeezing columns; above that it fills the card.
|
|
125
|
+
*/
|
|
79
126
|
export function DataTableTable({ className, ...props }: WithClass<ComponentProps<"table">>) {
|
|
80
127
|
return (
|
|
81
128
|
<div className="w-full max-w-full min-w-0 overflow-x-auto">
|
|
@@ -87,13 +134,21 @@ export function DataTableTable({ className, ...props }: WithClass<ComponentProps
|
|
|
87
134
|
);
|
|
88
135
|
}
|
|
89
136
|
|
|
90
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Header cell: 52px, 14px icon and label-md secondary text.
|
|
139
|
+
*
|
|
140
|
+
* Part of {@link DataTable}. It draws the top and bottom hairlines and 24px outer gutters (18px on mobile);
|
|
141
|
+
* `DataTableView` passes `aria-sort` here for a sortable column, which is why it stays a plain `<th>`.
|
|
142
|
+
*/
|
|
91
143
|
export function DataTableHead({
|
|
92
144
|
icon,
|
|
93
145
|
className,
|
|
94
146
|
children,
|
|
95
147
|
...props
|
|
96
|
-
}: WithClass<ComponentProps<"th">> & {
|
|
148
|
+
}: WithClass<ComponentProps<"th">> & {
|
|
149
|
+
/** 14px icon before the label, rendered inside the cell's inline label row. */
|
|
150
|
+
icon?: ReactNode;
|
|
151
|
+
}) {
|
|
97
152
|
return (
|
|
98
153
|
<th
|
|
99
154
|
className={cn(
|
|
@@ -110,11 +165,17 @@ export function DataTableHead({
|
|
|
110
165
|
);
|
|
111
166
|
}
|
|
112
167
|
|
|
168
|
+
/** Body row: one bottom hairline; `DataTableView` adds the hover, selected and staggered rise classes. */
|
|
113
169
|
export function DataTableRow({ className, ...props }: WithClass<ComponentProps<"tr">>) {
|
|
114
170
|
return <tr className={cn("border-b border-border-default", className)} {...props} />;
|
|
115
171
|
}
|
|
116
172
|
|
|
117
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Body cell: 52px; body-sm secondary text unless the content sets its own.
|
|
175
|
+
*
|
|
176
|
+
* Part of {@link DataTable}. It keeps text on one line, uses the same 24/18px outer gutters as the head,
|
|
177
|
+
* and turns a direct `<label>` into a flex box so a checkbox and its text line up in the cell.
|
|
178
|
+
*/
|
|
118
179
|
export function DataTableCell({ className, ...props }: WithClass<ComponentProps<"td">>) {
|
|
119
180
|
return (
|
|
120
181
|
<td
|
|
@@ -127,8 +188,19 @@ export function DataTableCell({ className, ...props }: WithClass<ComponentProps<
|
|
|
127
188
|
);
|
|
128
189
|
}
|
|
129
190
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
191
|
+
/**
|
|
192
|
+
* The 24px ink logo tile with a label-sm initial, beside a label-lg name. Part of {@link DataTable}.
|
|
193
|
+
*
|
|
194
|
+
* prettier-ignore keeps the parameter pattern identical to the original instead of letting the formatter
|
|
195
|
+
* expand the destructured parameter onto separate lines.
|
|
196
|
+
*/
|
|
197
|
+
// prettier-ignore
|
|
198
|
+
export function DataTableItem({ logo, children }: {
|
|
199
|
+
/** Tile content: a letter, or an icon the caller sizes (the tile is 24px square). */
|
|
200
|
+
logo: ReactNode;
|
|
201
|
+
/** The row's name, `label-lg` primary text truncated to the cell. */
|
|
202
|
+
children: ReactNode;
|
|
203
|
+
}) {
|
|
132
204
|
return (
|
|
133
205
|
<span className="flex items-center gap-2.5">
|
|
134
206
|
<span className="flex size-6 shrink-0 items-center justify-center rounded-xs bg-surface-ink text-label-sm text-text-on-ink">
|
|
@@ -139,12 +211,18 @@ export function DataTableItem({ logo, children }: { logo: ReactNode; children: R
|
|
|
139
211
|
);
|
|
140
212
|
}
|
|
141
213
|
|
|
142
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Figma `Table tag`: brand (sky tint) or neutral (subtle) label, 8px radius.
|
|
216
|
+
* Part of {@link DataTable}; it renders a non-interactive `span` so it fits inside a cell's text flow.
|
|
217
|
+
*/
|
|
143
218
|
export function DataTableTag({
|
|
144
219
|
tone = "brand",
|
|
145
220
|
className,
|
|
146
221
|
...props
|
|
147
|
-
}: WithClass<ComponentProps<"span">> & {
|
|
222
|
+
}: WithClass<ComponentProps<"span">> & {
|
|
223
|
+
/** `brand` is the sky tint with brand text; `neutral` is the subtle fill with secondary text. @default "brand" */
|
|
224
|
+
tone?: "brand" | "neutral";
|
|
225
|
+
}) {
|
|
148
226
|
return (
|
|
149
227
|
<span
|
|
150
228
|
className={cn(
|
|
@@ -157,9 +235,22 @@ export function DataTableTag({
|
|
|
157
235
|
);
|
|
158
236
|
}
|
|
159
237
|
|
|
160
|
-
|
|
238
|
+
/** Props for {@link DataTableFooter}: the add-row action and an optional caption hint. */
|
|
239
|
+
export type DataTableFooterProps = {
|
|
240
|
+
/** Fires when the add-row row is pressed; the footer itself adds nothing. */
|
|
241
|
+
onAdd?: () => void;
|
|
242
|
+
/** Text of the add action, `label-md` secondary. @default "Add row" */
|
|
243
|
+
addLabel?: string;
|
|
244
|
+
/** Caption at the right edge, such as a total or a keyboard hint. */
|
|
245
|
+
hint?: ReactNode;
|
|
246
|
+
/** Extra classes on the footer row, merged after the padding classes. */
|
|
247
|
+
className?: string;
|
|
248
|
+
};
|
|
161
249
|
|
|
162
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* Footer: "Add row" with a 16px plus and a caption hint on the right.
|
|
252
|
+
* Part of {@link DataTable}. The add action takes the free width, so the hint stays pinned right; both wrap onto a second line when narrow.
|
|
253
|
+
*/
|
|
163
254
|
export function DataTableFooter({ onAdd, addLabel = "Add row", hint, className }: DataTableFooterProps) {
|
|
164
255
|
return (
|
|
165
256
|
<div
|
|
@@ -181,13 +272,17 @@ export function DataTableFooter({ onAdd, addLabel = "Add row", hint, className }
|
|
|
181
272
|
);
|
|
182
273
|
}
|
|
183
274
|
|
|
275
|
+
/** The `meta` a horon column can carry; `DataTableView` reads it for the head icon and the two class hooks. */
|
|
184
276
|
export type DataTableColumnMeta = {
|
|
185
277
|
/** 14px icon before the header label. */
|
|
186
278
|
icon?: ReactNode;
|
|
279
|
+
/** Extra classes on this column's header cell, such as a width. */
|
|
187
280
|
headClassName?: string;
|
|
281
|
+
/** Extra classes on each body cell of this column. */
|
|
188
282
|
cellClassName?: string;
|
|
189
283
|
};
|
|
190
284
|
|
|
285
|
+
/** Empty meta object that gives `columnMeta` its type, so `meta` in a column def is checked against the shape above. */
|
|
191
286
|
const columnMeta: DataTableColumnMeta = {};
|
|
192
287
|
|
|
193
288
|
/** TanStack Table features every horon data table registers: sorting, filtering, selection, pagination. */
|
|
@@ -205,20 +300,54 @@ export const dataTableFeatures = tableFeatures({
|
|
|
205
300
|
columnMeta,
|
|
206
301
|
});
|
|
207
302
|
|
|
303
|
+
/** The feature set `dataTableFeatures` builds; the type every column and table instance is parameterised by. */
|
|
208
304
|
export type DataTableFeatures = typeof dataTableFeatures;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* A column definition bound to the horon feature set and to the row type.
|
|
308
|
+
* @typeParam TData - Shape of one row; derive it from the data array so `accessorKey` stays checked.
|
|
309
|
+
*/
|
|
209
310
|
export type DataTableColumn<TData extends RowData> = ColumnDef<DataTableFeatures, TData>;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The table instance `useDataTable` returns; pass it to {@link DataTableView}, {@link DataTableSearch} and {@link DataTablePagination}.
|
|
314
|
+
* @typeParam TData - Shape of one row, the same one the columns were built from.
|
|
315
|
+
*/
|
|
210
316
|
export type DataTableInstance<TData extends RowData> = ReactTable<DataTableFeatures, TData>;
|
|
211
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Options for {@link useDataTable}.
|
|
320
|
+
* @typeParam TData - Shape of one row.
|
|
321
|
+
*/
|
|
212
322
|
export type UseDataTableOptions<TData extends RowData> = {
|
|
323
|
+
/** Rows to display; the table never mutates them. */
|
|
213
324
|
data: TData[];
|
|
325
|
+
/** Column definitions, in display order; see {@link DataTableColumn}. */
|
|
214
326
|
columns: DataTableColumn<TData>[];
|
|
327
|
+
/** Derives a stable row id from the row and its index; the default id is the row index. */
|
|
215
328
|
getRowId?: (row: TData, index: number) => string;
|
|
216
329
|
/** Rows per page. Omit it to show every row on one page. */
|
|
217
330
|
pageSize?: number;
|
|
331
|
+
/** Makes every row selectable, which is what the checkbox column from {@link dataTableSelectColumn} toggles. @default false */
|
|
218
332
|
enableRowSelection?: boolean;
|
|
219
333
|
};
|
|
220
334
|
|
|
221
|
-
/**
|
|
335
|
+
/**
|
|
336
|
+
* Builds the TanStack table behind `DataTableView`, `DataTableSearch` and `DataTablePagination`.
|
|
337
|
+
*
|
|
338
|
+
* Registers {@link dataTableFeatures} and starts on page 1 with the given `pageSize` (10 by default in
|
|
339
|
+
* state). Without `pageSize` it turns on manual pagination, so the paginated row model is skipped and every
|
|
340
|
+
* row renders.
|
|
341
|
+
*
|
|
342
|
+
* @typeParam TData - Shape of one row.
|
|
343
|
+
* @param options - The table options; see {@link UseDataTableOptions}.
|
|
344
|
+
* @param options.data - Rows to display.
|
|
345
|
+
* @param options.columns - Column definitions, in display order.
|
|
346
|
+
* @param options.getRowId - Derives a stable row id; the row index is used when omitted.
|
|
347
|
+
* @param options.pageSize - Rows per page; omit it to show every row on one page.
|
|
348
|
+
* @param options.enableRowSelection - Makes rows selectable for the checkbox column.
|
|
349
|
+
* @returns A table instance to pass to the table parts.
|
|
350
|
+
*/
|
|
222
351
|
export function useDataTable<TData extends RowData>({
|
|
223
352
|
data,
|
|
224
353
|
columns,
|
|
@@ -237,7 +366,16 @@ export function useDataTable<TData extends RowData>({
|
|
|
237
366
|
});
|
|
238
367
|
}
|
|
239
368
|
|
|
240
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* Leading checkbox column; the header box selects every row on the current page.
|
|
371
|
+
*
|
|
372
|
+
* Part of the {@link DataTable} set. The header box reports indeterminate when only some page rows are
|
|
373
|
+
* selected, each cell box is labelled "Select row <n>" and is disabled when the table does not allow
|
|
374
|
+
* selecting that row, and the column is pinned 48px wide and unsortable.
|
|
375
|
+
*
|
|
376
|
+
* @typeParam TData - Shape of one row.
|
|
377
|
+
* @returns A column definition to put first in the `columns` array; it reads the table's own selection state.
|
|
378
|
+
*/
|
|
241
379
|
export function dataTableSelectColumn<TData extends RowData>(): DataTableColumn<TData> {
|
|
242
380
|
return {
|
|
243
381
|
id: "select",
|
|
@@ -269,13 +407,29 @@ export function dataTableSelectColumn<TData extends RowData>(): DataTableColumn<
|
|
|
269
407
|
|
|
270
408
|
const ariaSort = { asc: "ascending", desc: "descending" } as const;
|
|
271
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Props for {@link DataTableView}.
|
|
412
|
+
* @typeParam TData - Shape of one row.
|
|
413
|
+
*/
|
|
272
414
|
export type DataTableViewProps<TData extends RowData> = {
|
|
415
|
+
/** Instance from {@link useDataTable}. */
|
|
273
416
|
table: DataTableInstance<TData>;
|
|
417
|
+
/** Cell shown when the filtered rows are empty. @default "No results." */
|
|
274
418
|
emptyMessage?: ReactNode;
|
|
419
|
+
/** Extra classes on the `<table>`. */
|
|
275
420
|
className?: string;
|
|
276
421
|
};
|
|
277
422
|
|
|
278
|
-
/**
|
|
423
|
+
/**
|
|
424
|
+
* Renders a `useDataTable` instance with the Figma head and body cells; sortable heads toggle on click.
|
|
425
|
+
*
|
|
426
|
+
* A sortable head is a button with a three-state caret (none, ascending, descending) and sets `aria-sort` on
|
|
427
|
+
* the `<th>`; a non-sortable head shows the column's `meta.icon` instead. Rows rise in 30ms apart, hover with
|
|
428
|
+
* a 4% ink wash, and a selected row keeps the wash on its cells. A sort or page change remounts the
|
|
429
|
+
* body so the stagger replays. An empty row model renders one full-width cell with `emptyMessage`.
|
|
430
|
+
*
|
|
431
|
+
* @typeParam TData - Shape of one row, matching the instance.
|
|
432
|
+
*/
|
|
279
433
|
export function DataTableView<TData extends RowData>({
|
|
280
434
|
table,
|
|
281
435
|
emptyMessage = "No results.",
|
|
@@ -358,13 +512,27 @@ export function DataTableView<TData extends RowData>({
|
|
|
358
512
|
);
|
|
359
513
|
}
|
|
360
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Props for {@link DataTableSearch}.
|
|
517
|
+
* @typeParam TData - Shape of one row.
|
|
518
|
+
*/
|
|
361
519
|
export type DataTableSearchProps<TData extends RowData> = {
|
|
520
|
+
/** Instance from {@link useDataTable}. */
|
|
362
521
|
table: DataTableInstance<TData>;
|
|
522
|
+
/** Field placeholder; it is also the `aria-label`. @default "Search" */
|
|
363
523
|
placeholder?: string;
|
|
524
|
+
/** Extra classes on the input, merged after `min-w-0`. */
|
|
364
525
|
className?: string;
|
|
365
526
|
};
|
|
366
527
|
|
|
367
|
-
/**
|
|
528
|
+
/**
|
|
529
|
+
* Solid search field bound to the table's global filter.
|
|
530
|
+
* Part of the {@link DataTable} set: it is a small `Input` with a magnifier, writes every keystroke into the
|
|
531
|
+
* table's global filter and reads the current value back from the table state, so it stays in sync when
|
|
532
|
+
* something else clears the filter.
|
|
533
|
+
*
|
|
534
|
+
* @typeParam TData - Shape of one row.
|
|
535
|
+
*/
|
|
368
536
|
export function DataTableSearch<TData extends RowData>({
|
|
369
537
|
table,
|
|
370
538
|
placeholder = "Search",
|
|
@@ -384,12 +552,26 @@ export function DataTableSearch<TData extends RowData>({
|
|
|
384
552
|
);
|
|
385
553
|
}
|
|
386
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Props for {@link DataTablePagination}.
|
|
557
|
+
* @typeParam TData - Shape of one row.
|
|
558
|
+
*/
|
|
387
559
|
export type DataTablePaginationProps<TData extends RowData> = {
|
|
560
|
+
/** Instance from {@link useDataTable}. */
|
|
388
561
|
table: DataTableInstance<TData>;
|
|
562
|
+
/** Extra classes on the row, merged after the padding classes. */
|
|
389
563
|
className?: string;
|
|
390
564
|
};
|
|
391
565
|
|
|
392
|
-
/**
|
|
566
|
+
/**
|
|
567
|
+
* Row count (or selection count) with previous and next page buttons.
|
|
568
|
+
*
|
|
569
|
+
* Part of the {@link DataTable} set. With a selection it reads "n of m selected", otherwise "n rows"
|
|
570
|
+
* (singular for one); the page label always shows at least one page, and each arrow is disabled at its end
|
|
571
|
+
* of the range. The count follows the filtered rows, not the raw data.
|
|
572
|
+
*
|
|
573
|
+
* @typeParam TData - Shape of one row.
|
|
574
|
+
*/
|
|
393
575
|
export function DataTablePagination<TData extends RowData>({ table, className }: DataTablePaginationProps<TData>) {
|
|
394
576
|
const total = table.getFilteredRowModel().rows.length;
|
|
395
577
|
const selected = table.getFilteredSelectedRowModel().rows.length;
|