@horon-ai/react 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -2
- package/package.json +50 -5
- package/src/assets/sidebar-photo.jpg +0 -0
- package/src/components/agent-activity.tsx +204 -0
- package/src/components/agent-lane.tsx +80 -0
- package/src/components/agent-lanes-timeline.tsx +312 -0
- package/src/components/agent-panel.tsx +125 -0
- package/src/components/agent-steering-panel.tsx +276 -0
- package/src/components/approval-card.tsx +89 -0
- package/src/components/avatar.tsx +104 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/banner.tsx +103 -0
- package/src/components/bot-instructions-panel.tsx +369 -0
- package/src/components/bot-list-row.tsx +42 -0
- package/src/components/bot-memory-panel.tsx +229 -0
- package/src/components/bottom-sheet.tsx +140 -0
- package/src/components/breadcrumb-header.tsx +87 -0
- package/src/components/breadcrumb.tsx +86 -0
- package/src/components/button.tsx +146 -0
- package/src/components/calendar.tsx +410 -0
- package/src/components/card.tsx +79 -0
- package/src/components/chat-bubble.tsx +36 -0
- package/src/components/chat-controls.tsx +118 -0
- package/src/components/checkbox.tsx +73 -0
- package/src/components/chip.tsx +81 -0
- package/src/components/choice-card.tsx +55 -0
- package/src/components/code-panel.tsx +27 -0
- package/src/components/combobox.tsx +312 -0
- package/src/components/command-palette.tsx +206 -0
- package/src/components/composer.tsx +124 -0
- package/src/components/data-table.tsx +429 -0
- package/src/components/date-field.tsx +100 -0
- package/src/components/dialog.tsx +93 -0
- package/src/components/directory-card.tsx +59 -0
- package/src/components/document-extract-panel.tsx +398 -0
- package/src/components/empty-state.tsx +29 -0
- package/src/components/field.tsx +78 -0
- package/src/components/file-drop.tsx +89 -0
- package/src/components/filter-bar.tsx +83 -0
- package/src/components/flue-background.tsx +388 -0
- package/src/components/horon-icon.tsx +280 -0
- package/src/components/horon-wordmark.tsx +26 -0
- package/src/components/icon-button.tsx +61 -0
- package/src/components/input-group.tsx +112 -0
- package/src/components/input.tsx +72 -0
- package/src/components/integration-row.tsx +103 -0
- package/src/components/kbd.tsx +42 -0
- package/src/components/knowledge-base-panel.tsx +185 -0
- package/src/components/liquid-glass.tsx +24 -0
- package/src/components/list-detail.tsx +275 -0
- package/src/components/marketplace-card.tsx +97 -0
- package/src/components/mcp-server-row.tsx +122 -0
- package/src/components/menu.tsx +165 -0
- package/src/components/message-extras.tsx +146 -0
- package/src/components/modal.tsx +108 -0
- package/src/components/orchestrator-panel.tsx +175 -0
- package/src/components/page-header.tsx +48 -0
- package/src/components/plan-card.tsx +70 -0
- package/src/components/plan-step.tsx +111 -0
- package/src/components/plugin-row.tsx +128 -0
- package/src/components/progress.tsx +21 -0
- package/src/components/radio-group.tsx +84 -0
- package/src/components/research-run-panel.tsx +232 -0
- package/src/components/schedule.tsx +332 -0
- package/src/components/segmented-control.tsx +136 -0
- package/src/components/select.tsx +154 -0
- package/src/components/settings-list.tsx +99 -0
- package/src/components/sidebar.tsx +542 -0
- package/src/components/skeleton.tsx +75 -0
- package/src/components/slider.tsx +29 -0
- package/src/components/source-row.tsx +112 -0
- package/src/components/spinner.tsx +29 -0
- package/src/components/split-button.tsx +72 -0
- package/src/components/switch.tsx +79 -0
- package/src/components/tab-bar.tsx +90 -0
- package/src/components/tabs.tsx +162 -0
- package/src/components/task-row.tsx +159 -0
- package/src/components/textarea-field.tsx +127 -0
- package/src/components/textarea.tsx +47 -0
- package/src/components/toast.tsx +128 -0
- package/src/components/tool-call.tsx +215 -0
- package/src/components/tool-calls-header.tsx +65 -0
- package/src/components/tool-permissions.tsx +105 -0
- package/src/components/tooltip.tsx +33 -0
- package/src/components/top-bar.tsx +65 -0
- package/src/components/usage-meter.tsx +47 -0
- package/src/components/workspace-menu.tsx +77 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/use-active-indicator.ts +60 -0
- package/src/hooks/use-color-scheme.ts +27 -0
- package/src/hooks/use-copy-to-clipboard.ts +32 -0
- package/src/hooks/use-device.ts +41 -0
- package/src/hooks/use-hotkey.ts +35 -0
- package/src/hooks/use-media-query.ts +25 -0
- package/src/hooks/use-platform.ts +39 -0
- package/src/hooks/use-reduced-motion.ts +8 -0
- package/src/index.ts +99 -0
- package/src/lib/cn.ts +25 -0
- package/src/lib/cursor-ghost.ts +244 -0
- package/src/lib/platform.ts +169 -0
- package/src/lib/size.ts +10 -0
- package/src/styles/globals.css +564 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { CaretDown, CaretLeft, CaretRight, CaretUp, CaretUpDown, MagnifyingGlass, Plus } from "@phosphor-icons/react";
|
|
2
|
+
import {
|
|
3
|
+
columnFilteringFeature,
|
|
4
|
+
createFilteredRowModel,
|
|
5
|
+
createPaginatedRowModel,
|
|
6
|
+
createSortedRowModel,
|
|
7
|
+
filterFn_includesString,
|
|
8
|
+
globalFilteringFeature,
|
|
9
|
+
rowPaginationFeature,
|
|
10
|
+
rowSelectionFeature,
|
|
11
|
+
rowSortingFeature,
|
|
12
|
+
sortFn_alphanumeric,
|
|
13
|
+
sortFn_basic,
|
|
14
|
+
sortFn_datetime,
|
|
15
|
+
sortFn_text,
|
|
16
|
+
tableFeatures,
|
|
17
|
+
useTable,
|
|
18
|
+
type ColumnDef,
|
|
19
|
+
type ReactTable,
|
|
20
|
+
type RowData,
|
|
21
|
+
} from "@tanstack/react-table";
|
|
22
|
+
import type { ComponentProps, CSSProperties, ReactNode } from "react";
|
|
23
|
+
|
|
24
|
+
import { cn } from "../lib/cn";
|
|
25
|
+
import { Checkbox } from "./checkbox";
|
|
26
|
+
import { IconButton } from "./icon-button";
|
|
27
|
+
import { Input } from "./input";
|
|
28
|
+
|
|
29
|
+
type WithClass<T> = Omit<T, "className"> & { className?: string };
|
|
30
|
+
|
|
31
|
+
export type DataTableDevice = "desktop" | "tablet" | "mobile";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Figma `Data table`: a solid surface card, 20px radius, 1px default border.
|
|
35
|
+
* `device` sets the gutters: 24px on desktop and tablet, 18px on mobile (where the table scrolls sideways).
|
|
36
|
+
*/
|
|
37
|
+
export function DataTable({
|
|
38
|
+
device = "desktop",
|
|
39
|
+
className,
|
|
40
|
+
...props
|
|
41
|
+
}: WithClass<ComponentProps<"div">> & { device?: DataTableDevice }) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-device={device}
|
|
45
|
+
className={cn(
|
|
46
|
+
"group/dt flex min-w-0 flex-col overflow-clip rounded-xl border border-border-default bg-bg-surface",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type DataTableHeaderProps = {
|
|
55
|
+
/** 16px icon on the 26px brand tile. */
|
|
56
|
+
icon?: ReactNode;
|
|
57
|
+
title: ReactNode;
|
|
58
|
+
/** Trailing 16px icon buttons. */
|
|
59
|
+
actions?: ReactNode;
|
|
60
|
+
className?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function DataTableHeader({ icon, title, actions, className }: DataTableHeaderProps) {
|
|
64
|
+
return (
|
|
65
|
+
<div className={cn("flex items-center gap-3 px-6 py-4.5 group-data-[device=mobile]/dt:px-4.5", className)}>
|
|
66
|
+
{icon && (
|
|
67
|
+
<span className="flex size-6.5 shrink-0 items-center justify-center rounded-[7px] bg-bg-brand text-text-brand [&>svg]:size-4">
|
|
68
|
+
{icon}
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
71
|
+
<h3 className="min-w-0 flex-1 truncate text-label-lg text-text-primary">{title}</h3>
|
|
72
|
+
{actions && (
|
|
73
|
+
<div className="-my-1.5 flex shrink-0 items-center gap-3 text-text-secondary [&_svg]:size-4">{actions}</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function DataTableTable({ className, ...props }: WithClass<ComponentProps<"table">>) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="w-full max-w-full min-w-0 overflow-x-auto">
|
|
82
|
+
<table
|
|
83
|
+
className={cn("w-full border-collapse text-left group-data-[device=mobile]/dt:min-w-max", className)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Header cell: 52px, 14px icon and label-md secondary text. */
|
|
91
|
+
export function DataTableHead({
|
|
92
|
+
icon,
|
|
93
|
+
className,
|
|
94
|
+
children,
|
|
95
|
+
...props
|
|
96
|
+
}: WithClass<ComponentProps<"th">> & { icon?: ReactNode }) {
|
|
97
|
+
return (
|
|
98
|
+
<th
|
|
99
|
+
className={cn(
|
|
100
|
+
"font-normal h-13 border-y border-border-default pr-3 align-middle first:pl-6 group-data-[device=mobile]/dt:first:pl-4.5 last:pr-6 group-data-[device=mobile]/dt:last:pr-4.5 [&_svg]:size-3.5",
|
|
101
|
+
className,
|
|
102
|
+
)}
|
|
103
|
+
{...props}
|
|
104
|
+
>
|
|
105
|
+
<span className="flex items-center gap-2 text-label-md whitespace-nowrap text-text-secondary">
|
|
106
|
+
{icon}
|
|
107
|
+
{children}
|
|
108
|
+
</span>
|
|
109
|
+
</th>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function DataTableRow({ className, ...props }: WithClass<ComponentProps<"tr">>) {
|
|
114
|
+
return <tr className={cn("border-b border-border-default", className)} {...props} />;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Body cell: 52px; body-sm secondary text unless the content sets its own. */
|
|
118
|
+
export function DataTableCell({ className, ...props }: WithClass<ComponentProps<"td">>) {
|
|
119
|
+
return (
|
|
120
|
+
<td
|
|
121
|
+
className={cn(
|
|
122
|
+
"h-13 pr-3 align-middle text-body-sm whitespace-nowrap text-text-secondary first:pl-6 group-data-[device=mobile]/dt:first:pl-4.5 last:pr-6 group-data-[device=mobile]/dt:last:pr-4.5 [&>label]:flex",
|
|
123
|
+
className,
|
|
124
|
+
)}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The 24px ink logo tile with a label-sm initial, beside a label-lg name. */
|
|
131
|
+
export function DataTableItem({ logo, children }: { logo: ReactNode; children: ReactNode }) {
|
|
132
|
+
return (
|
|
133
|
+
<span className="flex items-center gap-2.5">
|
|
134
|
+
<span className="flex size-6 shrink-0 items-center justify-center rounded-xs bg-surface-ink text-label-sm text-text-on-ink">
|
|
135
|
+
{logo}
|
|
136
|
+
</span>
|
|
137
|
+
<span className="truncate text-label-lg text-text-primary">{children}</span>
|
|
138
|
+
</span>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Figma `Table tag`: brand (sky tint) or neutral (subtle) label, 8px radius. */
|
|
143
|
+
export function DataTableTag({
|
|
144
|
+
tone = "brand",
|
|
145
|
+
className,
|
|
146
|
+
...props
|
|
147
|
+
}: WithClass<ComponentProps<"span">> & { tone?: "brand" | "neutral" }) {
|
|
148
|
+
return (
|
|
149
|
+
<span
|
|
150
|
+
className={cn(
|
|
151
|
+
"inline-flex h-6.5 items-center rounded-sm px-2.5 text-label-md whitespace-nowrap",
|
|
152
|
+
tone === "brand" ? "bg-bg-brand text-text-brand" : "bg-bg-subtle text-text-secondary",
|
|
153
|
+
className,
|
|
154
|
+
)}
|
|
155
|
+
{...props}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type DataTableFooterProps = { onAdd?: () => void; addLabel?: string; hint?: ReactNode; className?: string };
|
|
161
|
+
|
|
162
|
+
/** Footer: "Add row" with a 16px plus and a caption hint on the right. */
|
|
163
|
+
export function DataTableFooter({ onAdd, addLabel = "Add row", hint, className }: DataTableFooterProps) {
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
className={cn(
|
|
167
|
+
"flex flex-wrap items-center gap-x-2 gap-y-1 px-6 pt-3.5 pb-4 group-data-[device=mobile]/dt:px-4.5",
|
|
168
|
+
className,
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={onAdd}
|
|
174
|
+
className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 text-label-md text-text-secondary motion-colors focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
175
|
+
>
|
|
176
|
+
<Plus aria-hidden className="size-4" />
|
|
177
|
+
{addLabel}
|
|
178
|
+
</button>
|
|
179
|
+
{hint && <span className="shrink-0 text-caption text-text-secondary">{hint}</span>}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type DataTableColumnMeta = {
|
|
185
|
+
/** 14px icon before the header label. */
|
|
186
|
+
icon?: ReactNode;
|
|
187
|
+
headClassName?: string;
|
|
188
|
+
cellClassName?: string;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const columnMeta: DataTableColumnMeta = {};
|
|
192
|
+
|
|
193
|
+
/** TanStack Table features every horon data table registers: sorting, filtering, selection, pagination. */
|
|
194
|
+
export const dataTableFeatures = tableFeatures({
|
|
195
|
+
rowSortingFeature,
|
|
196
|
+
columnFilteringFeature,
|
|
197
|
+
globalFilteringFeature,
|
|
198
|
+
rowSelectionFeature,
|
|
199
|
+
rowPaginationFeature,
|
|
200
|
+
sortedRowModel: createSortedRowModel(),
|
|
201
|
+
filteredRowModel: createFilteredRowModel(),
|
|
202
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
203
|
+
sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic, datetime: sortFn_datetime, text: sortFn_text },
|
|
204
|
+
filterFns: { includesString: filterFn_includesString },
|
|
205
|
+
columnMeta,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export type DataTableFeatures = typeof dataTableFeatures;
|
|
209
|
+
export type DataTableColumn<TData extends RowData> = ColumnDef<DataTableFeatures, TData>;
|
|
210
|
+
export type DataTableInstance<TData extends RowData> = ReactTable<DataTableFeatures, TData>;
|
|
211
|
+
|
|
212
|
+
export type UseDataTableOptions<TData extends RowData> = {
|
|
213
|
+
data: TData[];
|
|
214
|
+
columns: DataTableColumn<TData>[];
|
|
215
|
+
getRowId?: (row: TData, index: number) => string;
|
|
216
|
+
/** Rows per page. Omit it to show every row on one page. */
|
|
217
|
+
pageSize?: number;
|
|
218
|
+
enableRowSelection?: boolean;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/** Builds the TanStack table behind `DataTableView`, `DataTableSearch` and `DataTablePagination`. */
|
|
222
|
+
export function useDataTable<TData extends RowData>({
|
|
223
|
+
data,
|
|
224
|
+
columns,
|
|
225
|
+
getRowId,
|
|
226
|
+
pageSize,
|
|
227
|
+
enableRowSelection = false,
|
|
228
|
+
}: UseDataTableOptions<TData>): DataTableInstance<TData> {
|
|
229
|
+
return useTable({
|
|
230
|
+
features: dataTableFeatures,
|
|
231
|
+
data,
|
|
232
|
+
columns,
|
|
233
|
+
getRowId,
|
|
234
|
+
enableRowSelection,
|
|
235
|
+
manualPagination: pageSize === undefined,
|
|
236
|
+
initialState: { pagination: { pageIndex: 0, pageSize: pageSize ?? 10 } },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Leading checkbox column; the header box selects every row on the current page. */
|
|
241
|
+
export function dataTableSelectColumn<TData extends RowData>(): DataTableColumn<TData> {
|
|
242
|
+
return {
|
|
243
|
+
id: "select",
|
|
244
|
+
enableSorting: false,
|
|
245
|
+
meta: { headClassName: "w-12" },
|
|
246
|
+
header: ({ table }) => {
|
|
247
|
+
const all = table.getIsAllPageRowsSelected();
|
|
248
|
+
return (
|
|
249
|
+
<Checkbox
|
|
250
|
+
size="sm"
|
|
251
|
+
aria-label="Select all rows"
|
|
252
|
+
checked={all}
|
|
253
|
+
indeterminate={!all && table.getIsSomePageRowsSelected()}
|
|
254
|
+
onCheckedChange={(checked) => table.toggleAllPageRowsSelected(checked)}
|
|
255
|
+
/>
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
cell: ({ row }) => (
|
|
259
|
+
<Checkbox
|
|
260
|
+
size="sm"
|
|
261
|
+
aria-label={`Select row ${row.index + 1}`}
|
|
262
|
+
checked={row.getIsSelected()}
|
|
263
|
+
disabled={!row.getCanSelect()}
|
|
264
|
+
onCheckedChange={(checked) => row.toggleSelected(checked)}
|
|
265
|
+
/>
|
|
266
|
+
),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const ariaSort = { asc: "ascending", desc: "descending" } as const;
|
|
271
|
+
|
|
272
|
+
export type DataTableViewProps<TData extends RowData> = {
|
|
273
|
+
table: DataTableInstance<TData>;
|
|
274
|
+
emptyMessage?: ReactNode;
|
|
275
|
+
className?: string;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/** Renders a `useDataTable` instance with the Figma head and body cells; sortable heads toggle on click. */
|
|
279
|
+
export function DataTableView<TData extends RowData>({
|
|
280
|
+
table,
|
|
281
|
+
emptyMessage = "No results.",
|
|
282
|
+
className,
|
|
283
|
+
}: DataTableViewProps<TData>) {
|
|
284
|
+
const rows = table.getRowModel().rows;
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<DataTableTable className={className}>
|
|
288
|
+
<thead>
|
|
289
|
+
{table.getHeaderGroups().map((group) => (
|
|
290
|
+
<tr key={group.id}>
|
|
291
|
+
{group.headers.map((header) => {
|
|
292
|
+
const meta = header.column.columnDef.meta;
|
|
293
|
+
const canSort = header.column.getCanSort();
|
|
294
|
+
const sorted = header.column.getIsSorted();
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<DataTableHead
|
|
298
|
+
key={header.id}
|
|
299
|
+
colSpan={header.colSpan}
|
|
300
|
+
icon={canSort ? undefined : meta?.icon}
|
|
301
|
+
aria-sort={canSort ? (sorted ? ariaSort[sorted] : "none") : undefined}
|
|
302
|
+
className={meta?.headClassName}
|
|
303
|
+
>
|
|
304
|
+
{header.isPlaceholder ? null : canSort ? (
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
308
|
+
className="-mx-1 flex cursor-pointer items-center gap-2 rounded-xs px-1 hover:text-text-primary focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
309
|
+
>
|
|
310
|
+
{meta?.icon}
|
|
311
|
+
<table.FlexRender header={header} />
|
|
312
|
+
<span key={sorted || "none"} className="flex animate-fade-in">
|
|
313
|
+
{sorted === "asc" ? (
|
|
314
|
+
<CaretUp aria-hidden />
|
|
315
|
+
) : sorted === "desc" ? (
|
|
316
|
+
<CaretDown aria-hidden />
|
|
317
|
+
) : (
|
|
318
|
+
<CaretUpDown aria-hidden className="opacity-60" />
|
|
319
|
+
)}
|
|
320
|
+
</span>
|
|
321
|
+
</button>
|
|
322
|
+
) : (
|
|
323
|
+
<table.FlexRender header={header} />
|
|
324
|
+
)}
|
|
325
|
+
</DataTableHead>
|
|
326
|
+
);
|
|
327
|
+
})}
|
|
328
|
+
</tr>
|
|
329
|
+
))}
|
|
330
|
+
</thead>
|
|
331
|
+
<tbody>
|
|
332
|
+
{rows.length === 0 ? (
|
|
333
|
+
<DataTableRow>
|
|
334
|
+
<DataTableCell colSpan={table.getAllLeafColumns().length} className="h-24 text-center">
|
|
335
|
+
{emptyMessage}
|
|
336
|
+
</DataTableCell>
|
|
337
|
+
</DataTableRow>
|
|
338
|
+
) : (
|
|
339
|
+
rows.map((row, index) => (
|
|
340
|
+
<DataTableRow
|
|
341
|
+
key={`${table.state.pagination.pageIndex}:${table.state.sorting
|
|
342
|
+
.map((sort) => `${sort.id}${sort.desc ? "d" : "a"}`)
|
|
343
|
+
.join()}:${row.id}`}
|
|
344
|
+
className="motion-rise motion-colors hover:bg-overlay-ink-4 data-selected:[&>td]:bg-overlay-ink-4 data-selected:hover:[&>td]:bg-overlay-ink-6"
|
|
345
|
+
style={{ "--i": index, animationDelay: "calc(var(--i) * 30ms)" } as CSSProperties}
|
|
346
|
+
data-selected={row.getIsSelected() ? "" : undefined}
|
|
347
|
+
>
|
|
348
|
+
{row.getAllCells().map((cell) => (
|
|
349
|
+
<DataTableCell key={cell.id} className={cell.column.columnDef.meta?.cellClassName}>
|
|
350
|
+
<table.FlexRender cell={cell} />
|
|
351
|
+
</DataTableCell>
|
|
352
|
+
))}
|
|
353
|
+
</DataTableRow>
|
|
354
|
+
))
|
|
355
|
+
)}
|
|
356
|
+
</tbody>
|
|
357
|
+
</DataTableTable>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export type DataTableSearchProps<TData extends RowData> = {
|
|
362
|
+
table: DataTableInstance<TData>;
|
|
363
|
+
placeholder?: string;
|
|
364
|
+
className?: string;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/** Solid search field bound to the table's global filter. */
|
|
368
|
+
export function DataTableSearch<TData extends RowData>({
|
|
369
|
+
table,
|
|
370
|
+
placeholder = "Search",
|
|
371
|
+
className,
|
|
372
|
+
}: DataTableSearchProps<TData>) {
|
|
373
|
+
return (
|
|
374
|
+
<Input
|
|
375
|
+
surface="solid"
|
|
376
|
+
size="sm"
|
|
377
|
+
icon={<MagnifyingGlass />}
|
|
378
|
+
placeholder={placeholder}
|
|
379
|
+
aria-label={placeholder}
|
|
380
|
+
value={String(table.state.globalFilter ?? "")}
|
|
381
|
+
onChange={(event) => table.setGlobalFilter(event.target.value)}
|
|
382
|
+
className={cn("min-w-0", className)}
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export type DataTablePaginationProps<TData extends RowData> = {
|
|
388
|
+
table: DataTableInstance<TData>;
|
|
389
|
+
className?: string;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
/** Row count (or selection count) with previous and next page buttons. */
|
|
393
|
+
export function DataTablePagination<TData extends RowData>({ table, className }: DataTablePaginationProps<TData>) {
|
|
394
|
+
const total = table.getFilteredRowModel().rows.length;
|
|
395
|
+
const selected = table.getFilteredSelectedRowModel().rows.length;
|
|
396
|
+
const pageCount = Math.max(table.getPageCount(), 1);
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<div
|
|
400
|
+
className={cn(
|
|
401
|
+
"flex flex-wrap items-center justify-between gap-x-4 gap-y-2 px-6 py-3 group-data-[device=mobile]/dt:px-4.5",
|
|
402
|
+
className,
|
|
403
|
+
)}
|
|
404
|
+
>
|
|
405
|
+
<p className="text-caption text-text-secondary tabular-nums">
|
|
406
|
+
{selected > 0 ? `${selected} of ${total} selected` : `${total} ${total === 1 ? "row" : "rows"}`}
|
|
407
|
+
</p>
|
|
408
|
+
<div className="flex items-center gap-2">
|
|
409
|
+
<IconButton
|
|
410
|
+
size="sm"
|
|
411
|
+
icon={<CaretLeft />}
|
|
412
|
+
label="Previous page"
|
|
413
|
+
onClick={() => table.previousPage()}
|
|
414
|
+
disabled={!table.getCanPreviousPage()}
|
|
415
|
+
/>
|
|
416
|
+
<span className="text-caption text-text-secondary tabular-nums">
|
|
417
|
+
Page {table.state.pagination.pageIndex + 1} of {pageCount}
|
|
418
|
+
</span>
|
|
419
|
+
<IconButton
|
|
420
|
+
size="sm"
|
|
421
|
+
icon={<CaretRight />}
|
|
422
|
+
label="Next page"
|
|
423
|
+
onClick={() => table.nextPage()}
|
|
424
|
+
disabled={!table.getCanNextPage()}
|
|
425
|
+
/>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Popover } from "@base-ui/react/popover";
|
|
2
|
+
import { CalendarBlank } from "@phosphor-icons/react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import type { ControlSize } from "../lib/size";
|
|
6
|
+
import { Calendar, type CalendarProps, type DateRange } from "./calendar";
|
|
7
|
+
import { fieldBoxClass, fieldSizeClass, fieldTextClass, type InputSurface } from "./input";
|
|
8
|
+
|
|
9
|
+
export type DateFieldSize = ControlSize;
|
|
10
|
+
export type DateFieldSurface = InputSurface;
|
|
11
|
+
|
|
12
|
+
type WithoutCalendarSurface<T> = T extends unknown ? Omit<T, "size" | "surface"> : never;
|
|
13
|
+
|
|
14
|
+
export type DateFieldProps = WithoutCalendarSurface<CalendarProps> & {
|
|
15
|
+
size?: DateFieldSize;
|
|
16
|
+
/** `solid` (default) on raised surfaces, `glass` over imagery and glass panels, `solid-on-page` directly on the page background. */
|
|
17
|
+
surface?: DateFieldSurface;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
invalid?: boolean;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
/** Formats the trigger text; defaults to "Oct 20, 2026" and "Oct 20 – Oct 24, 2026". */
|
|
22
|
+
format?: (value: Date | DateRange) => string;
|
|
23
|
+
triggerClassName?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function defaultFormat(value: Date | DateRange, locale?: string): string {
|
|
27
|
+
const withYear = new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", year: "numeric" });
|
|
28
|
+
if (value instanceof Date) return withYear.format(value);
|
|
29
|
+
if (!value.start) return "";
|
|
30
|
+
if (!value.end) return withYear.format(value.start);
|
|
31
|
+
const sameYear = value.start.getFullYear() === value.end.getFullYear();
|
|
32
|
+
const start = sameYear
|
|
33
|
+
? new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(value.start)
|
|
34
|
+
: withYear.format(value.start);
|
|
35
|
+
return `${start} – ${withYear.format(value.end)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The calendar fills its parent, and a popover's positioner is auto-width, so the popup names the width it should take, capped at the viewport. */
|
|
39
|
+
const popupCalendarWidthClass: Record<DateFieldSize, string> = {
|
|
40
|
+
sm: "w-62.5 max-w-[calc(100vw-2rem)]",
|
|
41
|
+
md: "w-78.5 max-w-[calc(100vw-2rem)]",
|
|
42
|
+
lg: "w-93.5 max-w-[calc(100vw-2rem)]",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Figma `Date field` (60:244): a 280px field that opens the calendar, in three sizes and two surfaces. */
|
|
46
|
+
export function DateField({
|
|
47
|
+
size = "md",
|
|
48
|
+
surface = "solid",
|
|
49
|
+
placeholder = "Pick a date",
|
|
50
|
+
invalid,
|
|
51
|
+
disabled,
|
|
52
|
+
format,
|
|
53
|
+
triggerClassName,
|
|
54
|
+
...calendar
|
|
55
|
+
}: DateFieldProps) {
|
|
56
|
+
const value = calendar.value ?? null;
|
|
57
|
+
const text = value ? (format ? format(value) : defaultFormat(value, calendar.locale)) : "";
|
|
58
|
+
return (
|
|
59
|
+
<Popover.Root>
|
|
60
|
+
<Popover.Trigger
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
aria-invalid={invalid || undefined}
|
|
63
|
+
className={cn(
|
|
64
|
+
fieldBoxClass.base,
|
|
65
|
+
fieldBoxClass[surface],
|
|
66
|
+
fieldSizeClass[size],
|
|
67
|
+
"w-full max-w-70 cursor-pointer text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand data-disabled:cursor-not-allowed",
|
|
68
|
+
surface !== "glass" && "hover:enabled:not-data-invalid:border-border-strong",
|
|
69
|
+
triggerClassName,
|
|
70
|
+
)}
|
|
71
|
+
data-invalid={invalid ? "" : undefined}
|
|
72
|
+
>
|
|
73
|
+
<span className="relative flex shrink-0 text-text-secondary">
|
|
74
|
+
<CalendarBlank />
|
|
75
|
+
</span>
|
|
76
|
+
<span
|
|
77
|
+
className={cn(
|
|
78
|
+
"relative min-w-0 flex-1 truncate",
|
|
79
|
+
fieldTextClass[size],
|
|
80
|
+
disabled ? "text-text-disabled" : text ? "text-text-primary" : "text-text-placeholder",
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
{text || placeholder}
|
|
84
|
+
</span>
|
|
85
|
+
</Popover.Trigger>
|
|
86
|
+
<Popover.Portal>
|
|
87
|
+
<Popover.Positioner sideOffset={4} align="start" className="z-50 outline-none">
|
|
88
|
+
<Popover.Popup className="motion-popup">
|
|
89
|
+
<Calendar
|
|
90
|
+
{...calendar}
|
|
91
|
+
size={size}
|
|
92
|
+
surface={surface === "glass" ? "glass" : "solid"}
|
|
93
|
+
className={popupCalendarWidthClass[size]}
|
|
94
|
+
/>
|
|
95
|
+
</Popover.Popup>
|
|
96
|
+
</Popover.Positioner>
|
|
97
|
+
</Popover.Portal>
|
|
98
|
+
</Popover.Root>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Dialog as BaseDialog } from "@base-ui/react/dialog";
|
|
2
|
+
import { X } from "@phosphor-icons/react";
|
|
3
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
|
|
7
|
+
export const Dialog = BaseDialog.Root;
|
|
8
|
+
export const DialogTrigger = BaseDialog.Trigger;
|
|
9
|
+
export const DialogClose = BaseDialog.Close;
|
|
10
|
+
|
|
11
|
+
export type DialogContentProps = Omit<ComponentProps<typeof BaseDialog.Popup>, "className" | "title"> & {
|
|
12
|
+
title: ReactNode;
|
|
13
|
+
description?: ReactNode;
|
|
14
|
+
/** Label for the close button; hides it when `null`. */
|
|
15
|
+
closeLabel?: string | null;
|
|
16
|
+
/** Slot marker on the close control, for a surface its own app selects the control by. */
|
|
17
|
+
closeSlot?: string;
|
|
18
|
+
/** Tile or icon set before the title (Paper settings modals). */
|
|
19
|
+
leading?: ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* `glass` is the veil dialog (`veil-overlay`). `solid` is the Paper workspace modal:
|
|
22
|
+
* opaque default fill, 20px radius, 24px padding and a 0 24 64 shadow, no glass layer.
|
|
23
|
+
* Both surfaces share the 16px block rhythm between title, description and actions.
|
|
24
|
+
*/
|
|
25
|
+
surface?: "glass" | "solid";
|
|
26
|
+
className?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Figma `Dialog on scrim` (63:329): a 40% scrim and a 420px veil dialog,
|
|
31
|
+
* 24px padding, title-md heading with a 32px close target, body-md description.
|
|
32
|
+
*/
|
|
33
|
+
export function DialogContent({
|
|
34
|
+
title,
|
|
35
|
+
description,
|
|
36
|
+
closeLabel = "Close",
|
|
37
|
+
closeSlot,
|
|
38
|
+
leading,
|
|
39
|
+
surface = "solid",
|
|
40
|
+
className,
|
|
41
|
+
children,
|
|
42
|
+
...props
|
|
43
|
+
}: DialogContentProps) {
|
|
44
|
+
const solid = surface === "solid";
|
|
45
|
+
return (
|
|
46
|
+
<BaseDialog.Portal>
|
|
47
|
+
<BaseDialog.Backdrop className="fixed inset-0 z-50 bg-bg-scrim/32 motion-fade backdrop-blur-[6px]" />
|
|
48
|
+
<BaseDialog.Viewport className="fixed inset-0 z-50 flex items-center justify-center px-4">
|
|
49
|
+
<BaseDialog.Popup
|
|
50
|
+
className={cn(
|
|
51
|
+
// 420px where it fits; the viewport's 16px gutters cap `w-full` below that.
|
|
52
|
+
"relative flex max-h-[calc(100dvh-2rem)] w-full max-w-105 flex-col items-start gap-4 overflow-y-auto overscroll-contain rounded-xl p-6 outline-none",
|
|
53
|
+
solid ? "bg-bg-surface shadow-[0_24px_64px_#0A142433]" : "veil-overlay",
|
|
54
|
+
"motion-popup duration-240 data-starting-style:translate-y-2",
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
<div className={cn("relative flex w-full items-start", leading ? "gap-3.5" : "gap-2")}>
|
|
60
|
+
{leading}
|
|
61
|
+
<BaseDialog.Title className="min-w-0 flex-1 text-title-md text-text-primary">{title}</BaseDialog.Title>
|
|
62
|
+
{closeLabel !== null && (
|
|
63
|
+
<BaseDialog.Close
|
|
64
|
+
aria-label={closeLabel}
|
|
65
|
+
data-slot={closeSlot}
|
|
66
|
+
className="relative -mt-1 -mr-2 flex size-8 shrink-0 motion-press cursor-pointer items-center justify-center rounded-full text-text-secondary motion-colors hover:bg-overlay-ink-6 hover:text-text-primary focus-visible:outline-2 focus-visible:outline-text-brand [&_svg]:size-4"
|
|
67
|
+
>
|
|
68
|
+
<X />
|
|
69
|
+
</BaseDialog.Close>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
{description && (
|
|
73
|
+
<BaseDialog.Description className="relative text-body-md text-text-secondary">
|
|
74
|
+
{description}
|
|
75
|
+
</BaseDialog.Description>
|
|
76
|
+
)}
|
|
77
|
+
{children}
|
|
78
|
+
</BaseDialog.Popup>
|
|
79
|
+
</BaseDialog.Viewport>
|
|
80
|
+
</BaseDialog.Portal>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The action row: secondary first, the primary action at the right edge, 8px apart.
|
|
86
|
+
* `w-full` is what right-aligns it — the dialog popup stacks children at their content width.
|
|
87
|
+
*/
|
|
88
|
+
export function DialogFooter({
|
|
89
|
+
className,
|
|
90
|
+
...props
|
|
91
|
+
}: Omit<ComponentProps<"div">, "className"> & { className?: string }) {
|
|
92
|
+
return <div className={cn("relative flex w-full flex-wrap items-center justify-end gap-2", className)} {...props} />;
|
|
93
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Plus } from "@phosphor-icons/react";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { IconButton } from "./icon-button";
|
|
6
|
+
import { LiquidGlass } from "./liquid-glass";
|
|
7
|
+
|
|
8
|
+
export type DirectoryCardProps = Omit<ComponentProps<"div">, "children"> & {
|
|
9
|
+
/** 20px icon shown in inverse on the 40px tile. */
|
|
10
|
+
icon: ReactNode;
|
|
11
|
+
name: ReactNode;
|
|
12
|
+
/** One line; longer text is clamped. */
|
|
13
|
+
description?: ReactNode;
|
|
14
|
+
onAdd?: () => void;
|
|
15
|
+
addLabel?: string;
|
|
16
|
+
/** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. */
|
|
17
|
+
surface?: "solid" | "glass";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Paper `Directory card` (Settings 1VJJ-0): 40px tile, name and one-line description, plus an add button. */
|
|
21
|
+
export function DirectoryCard({
|
|
22
|
+
icon,
|
|
23
|
+
name,
|
|
24
|
+
description,
|
|
25
|
+
onAdd,
|
|
26
|
+
addLabel = "Add",
|
|
27
|
+
surface = "solid",
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: DirectoryCardProps) {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
"relative flex items-center gap-3 overflow-clip rounded-2xl py-3 pr-3 pl-3",
|
|
35
|
+
surface === "glass" ? "bg-glass-thick" : "border border-border-default bg-bg-surface",
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{surface === "glass" && <LiquidGlass />}
|
|
41
|
+
<span className="relative flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-sky-500 text-neutral-50 [&>svg]:size-5">
|
|
42
|
+
{icon}
|
|
43
|
+
</span>
|
|
44
|
+
<span className="relative flex min-w-0 flex-1 flex-col gap-0.5">
|
|
45
|
+
<span className="truncate text-label-md text-text-primary">{name}</span>
|
|
46
|
+
{description && <span className="line-clamp-1 text-caption text-text-secondary">{description}</span>}
|
|
47
|
+
</span>
|
|
48
|
+
{onAdd && (
|
|
49
|
+
<IconButton
|
|
50
|
+
variant={surface === "glass" ? "glass" : "solid"}
|
|
51
|
+
size="sm"
|
|
52
|
+
label={addLabel}
|
|
53
|
+
icon={<Plus aria-hidden />}
|
|
54
|
+
onClick={onAdd}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|