@burtson-labs/ui 0.9.1 → 0.10.0

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.
@@ -0,0 +1,264 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { Button } from "./button.js";
3
+ import { Checkbox } from "./checkbox.js";
4
+ import { Input } from "./input.js";
5
+ import { Pagination } from "./pagination.js";
6
+ import { Skeleton } from "./skeleton.js";
7
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table.js";
8
+ import * as React from "react";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ import ChevronsUpDown from "@burtson-labs/icons/react/chevrons-up-down";
11
+ import Search from "@burtson-labs/icons/react/search";
12
+ import ArrowUp from "@burtson-labs/icons/react/arrow-up";
13
+ import ArrowDown from "@burtson-labs/icons/react/arrow-down";
14
+ //#region src/components/data-table.tsx
15
+ var nextSort = (current, id) => {
16
+ if (!current || current.columnId !== id) return {
17
+ columnId: id,
18
+ direction: "asc"
19
+ };
20
+ return current.direction === "asc" ? {
21
+ columnId: id,
22
+ direction: "desc"
23
+ } : null;
24
+ };
25
+ var INTERACTIVE = "a,button,input,select,textarea,[role=\"checkbox\"],[role=\"menuitem\"]";
26
+ /**
27
+ * A table recipe for records: sort, search, select, page, and row actions,
28
+ * with loading, empty and error states. Fully controlled, so the data can
29
+ * come from a server; the table never reorders rows itself.
30
+ */
31
+ function DataTable({ "aria-label": ariaLabel, columns, rows, getRowId, getRowLabel, sort, onSortChange, filter, onFilterChange, filterPlaceholder = "Search", selected, onSelectedChange, onRowAction, rowActions, page, pageCount, onPageChange, pageSummary, loading = false, error, onRetry, empty = "No results.", toolbar, density = "default", stickyHeader, className, ...props }) {
32
+ const selectable = Boolean(onSelectedChange);
33
+ const selectedSet = React.useMemo(() => new Set(selected ?? []), [selected]);
34
+ const ids = rows.map(getRowId);
35
+ const pageSelected = ids.filter((id) => selectedSet.has(id)).length;
36
+ const allState = ids.length > 0 && pageSelected === ids.length ? true : pageSelected > 0 ? "indeterminate" : false;
37
+ const colCount = columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0);
38
+ const interactive = Boolean(onRowAction) || selectable;
39
+ const [active, setActive] = React.useState(0);
40
+ const frameRef = React.useRef(null);
41
+ const activeRow = Math.min(active, Math.max(0, rows.length - 1));
42
+ const toggle = (id) => {
43
+ if (!onSelectedChange) return;
44
+ const next = new Set(selectedSet);
45
+ if (next.has(id)) next.delete(id);
46
+ else next.add(id);
47
+ onSelectedChange([...next]);
48
+ };
49
+ const toggleAll = () => {
50
+ if (!onSelectedChange) return;
51
+ const next = new Set(selectedSet);
52
+ if (allState === true) ids.forEach((id) => next.delete(id));
53
+ else ids.forEach((id) => next.add(id));
54
+ onSelectedChange([...next]);
55
+ };
56
+ const focusRow = (i) => {
57
+ const clamped = Math.min(Math.max(i, 0), rows.length - 1);
58
+ setActive(clamped);
59
+ frameRef.current?.querySelectorAll("[data-slot=\"table-body\"] > [data-slot=\"table-row\"]")[clamped]?.focus();
60
+ };
61
+ const onRowKeyDown = (event, i, row) => {
62
+ if (event.target !== event.currentTarget) return;
63
+ switch (event.key) {
64
+ case "ArrowDown":
65
+ event.preventDefault();
66
+ focusRow(i + 1);
67
+ break;
68
+ case "ArrowUp":
69
+ event.preventDefault();
70
+ focusRow(i - 1);
71
+ break;
72
+ case "Home":
73
+ event.preventDefault();
74
+ focusRow(0);
75
+ break;
76
+ case "End":
77
+ event.preventDefault();
78
+ focusRow(rows.length - 1);
79
+ break;
80
+ case "Enter":
81
+ if (onRowAction) {
82
+ event.preventDefault();
83
+ onRowAction(row);
84
+ }
85
+ break;
86
+ case " ": if (selectable) {
87
+ event.preventDefault();
88
+ toggle(getRowId(row));
89
+ }
90
+ }
91
+ };
92
+ const showToolbar = Boolean(onFilterChange || toolbar || selectable && selectedSet.size);
93
+ let body;
94
+ if (error) body = /* @__PURE__ */ jsx(TableRow, {
95
+ className: "hover:bg-transparent",
96
+ children: /* @__PURE__ */ jsx(TableCell, {
97
+ colSpan: colCount,
98
+ className: "h-auto py-8 text-center whitespace-normal",
99
+ children: /* @__PURE__ */ jsxs("div", {
100
+ role: "alert",
101
+ className: "grid justify-items-center gap-3 text-sm",
102
+ children: [/* @__PURE__ */ jsx("span", {
103
+ className: "text-destructive",
104
+ children: error
105
+ }), onRetry && /* @__PURE__ */ jsx(Button, {
106
+ type: "button",
107
+ size: "sm",
108
+ variant: "outline",
109
+ onClick: onRetry,
110
+ children: "Try again"
111
+ })]
112
+ })
113
+ })
114
+ });
115
+ else if (loading && rows.length === 0) body = Array.from({ length: 3 }, (_, r) => /* @__PURE__ */ jsx(TableRow, {
116
+ className: "hover:bg-transparent",
117
+ "aria-hidden": true,
118
+ children: Array.from({ length: colCount }, (_, c) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-full max-w-40" }) }, c))
119
+ }, `loading-${r}`));
120
+ else if (rows.length === 0) body = /* @__PURE__ */ jsx(TableRow, {
121
+ className: "hover:bg-transparent",
122
+ children: /* @__PURE__ */ jsx(TableCell, {
123
+ colSpan: colCount,
124
+ className: "h-auto py-10 text-center whitespace-normal text-muted-foreground",
125
+ children: empty
126
+ })
127
+ });
128
+ else body = rows.map((row, i) => {
129
+ const id = getRowId(row);
130
+ const isSelected = selectedSet.has(id);
131
+ const label = getRowLabel?.(row) ?? id;
132
+ return /* @__PURE__ */ jsxs(TableRow, {
133
+ selected: isSelected,
134
+ tabIndex: interactive ? i === activeRow ? 0 : -1 : void 0,
135
+ onFocus: () => setActive(i),
136
+ onKeyDown: interactive ? (e) => onRowKeyDown(e, i, row) : void 0,
137
+ onClick: onRowAction ? (e) => {
138
+ if (e.target.closest(INTERACTIVE)) return;
139
+ onRowAction(row);
140
+ } : void 0,
141
+ className: cn(interactive && "outline-none focus-visible:bg-muted/60 focus-visible:shadow-[inset_2px_0_0_var(--ring)]", onRowAction && "cursor-pointer"),
142
+ children: [
143
+ selectable && /* @__PURE__ */ jsx(TableCell, {
144
+ className: "w-10",
145
+ children: /* @__PURE__ */ jsx(Checkbox, {
146
+ checked: isSelected,
147
+ onCheckedChange: () => toggle(id),
148
+ "aria-label": `Select ${label}`,
149
+ tabIndex: -1
150
+ })
151
+ }),
152
+ columns.map((col) => /* @__PURE__ */ jsx(TableCell, {
153
+ numeric: col.numeric,
154
+ className: col.className,
155
+ children: col.cell(row)
156
+ }, col.id)),
157
+ rowActions && /* @__PURE__ */ jsx(TableCell, {
158
+ className: "w-12 text-right",
159
+ children: rowActions(row)
160
+ })
161
+ ]
162
+ }, id);
163
+ });
164
+ return /* @__PURE__ */ jsxs("div", {
165
+ "data-slot": "data-table",
166
+ className: cn("grid min-w-0 gap-3", className),
167
+ ...props,
168
+ children: [
169
+ showToolbar && /* @__PURE__ */ jsxs("div", {
170
+ className: "flex flex-wrap items-center gap-2",
171
+ children: [
172
+ onFilterChange && /* @__PURE__ */ jsxs("div", {
173
+ className: "relative w-full max-w-xs min-w-0 flex-1",
174
+ children: [/* @__PURE__ */ jsx(Search, {
175
+ className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground",
176
+ "aria-hidden": true
177
+ }), /* @__PURE__ */ jsx(Input, {
178
+ type: "search",
179
+ value: filter ?? "",
180
+ onChange: (e) => onFilterChange(e.target.value),
181
+ placeholder: filterPlaceholder,
182
+ "aria-label": `Search ${ariaLabel.toLowerCase()}`,
183
+ className: "pl-8"
184
+ })]
185
+ }),
186
+ selectable && /* @__PURE__ */ jsx("span", {
187
+ "aria-live": "polite",
188
+ className: "text-xs text-muted-foreground tabular-nums",
189
+ children: selectedSet.size ? `${selectedSet.size} selected` : ""
190
+ }),
191
+ toolbar && /* @__PURE__ */ jsx("div", {
192
+ className: "ml-auto flex flex-wrap items-center gap-2",
193
+ children: toolbar
194
+ })
195
+ ]
196
+ }),
197
+ /* @__PURE__ */ jsx("div", {
198
+ ref: frameRef,
199
+ className: "rounded-lg border bg-surface",
200
+ children: /* @__PURE__ */ jsxs(Table, {
201
+ "aria-label": ariaLabel,
202
+ "aria-busy": loading || void 0,
203
+ density,
204
+ stickyHeader,
205
+ className: cn(loading && rows.length > 0 && "opacity-60 transition-opacity"),
206
+ children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, {
207
+ className: "hover:bg-transparent",
208
+ children: [
209
+ selectable && /* @__PURE__ */ jsx(TableHead, {
210
+ className: "w-10",
211
+ children: /* @__PURE__ */ jsx(Checkbox, {
212
+ checked: allState,
213
+ onCheckedChange: toggleAll,
214
+ disabled: rows.length === 0,
215
+ "aria-label": "Select all rows on this page"
216
+ })
217
+ }),
218
+ columns.map((col) => {
219
+ const sorted = sort?.columnId === col.id ? sort.direction : void 0;
220
+ return /* @__PURE__ */ jsx(TableHead, {
221
+ numeric: col.numeric,
222
+ className: col.className,
223
+ "aria-sort": col.sortable ? sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none" : void 0,
224
+ children: col.sortable && onSortChange ? /* @__PURE__ */ jsxs("button", {
225
+ type: "button",
226
+ onClick: () => onSortChange(nextSort(sort, col.id)),
227
+ className: cn("-mx-1.5 inline-flex h-7 items-center gap-1 rounded-sm px-1.5 uppercase outline-none hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20", sorted && "text-foreground", col.numeric && "flex-row-reverse"),
228
+ children: [col.header, sorted === "asc" ? /* @__PURE__ */ jsx(ArrowUp, {
229
+ className: "size-3.5",
230
+ "aria-hidden": true
231
+ }) : sorted === "desc" ? /* @__PURE__ */ jsx(ArrowDown, {
232
+ className: "size-3.5",
233
+ "aria-hidden": true
234
+ }) : /* @__PURE__ */ jsx(ChevronsUpDown, {
235
+ className: "size-3.5 opacity-50",
236
+ "aria-hidden": true
237
+ })]
238
+ }) : col.header
239
+ }, col.id);
240
+ }),
241
+ rowActions && /* @__PURE__ */ jsx(TableHead, {
242
+ className: "w-12",
243
+ children: /* @__PURE__ */ jsx("span", {
244
+ className: "sr-only",
245
+ children: "Actions"
246
+ })
247
+ })
248
+ ]
249
+ }) }), /* @__PURE__ */ jsx(TableBody, { children: body })]
250
+ })
251
+ }),
252
+ onPageChange && page !== void 0 && pageCount !== void 0 && pageCount > 0 && /* @__PURE__ */ jsx(Pagination, {
253
+ page,
254
+ pageCount,
255
+ onPageChange,
256
+ summary: pageSummary
257
+ })
258
+ ]
259
+ });
260
+ }
261
+ //#endregion
262
+ export { DataTable };
263
+
264
+ //# sourceMappingURL=data-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-table.js","names":[],"sources":["../../src/components/data-table.tsx"],"sourcesContent":["import ArrowDown from '@burtson-labs/icons/react/arrow-down';\nimport ArrowUp from '@burtson-labs/icons/react/arrow-up';\nimport ChevronsUpDown from '@burtson-labs/icons/react/chevrons-up-down';\nimport Search from '@burtson-labs/icons/react/search';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\nimport { Checkbox } from './checkbox';\nimport { Input } from './input';\nimport { Pagination } from './pagination';\nimport { Skeleton } from './skeleton';\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './table';\n\nexport interface DataTableColumn<T> {\n id: string;\n header: React.ReactNode;\n cell: (row: T) => React.ReactNode;\n sortable?: boolean;\n /** Right-aligned tabular figures. */\n numeric?: boolean;\n className?: string;\n}\n\nexport type SortDirection = 'asc' | 'desc';\n\nexport interface DataTableSort {\n columnId: string;\n direction: SortDirection;\n}\n\nexport interface DataTableProps<T> extends Omit<React.ComponentProps<'div'>, 'children'> {\n /** Required: what the table lists, e.g. \"Workspace members\". */\n 'aria-label': string;\n columns: DataTableColumn<T>[];\n /** The rows to show now: already sorted, filtered and paged by the app or server. */\n rows: T[];\n getRowId: (row: T) => string;\n /** Names a row for screen readers (\"Select Ada Lovelace\"). Defaults to its id. */\n getRowLabel?: (row: T) => string;\n\n /** Controlled sort. A sortable header cycles ascending, descending, none. */\n sort?: DataTableSort | null;\n onSortChange?: (sort: DataTableSort | null) => void;\n\n /** Controlled filter text; a search box appears when onFilterChange is set. */\n filter?: string;\n onFilterChange?: (value: string) => void;\n filterPlaceholder?: string;\n\n /** Controlled selection by row id; checkboxes appear when onSelectedChange is set. */\n selected?: string[];\n onSelectedChange?: (ids: string[]) => void;\n\n /** Enter or a click on the row, e.g. open the record. */\n onRowAction?: (row: T) => void;\n /** Per-row controls in a trailing column, e.g. a DropdownMenu. */\n rowActions?: (row: T) => React.ReactNode;\n\n /** 1-based; Pagination appears when onPageChange is set. */\n page?: number;\n pageCount?: number;\n onPageChange?: (page: number) => void;\n /** e.g. \"1–25 of 312\". */\n pageSummary?: React.ReactNode;\n\n loading?: boolean;\n /** A message shown in place of the rows. */\n error?: React.ReactNode;\n onRetry?: () => void;\n /** Shown when there are no rows and nothing failed. */\n empty?: React.ReactNode;\n /** Extra controls beside the search box, e.g. filters or a bulk action. */\n toolbar?: React.ReactNode;\n density?: 'compact' | 'default';\n stickyHeader?: boolean;\n}\n\nconst nextSort = (current: DataTableSort | null | undefined, id: string): DataTableSort | null => {\n if (!current || current.columnId !== id) return { columnId: id, direction: 'asc' };\n return current.direction === 'asc' ? { columnId: id, direction: 'desc' } : null;\n};\n\n// Clicks and keys aimed at these belong to them, not to the row.\nconst INTERACTIVE = 'a,button,input,select,textarea,[role=\"checkbox\"],[role=\"menuitem\"]';\n\n/**\n * A table recipe for records: sort, search, select, page, and row actions,\n * with loading, empty and error states. Fully controlled, so the data can\n * come from a server; the table never reorders rows itself.\n */\nfunction DataTable<T>({\n 'aria-label': ariaLabel,\n columns,\n rows,\n getRowId,\n getRowLabel,\n sort,\n onSortChange,\n filter,\n onFilterChange,\n filterPlaceholder = 'Search',\n selected,\n onSelectedChange,\n onRowAction,\n rowActions,\n page,\n pageCount,\n onPageChange,\n pageSummary,\n loading = false,\n error,\n onRetry,\n empty = 'No results.',\n toolbar,\n density = 'default',\n stickyHeader,\n className,\n ...props\n}: DataTableProps<T>) {\n const selectable = Boolean(onSelectedChange);\n const selectedSet = React.useMemo(() => new Set(selected ?? []), [selected]);\n const ids = rows.map(getRowId);\n const pageSelected = ids.filter((id) => selectedSet.has(id)).length;\n const allState: boolean | 'indeterminate' =\n ids.length > 0 && pageSelected === ids.length\n ? true\n : pageSelected > 0\n ? 'indeterminate'\n : false;\n const colCount = columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0);\n const interactive = Boolean(onRowAction) || selectable;\n\n // One tab stop in the body: the active row. Arrows move it.\n const [active, setActive] = React.useState(0);\n const frameRef = React.useRef<HTMLDivElement>(null);\n // Rows can shrink under the active index (a new page, a filter).\n const activeRow = Math.min(active, Math.max(0, rows.length - 1));\n\n const toggle = (id: string) => {\n if (!onSelectedChange) return;\n const next = new Set(selectedSet);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n onSelectedChange([...next]);\n };\n const toggleAll = () => {\n if (!onSelectedChange) return;\n const next = new Set(selectedSet);\n if (allState === true) ids.forEach((id) => next.delete(id));\n else ids.forEach((id) => next.add(id));\n onSelectedChange([...next]);\n };\n const focusRow = (i: number) => {\n const clamped = Math.min(Math.max(i, 0), rows.length - 1);\n setActive(clamped);\n frameRef.current\n ?.querySelectorAll<HTMLElement>('[data-slot=\"table-body\"] > [data-slot=\"table-row\"]')\n [clamped]?.focus();\n };\n\n const onRowKeyDown = (event: React.KeyboardEvent<HTMLTableRowElement>, i: number, row: T) => {\n if (event.target !== event.currentTarget) return;\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n focusRow(i + 1);\n break;\n case 'ArrowUp':\n event.preventDefault();\n focusRow(i - 1);\n break;\n case 'Home':\n event.preventDefault();\n focusRow(0);\n break;\n case 'End':\n event.preventDefault();\n focusRow(rows.length - 1);\n break;\n case 'Enter':\n if (onRowAction) {\n event.preventDefault();\n onRowAction(row);\n }\n break;\n case ' ':\n if (selectable) {\n event.preventDefault();\n toggle(getRowId(row));\n }\n break;\n }\n };\n\n const showToolbar = Boolean(onFilterChange || toolbar || (selectable && selectedSet.size));\n let body: React.ReactNode;\n if (error) {\n body = (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell colSpan={colCount} className=\"h-auto py-8 text-center whitespace-normal\">\n <div role=\"alert\" className=\"grid justify-items-center gap-3 text-sm\">\n <span className=\"text-destructive\">{error}</span>\n {onRetry && (\n <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={onRetry}>\n Try again\n </Button>\n )}\n </div>\n </TableCell>\n </TableRow>\n );\n } else if (loading && rows.length === 0) {\n body = Array.from({ length: 3 }, (_, r) => (\n <TableRow key={`loading-${r}`} className=\"hover:bg-transparent\" aria-hidden>\n {Array.from({ length: colCount }, (_, c) => (\n <TableCell key={c}>\n <Skeleton className=\"h-3.5 w-full max-w-40\" />\n </TableCell>\n ))}\n </TableRow>\n ));\n } else if (rows.length === 0) {\n body = (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell\n colSpan={colCount}\n className=\"h-auto py-10 text-center whitespace-normal text-muted-foreground\"\n >\n {empty}\n </TableCell>\n </TableRow>\n );\n } else {\n body = rows.map((row, i) => {\n const id = getRowId(row);\n const isSelected = selectedSet.has(id);\n const label = getRowLabel?.(row) ?? id;\n return (\n <TableRow\n key={id}\n selected={isSelected}\n tabIndex={interactive ? (i === activeRow ? 0 : -1) : undefined}\n onFocus={() => setActive(i)}\n onKeyDown={interactive ? (e) => onRowKeyDown(e, i, row) : undefined}\n onClick={\n onRowAction\n ? (e) => {\n if ((e.target as HTMLElement).closest(INTERACTIVE)) return;\n onRowAction(row);\n }\n : undefined\n }\n className={cn(\n interactive &&\n 'outline-none focus-visible:bg-muted/60 focus-visible:shadow-[inset_2px_0_0_var(--ring)]',\n onRowAction && 'cursor-pointer',\n )}\n >\n {selectable && (\n <TableCell className=\"w-10\">\n <Checkbox\n checked={isSelected}\n onCheckedChange={() => toggle(id)}\n aria-label={`Select ${label}`}\n tabIndex={-1}\n />\n </TableCell>\n )}\n {columns.map((col) => (\n <TableCell key={col.id} numeric={col.numeric} className={col.className}>\n {col.cell(row)}\n </TableCell>\n ))}\n {rowActions && <TableCell className=\"w-12 text-right\">{rowActions(row)}</TableCell>}\n </TableRow>\n );\n });\n }\n\n return (\n <div data-slot=\"data-table\" className={cn('grid min-w-0 gap-3', className)} {...props}>\n {showToolbar && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {onFilterChange && (\n <div className=\"relative w-full max-w-xs min-w-0 flex-1\">\n <Search\n className=\"pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground\"\n aria-hidden\n />\n <Input\n type=\"search\"\n value={filter ?? ''}\n onChange={(e) => onFilterChange(e.target.value)}\n placeholder={filterPlaceholder}\n aria-label={`Search ${ariaLabel.toLowerCase()}`}\n className=\"pl-8\"\n />\n </div>\n )}\n {selectable && (\n <span aria-live=\"polite\" className=\"text-xs text-muted-foreground tabular-nums\">\n {selectedSet.size ? `${selectedSet.size} selected` : ''}\n </span>\n )}\n {toolbar && <div className=\"ml-auto flex flex-wrap items-center gap-2\">{toolbar}</div>}\n </div>\n )}\n <div ref={frameRef} className=\"rounded-lg border bg-surface\">\n <Table\n aria-label={ariaLabel}\n aria-busy={loading || undefined}\n density={density}\n stickyHeader={stickyHeader}\n className={cn(loading && rows.length > 0 && 'opacity-60 transition-opacity')}\n >\n <TableHeader>\n <TableRow className=\"hover:bg-transparent\">\n {selectable && (\n <TableHead className=\"w-10\">\n <Checkbox\n checked={allState}\n onCheckedChange={toggleAll}\n disabled={rows.length === 0}\n aria-label=\"Select all rows on this page\"\n />\n </TableHead>\n )}\n {columns.map((col) => {\n const sorted = sort?.columnId === col.id ? sort.direction : undefined;\n return (\n <TableHead\n key={col.id}\n numeric={col.numeric}\n className={col.className}\n aria-sort={\n col.sortable\n ? sorted === 'asc'\n ? 'ascending'\n : sorted === 'desc'\n ? 'descending'\n : 'none'\n : undefined\n }\n >\n {col.sortable && onSortChange ? (\n <button\n type=\"button\"\n onClick={() => onSortChange(nextSort(sort, col.id))}\n className={cn(\n '-mx-1.5 inline-flex h-7 items-center gap-1 rounded-sm px-1.5 uppercase outline-none hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20',\n sorted && 'text-foreground',\n col.numeric && 'flex-row-reverse',\n )}\n >\n {col.header}\n {sorted === 'asc' ? (\n <ArrowUp className=\"size-3.5\" aria-hidden />\n ) : sorted === 'desc' ? (\n <ArrowDown className=\"size-3.5\" aria-hidden />\n ) : (\n <ChevronsUpDown className=\"size-3.5 opacity-50\" aria-hidden />\n )}\n </button>\n ) : (\n col.header\n )}\n </TableHead>\n );\n })}\n {rowActions && (\n <TableHead className=\"w-12\">\n <span className=\"sr-only\">Actions</span>\n </TableHead>\n )}\n </TableRow>\n </TableHeader>\n <TableBody>{body}</TableBody>\n </Table>\n </div>\n {onPageChange && page !== undefined && pageCount !== undefined && pageCount > 0 && (\n <Pagination\n page={page}\n pageCount={pageCount}\n onPageChange={onPageChange}\n summary={pageSummary}\n />\n )}\n </div>\n );\n}\n\nexport { DataTable };\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+ export interface MobileNavItem {
3
+ id: string;
4
+ label: string;
5
+ /** An icon element, e.g. <MessageSquare />; sized to 20px. */
6
+ icon: React.ReactNode;
7
+ /** Renders a link; leave out for a button that only calls onValueChange. */
8
+ href?: string;
9
+ /** A count, or true for a dot. */
10
+ badge?: number | boolean;
11
+ }
12
+ export interface MobileNavProps extends Omit<React.ComponentProps<'nav'>, 'onChange'> {
13
+ /** Three to five destinations the app defines. */
14
+ items: MobileNavItem[];
15
+ /** The current destination's id. */
16
+ value: string;
17
+ /**
18
+ * Called on every press. For a link, call `event.preventDefault()` and
19
+ * route yourself to keep a single-page app from reloading.
20
+ */
21
+ onValueChange?: (id: string, event: React.MouseEvent<HTMLElement>) => void;
22
+ /** `fixed` pins it to the bottom of the viewport. */
23
+ position?: 'fixed' | 'static';
24
+ 'aria-label'?: string;
25
+ }
26
+ /**
27
+ * The phone's bottom tab bar: icon over a visible label, 44px+ targets, and
28
+ * room for the home indicator (safe-area insets). Destinations come from the
29
+ * app; don't pad it with empty sections.
30
+ */
31
+ declare function MobileNav({ items, value, onValueChange, position, className, 'aria-label': ariaLabel, ...props }: MobileNavProps): React.JSX.Element;
32
+ export { MobileNav };
@@ -0,0 +1,72 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/components/mobile-nav.tsx
5
+ /**
6
+ * The phone's bottom tab bar: icon over a visible label, 44px+ targets, and
7
+ * room for the home indicator (safe-area insets). Destinations come from the
8
+ * app; don't pad it with empty sections.
9
+ */
10
+ function MobileNav({ items, value, onValueChange, position = "static", className, "aria-label": ariaLabel = "Primary", ...props }) {
11
+ return /* @__PURE__ */ jsx("nav", {
12
+ "data-slot": "mobile-nav",
13
+ "aria-label": ariaLabel,
14
+ className: cn("border-t bg-surface/95 pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] backdrop-blur supports-[backdrop-filter]:bg-surface/80", position === "fixed" && "fixed inset-x-0 bottom-0 z-40", className),
15
+ ...props,
16
+ children: /* @__PURE__ */ jsx("ul", {
17
+ className: "mx-auto flex max-w-lg",
18
+ children: items.map((item) => {
19
+ const active = item.id === value;
20
+ const count = typeof item.badge === "number" && item.badge > 0 ? item.badge : void 0;
21
+ const dot = item.badge === true;
22
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
23
+ /* @__PURE__ */ jsxs("span", {
24
+ className: "relative [&_svg]:size-5",
25
+ "aria-hidden": true,
26
+ children: [item.icon, (count !== void 0 || dot) && /* @__PURE__ */ jsx("span", {
27
+ className: cn("absolute -top-1 left-3 grid place-items-center rounded-full bg-destructive text-[10px] leading-none font-semibold text-destructive-foreground ring-2 ring-surface tabular-nums", count !== void 0 ? "h-4 min-w-4 px-1" : "size-2"),
28
+ children: count !== void 0 ? count > 99 ? "99+" : count : null
29
+ })]
30
+ }),
31
+ /* @__PURE__ */ jsx("span", {
32
+ className: "max-w-full truncate",
33
+ children: item.label
34
+ }),
35
+ count !== void 0 && /* @__PURE__ */ jsxs("span", {
36
+ className: "sr-only",
37
+ children: [
38
+ ", ",
39
+ count,
40
+ " new"
41
+ ]
42
+ }),
43
+ dot && /* @__PURE__ */ jsx("span", {
44
+ className: "sr-only",
45
+ children: ", new"
46
+ })
47
+ ] });
48
+ const classes = cn("flex min-h-14 w-full min-w-11 flex-col items-center justify-center gap-1 px-1 py-1.5 text-[11px] font-medium text-muted-foreground outline-none transition-colors select-none [-webkit-tap-highlight-color:transparent] hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/25 focus-visible:ring-inset", active && "text-brand hover:text-brand");
49
+ return /* @__PURE__ */ jsx("li", {
50
+ className: "min-w-0 flex-1",
51
+ children: item.href ? /* @__PURE__ */ jsx("a", {
52
+ href: item.href,
53
+ "aria-current": active ? "page" : void 0,
54
+ onClick: (e) => onValueChange?.(item.id, e),
55
+ className: classes,
56
+ children: content
57
+ }) : /* @__PURE__ */ jsx("button", {
58
+ type: "button",
59
+ "aria-current": active ? "page" : void 0,
60
+ onClick: (e) => onValueChange?.(item.id, e),
61
+ className: classes,
62
+ children: content
63
+ })
64
+ }, item.id);
65
+ })
66
+ })
67
+ });
68
+ }
69
+ //#endregion
70
+ export { MobileNav };
71
+
72
+ //# sourceMappingURL=mobile-nav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile-nav.js","names":["_Fragment"],"sources":["../../src/components/mobile-nav.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nexport interface MobileNavItem {\n id: string;\n label: string;\n /** An icon element, e.g. <MessageSquare />; sized to 20px. */\n icon: React.ReactNode;\n /** Renders a link; leave out for a button that only calls onValueChange. */\n href?: string;\n /** A count, or true for a dot. */\n badge?: number | boolean;\n}\n\nexport interface MobileNavProps extends Omit<React.ComponentProps<'nav'>, 'onChange'> {\n /** Three to five destinations the app defines. */\n items: MobileNavItem[];\n /** The current destination's id. */\n value: string;\n /**\n * Called on every press. For a link, call `event.preventDefault()` and\n * route yourself to keep a single-page app from reloading.\n */\n onValueChange?: (id: string, event: React.MouseEvent<HTMLElement>) => void;\n /** `fixed` pins it to the bottom of the viewport. */\n position?: 'fixed' | 'static';\n 'aria-label'?: string;\n}\n\n/**\n * The phone's bottom tab bar: icon over a visible label, 44px+ targets, and\n * room for the home indicator (safe-area insets). Destinations come from the\n * app; don't pad it with empty sections.\n */\nfunction MobileNav({\n items,\n value,\n onValueChange,\n position = 'static',\n className,\n 'aria-label': ariaLabel = 'Primary',\n ...props\n}: MobileNavProps) {\n return (\n <nav\n data-slot=\"mobile-nav\"\n aria-label={ariaLabel}\n className={cn(\n 'border-t bg-surface/95 pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] backdrop-blur supports-[backdrop-filter]:bg-surface/80',\n position === 'fixed' && 'fixed inset-x-0 bottom-0 z-40',\n className,\n )}\n {...props}\n >\n <ul className=\"mx-auto flex max-w-lg\">\n {items.map((item) => {\n const active = item.id === value;\n const count = typeof item.badge === 'number' && item.badge > 0 ? item.badge : undefined;\n const dot = item.badge === true;\n const content = (\n <>\n <span className=\"relative [&_svg]:size-5\" aria-hidden>\n {item.icon}\n {(count !== undefined || dot) && (\n <span\n className={cn(\n 'absolute -top-1 left-3 grid place-items-center rounded-full bg-destructive text-[10px] leading-none font-semibold text-destructive-foreground ring-2 ring-surface tabular-nums',\n count !== undefined ? 'h-4 min-w-4 px-1' : 'size-2',\n )}\n >\n {count !== undefined ? (count > 99 ? '99+' : count) : null}\n </span>\n )}\n </span>\n <span className=\"max-w-full truncate\">{item.label}</span>\n {count !== undefined && <span className=\"sr-only\">, {count} new</span>}\n {dot && <span className=\"sr-only\">, new</span>}\n </>\n );\n const classes = cn(\n 'flex min-h-14 w-full min-w-11 flex-col items-center justify-center gap-1 px-1 py-1.5 text-[11px] font-medium text-muted-foreground outline-none transition-colors select-none [-webkit-tap-highlight-color:transparent] hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/25 focus-visible:ring-inset',\n active && 'text-brand hover:text-brand',\n );\n return (\n <li key={item.id} className=\"min-w-0 flex-1\">\n {item.href ? (\n <a\n href={item.href}\n aria-current={active ? 'page' : undefined}\n onClick={(e) => onValueChange?.(item.id, e)}\n className={classes}\n >\n {content}\n </a>\n ) : (\n <button\n type=\"button\"\n aria-current={active ? 'page' : undefined}\n onClick={(e) => onValueChange?.(item.id, e)}\n className={classes}\n >\n {content}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n </nav>\n );\n}\n\nexport { MobileNav };\n"],"mappings":";;;;;;;;;AAmCA,SAAS,UAAU,EACjB,OACA,OACA,eACA,WAAW,UACX,WACA,cAAc,YAAY,WAC1B,GAAG,SACc;CACjB,OACE,oBAAC,OAAD;EACE,aAAU;EACV,cAAY;EACZ,WAAW,GACT,iLACA,aAAa,WAAW,iCACxB,SACF;EACA,GAAI;EAEJ,UAAA,oBAAC,MAAD;GAAI,WAAU;GACX,UAAA,MAAM,KAAK,SAAS;IACnB,MAAM,SAAS,KAAK,OAAO;IAC3B,MAAM,QAAQ,OAAO,KAAK,UAAU,YAAY,KAAK,QAAQ,IAAI,KAAK,QAAQ,KAAA;IAC9E,MAAM,MAAM,KAAK,UAAU;IAC3B,MAAM,UACJ,qBAAAA,UAAA,EAAA,UAAA;KACE,qBAAC,QAAD;MAAM,WAAU;MAA0B,eAAA;MAA1C,UAAA,CACG,KAAK,OACJ,UAAU,KAAA,KAAa,QACvB,oBAAC,QAAD;OACE,WAAW,GACT,kLACA,UAAU,KAAA,IAAY,qBAAqB,QAC7C;OAEC,UAAA,UAAU,KAAA,IAAa,QAAQ,KAAK,QAAQ,QAAS;MAClD,CAAA,CAEJ;;KACN,oBAAC,QAAD;MAAM,WAAU;MAAuB,UAAA,KAAK;KAAY,CAAA;KACvD,UAAU,KAAA,KAAa,qBAAC,QAAD;MAAM,WAAU;MAAhB,UAAA;OAA0B;OAAG;OAAM;MAAU;;KACpE,OAAO,oBAAC,QAAD;MAAM,WAAU;MAAU,UAAA;KAAW,CAAA;IAC7C,EAAA,CAAA;IAEJ,MAAM,UAAU,GACd,8TACA,UAAU,6BACZ;IACA,OACE,oBAAC,MAAD;KAAkB,WAAU;KACzB,UAAA,KAAK,OACJ,oBAAC,KAAD;MACE,MAAM,KAAK;MACX,gBAAc,SAAS,SAAS,KAAA;MAChC,UAAU,MAAM,gBAAgB,KAAK,IAAI,CAAC;MAC1C,WAAW;MAEV,UAAA;KACA,CAAA,IAEH,oBAAC,UAAD;MACE,MAAK;MACL,gBAAc,SAAS,SAAS,KAAA;MAChC,UAAU,MAAM,gBAAgB,KAAK,IAAI,CAAC;MAC1C,WAAW;MAEV,UAAA;KACK,CAAA;IAER,GApBK,KAAK,EAoBV;GAER,CAAC;EACC,CAAA;CACD,CAAA;AAET"}
@@ -6,11 +6,17 @@ declare function SheetClose(props: React.ComponentProps<typeof SheetPrimitive.Cl
6
6
  declare const sideClasses: {
7
7
  readonly right: "inset-y-0 right-0 h-full w-3/4 border-l [--bl-sheet-x:100%] sm:max-w-sm";
8
8
  readonly left: "inset-y-0 left-0 h-full w-3/4 border-r [--bl-sheet-x:-100%] sm:max-w-sm";
9
- readonly top: "inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b [--bl-sheet-x:0] [--bl-sheet-y:-100%]";
10
- readonly bottom: "inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t [--bl-sheet-x:0] [--bl-sheet-y:100%]";
9
+ readonly top: "inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b pt-[env(safe-area-inset-top)] [--bl-sheet-x:0] [--bl-sheet-y:-100%]";
10
+ readonly bottom: "inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t pb-[env(safe-area-inset-bottom)] [--bl-sheet-x:0] [--bl-sheet-y:100%]";
11
11
  };
12
- declare function SheetContent({ className, children, side, ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & {
12
+ declare function SheetContent({ className, children, side, handle, ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & {
13
13
  side?: keyof typeof sideClasses;
14
+ /**
15
+ * A grab bar at the top edge, the cue that this is a drawer. Visual only:
16
+ * it does not drag; close with the button, Escape or the overlay. On by
17
+ * default for bottom sheets.
18
+ */
19
+ handle?: boolean;
14
20
  }): React.JSX.Element;
15
21
  declare function SheetHeader({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
16
22
  declare function SheetFooter({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
@@ -25,10 +25,10 @@ function SheetClose(props) {
25
25
  var sideClasses = {
26
26
  right: "inset-y-0 right-0 h-full w-3/4 border-l [--bl-sheet-x:100%] sm:max-w-sm",
27
27
  left: "inset-y-0 left-0 h-full w-3/4 border-r [--bl-sheet-x:-100%] sm:max-w-sm",
28
- top: "inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b [--bl-sheet-x:0] [--bl-sheet-y:-100%]",
29
- bottom: "inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t [--bl-sheet-x:0] [--bl-sheet-y:100%]"
28
+ top: "inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b pt-[env(safe-area-inset-top)] [--bl-sheet-x:0] [--bl-sheet-y:-100%]",
29
+ bottom: "inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t pb-[env(safe-area-inset-bottom)] [--bl-sheet-x:0] [--bl-sheet-y:100%]"
30
30
  };
31
- function SheetContent({ className, children, side = "right", ...props }) {
31
+ function SheetContent({ className, children, side = "right", handle = side === "bottom", ...props }) {
32
32
  return /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, {
33
33
  "data-slot": "sheet-overlay",
34
34
  className: "fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in"
@@ -36,13 +36,21 @@ function SheetContent({ className, children, side = "right", ...props }) {
36
36
  "data-slot": "sheet-content",
37
37
  className: cn("fixed z-50 max-h-dvh overflow-y-auto overscroll-contain flex flex-col gap-4 border-border-strong bg-surface-raised text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-sheet-out data-[state=open]:animate-sheet-in", sideClasses[side], className),
38
38
  ...props,
39
- children: [children, /* @__PURE__ */ jsxs(Dialog.Close, {
40
- className: "absolute top-3.5 right-3.5 grid size-8 place-items-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20 [&_svg]:size-4",
41
- children: [/* @__PURE__ */ jsx(X, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", {
42
- className: "sr-only",
43
- children: "Close"
44
- })]
45
- })]
39
+ children: [
40
+ handle && /* @__PURE__ */ jsx("div", {
41
+ "data-slot": "sheet-handle",
42
+ "aria-hidden": true,
43
+ className: "mx-auto mt-2 -mb-2 h-1 w-10 shrink-0 rounded-full bg-border-strong"
44
+ }),
45
+ children,
46
+ /* @__PURE__ */ jsxs(Dialog.Close, {
47
+ className: cn("absolute right-3 grid size-8 place-items-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20 pointer-coarse:size-11 [&_svg]:size-4 pointer-coarse:[&_svg]:size-5", side === "top" ? "top-[calc(env(safe-area-inset-top)+0.75rem)]" : "top-3"),
48
+ children: [/* @__PURE__ */ jsx(X, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", {
49
+ className: "sr-only",
50
+ children: "Close"
51
+ })]
52
+ })
53
+ ]
46
54
  })] });
47
55
  }
48
56
  function SheetHeader({ className, ...props }) {
@@ -1 +1 @@
1
- {"version":3,"file":"sheet.js","names":["SheetPrimitive"],"sources":["../../src/components/sheet.tsx"],"sourcesContent":["import X from '@burtson-labs/icons/react/x';\nimport { Dialog as SheetPrimitive } from 'radix-ui';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nfunction Sheet(props: React.ComponentProps<typeof SheetPrimitive.Root>) {\n return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger(props: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose(props: React.ComponentProps<typeof SheetPrimitive.Close>) {\n return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nconst sideClasses = {\n right: 'inset-y-0 right-0 h-full w-3/4 border-l [--bl-sheet-x:100%] sm:max-w-sm',\n left: 'inset-y-0 left-0 h-full w-3/4 border-r [--bl-sheet-x:-100%] sm:max-w-sm',\n // Top and bottom sheets are the mobile drawers.\n top: 'inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b [--bl-sheet-x:0] [--bl-sheet-y:-100%]',\n bottom:\n 'inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t [--bl-sheet-x:0] [--bl-sheet-y:100%]',\n} as const;\n\nfunction SheetContent({\n className,\n children,\n side = 'right',\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & { side?: keyof typeof sideClasses }) {\n return (\n <SheetPrimitive.Portal>\n <SheetPrimitive.Overlay\n data-slot=\"sheet-overlay\"\n className=\"fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in\"\n />\n <SheetPrimitive.Content\n data-slot=\"sheet-content\"\n className={cn(\n 'fixed z-50 max-h-dvh overflow-y-auto overscroll-contain flex flex-col gap-4 border-border-strong bg-surface-raised text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-sheet-out data-[state=open]:animate-sheet-in',\n sideClasses[side],\n className,\n )}\n {...props}\n >\n {children}\n <SheetPrimitive.Close className=\"absolute top-3.5 right-3.5 grid size-8 place-items-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20 [&_svg]:size-4\">\n <X aria-hidden />\n <span className=\"sr-only\">Close</span>\n </SheetPrimitive.Close>\n </SheetPrimitive.Content>\n </SheetPrimitive.Portal>\n );\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"sheet-header\"\n className={cn('flex flex-col gap-1.5 p-4', className)}\n {...props}\n />\n );\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"sheet-footer\"\n className={cn('mt-auto flex flex-col gap-2 p-4', className)}\n {...props}\n />\n );\n}\n\nfunction SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {\n return (\n <SheetPrimitive.Title\n data-slot=\"sheet-title\"\n className={cn('text-base font-semibold tracking-[-0.02em] text-foreground', className)}\n {...props}\n />\n );\n}\n\nfunction SheetDescription({\n className,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n return (\n <SheetPrimitive.Description\n data-slot=\"sheet-description\"\n className={cn('text-sm text-muted-foreground', className)}\n {...props}\n />\n );\n}\n\nexport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n};\n"],"mappings":";;;;;;AAMA,SAAS,MAAM,OAAyD;CACtE,OAAO,oBAACA,OAAe,MAAhB;EAAqB,aAAU;EAAQ,GAAI;CAAQ,CAAA;AAC5D;AAEA,SAAS,aAAa,OAA4D;CAChF,OAAO,oBAACA,OAAe,SAAhB;EAAwB,aAAU;EAAgB,GAAI;CAAQ,CAAA;AACvE;AAEA,SAAS,WAAW,OAA0D;CAC5E,OAAO,oBAACA,OAAe,OAAhB;EAAsB,aAAU;EAAc,GAAI;CAAQ,CAAA;AACnE;AAEA,IAAM,cAAc;CAClB,OAAO;CACP,MAAM;CAEN,KAAK;CACL,QACE;AACJ;AAEA,SAAS,aAAa,EACpB,WACA,UACA,OAAO,SACP,GAAG,SACyF;CAC5F,OACE,qBAACA,OAAe,QAAhB,EAAA,UAAA,CACE,oBAACA,OAAe,SAAhB;EACE,aAAU;EACV,WAAU;CACX,CAAA,GACD,qBAACA,OAAe,SAAhB;EACE,aAAU;EACV,WAAW,GACT,mQACA,YAAY,OACZ,SACF;EACA,GAAI;EAPN,UAAA,CASG,UACD,qBAACA,OAAe,OAAhB;GAAsB,WAAU;GAAhC,UAAA,CACE,oBAAC,GAAD,EAAG,eAAA,KAAa,CAAA,GAChB,oBAAC,QAAD;IAAM,WAAU;IAAU,UAAA;GAAW,CAAA,CACjB;EACA,CAAA,CAAA;CACH,CAAA,CAAA,EAAA,CAAA;AAE3B;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,SAAsC;CACzE,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,6BAA6B,SAAS;EACpD,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,SAAsC;CACzE,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,mCAAmC,SAAS;EAC1D,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,SAA4D;CAC9F,OACE,oBAACA,OAAe,OAAhB;EACE,aAAU;EACV,WAAW,GAAG,8DAA8D,SAAS;EACrF,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,iBAAiB,EACxB,WACA,GAAG,SACuD;CAC1D,OACE,oBAACA,OAAe,aAAhB;EACE,aAAU;EACV,WAAW,GAAG,iCAAiC,SAAS;EACxD,GAAI;CACL,CAAA;AAEL"}
1
+ {"version":3,"file":"sheet.js","names":["SheetPrimitive"],"sources":["../../src/components/sheet.tsx"],"sourcesContent":["import X from '@burtson-labs/icons/react/x';\nimport { Dialog as SheetPrimitive } from 'radix-ui';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nfunction Sheet(props: React.ComponentProps<typeof SheetPrimitive.Root>) {\n return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger(props: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose(props: React.ComponentProps<typeof SheetPrimitive.Close>) {\n return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nconst sideClasses = {\n right: 'inset-y-0 right-0 h-full w-3/4 border-l [--bl-sheet-x:100%] sm:max-w-sm',\n left: 'inset-y-0 left-0 h-full w-3/4 border-r [--bl-sheet-x:-100%] sm:max-w-sm',\n // Top and bottom sheets are the mobile drawers. Each keeps clear of the\n // notch or home indicator on its edge (safe-area insets).\n top: 'inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b pt-[env(safe-area-inset-top)] [--bl-sheet-x:0] [--bl-sheet-y:-100%]',\n bottom:\n 'inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t pb-[env(safe-area-inset-bottom)] [--bl-sheet-x:0] [--bl-sheet-y:100%]',\n} as const;\n\nfunction SheetContent({\n className,\n children,\n side = 'right',\n handle = side === 'bottom',\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n side?: keyof typeof sideClasses;\n /**\n * A grab bar at the top edge, the cue that this is a drawer. Visual only:\n * it does not drag; close with the button, Escape or the overlay. On by\n * default for bottom sheets.\n */\n handle?: boolean;\n}) {\n return (\n <SheetPrimitive.Portal>\n <SheetPrimitive.Overlay\n data-slot=\"sheet-overlay\"\n className=\"fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in\"\n />\n <SheetPrimitive.Content\n data-slot=\"sheet-content\"\n className={cn(\n 'fixed z-50 max-h-dvh overflow-y-auto overscroll-contain flex flex-col gap-4 border-border-strong bg-surface-raised text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-sheet-out data-[state=open]:animate-sheet-in',\n sideClasses[side],\n className,\n )}\n {...props}\n >\n {handle && (\n <div\n data-slot=\"sheet-handle\"\n aria-hidden\n className=\"mx-auto mt-2 -mb-2 h-1 w-10 shrink-0 rounded-full bg-border-strong\"\n />\n )}\n {children}\n {/* 44px on touch screens, 32px with a mouse. */}\n <SheetPrimitive.Close\n className={cn(\n 'absolute right-3 grid size-8 place-items-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20 pointer-coarse:size-11 [&_svg]:size-4 pointer-coarse:[&_svg]:size-5',\n side === 'top' ? 'top-[calc(env(safe-area-inset-top)+0.75rem)]' : 'top-3',\n )}\n >\n <X aria-hidden />\n <span className=\"sr-only\">Close</span>\n </SheetPrimitive.Close>\n </SheetPrimitive.Content>\n </SheetPrimitive.Portal>\n );\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"sheet-header\"\n className={cn('flex flex-col gap-1.5 p-4', className)}\n {...props}\n />\n );\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"sheet-footer\"\n className={cn('mt-auto flex flex-col gap-2 p-4', className)}\n {...props}\n />\n );\n}\n\nfunction SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {\n return (\n <SheetPrimitive.Title\n data-slot=\"sheet-title\"\n className={cn('text-base font-semibold tracking-[-0.02em] text-foreground', className)}\n {...props}\n />\n );\n}\n\nfunction SheetDescription({\n className,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n return (\n <SheetPrimitive.Description\n data-slot=\"sheet-description\"\n className={cn('text-sm text-muted-foreground', className)}\n {...props}\n />\n );\n}\n\nexport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n};\n"],"mappings":";;;;;;AAMA,SAAS,MAAM,OAAyD;CACtE,OAAO,oBAACA,OAAe,MAAhB;EAAqB,aAAU;EAAQ,GAAI;CAAQ,CAAA;AAC5D;AAEA,SAAS,aAAa,OAA4D;CAChF,OAAO,oBAACA,OAAe,SAAhB;EAAwB,aAAU;EAAgB,GAAI;CAAQ,CAAA;AACvE;AAEA,SAAS,WAAW,OAA0D;CAC5E,OAAO,oBAACA,OAAe,OAAhB;EAAsB,aAAU;EAAc,GAAI;CAAQ,CAAA;AACnE;AAEA,IAAM,cAAc;CAClB,OAAO;CACP,MAAM;CAGN,KAAK;CACL,QACE;AACJ;AAEA,SAAS,aAAa,EACpB,WACA,UACA,OAAO,SACP,SAAS,SAAS,UAClB,GAAG,SASF;CACD,OACE,qBAACA,OAAe,QAAhB,EAAA,UAAA,CACE,oBAACA,OAAe,SAAhB;EACE,aAAU;EACV,WAAU;CACX,CAAA,GACD,qBAACA,OAAe,SAAhB;EACE,aAAU;EACV,WAAW,GACT,mQACA,YAAY,OACZ,SACF;EACA,GAAI;EAPN,UAAA;GASG,UACC,oBAAC,OAAD;IACE,aAAU;IACV,eAAA;IACA,WAAU;GACX,CAAA;GAEF;GAED,qBAACA,OAAe,OAAhB;IACE,WAAW,GACT,gRACA,SAAS,QAAQ,iDAAiD,OACpE;IAJF,UAAA,CAME,oBAAC,GAAD,EAAG,eAAA,KAAa,CAAA,GAChB,oBAAC,QAAD;KAAM,WAAU;KAAU,UAAA;IAAW,CAAA,CACjB;;EACA;CACH,CAAA,CAAA,EAAA,CAAA;AAE3B;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,SAAsC;CACzE,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,6BAA6B,SAAS;EACpD,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,SAAsC;CACzE,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,mCAAmC,SAAS;EAC1D,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,SAA4D;CAC9F,OACE,oBAACA,OAAe,OAAhB;EACE,aAAU;EACV,WAAW,GAAG,8DAA8D,SAAS;EACrF,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,iBAAiB,EACxB,WACA,GAAG,SACuD;CAC1D,OACE,oBAACA,OAAe,aAAhB;EACE,aAAU;EACV,WAAW,GAAG,iCAAiC,SAAS;EACxD,GAAI;CACL,CAAA;AAEL"}
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+ import { PopoverTrigger } from './popover';
3
+ /** Where an answer came from. The app supplies provenance; this only shows it. */
4
+ export interface Source {
5
+ id: string;
6
+ title: string;
7
+ /** Only http(s) links are rendered as links. */
8
+ url?: string;
9
+ /** The passage the answer relied on. */
10
+ snippet?: string;
11
+ /** Short location, e.g. "page 4" or "Updated 2 days ago". */
12
+ meta?: string;
13
+ icon?: React.ReactNode;
14
+ }
15
+ /** An http(s) URL, or undefined for anything else (javascript:, data:, relative). */
16
+ export declare function safeSourceHref(url: string | undefined): string | undefined;
17
+ export interface SourceCitationProps extends Omit<React.ComponentProps<typeof PopoverTrigger>, 'children'> {
18
+ /** 1-based number shown in the text; matches the SourceList entry. */
19
+ index: number;
20
+ source: Source;
21
+ }
22
+ /** An inline [n] marker in an answer; opens the source it points to. */
23
+ declare function SourceCitation({ index, source, className, ...props }: SourceCitationProps): React.JSX.Element;
24
+ export interface SourceListProps extends React.ComponentProps<'section'> {
25
+ sources: Source[];
26
+ /** Heading; also the list's accessible name. */
27
+ label?: string;
28
+ }
29
+ /** Numbered sources under an answer, in citation order. */
30
+ declare function SourceList({ sources, label, className, ...props }: SourceListProps): React.JSX.Element | null;
31
+ export { SourceCitation, SourceList };