@butinapp/ui 0.1.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.
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/components/badge.d.ts +7 -0
- package/dist/components/badge.js +34 -0
- package/dist/components/button.d.ts +11 -0
- package/dist/components/button.js +30 -0
- package/dist/components/card.d.ts +8 -0
- package/dist/components/card.js +10 -0
- package/dist/components/checkbox.d.ts +3 -0
- package/dist/components/checkbox.js +7 -0
- package/dist/components/combobox.d.ts +14 -0
- package/dist/components/combobox.js +53 -0
- package/dist/components/data-table-model.d.ts +40 -0
- package/dist/components/data-table-model.js +105 -0
- package/dist/components/data-table.d.ts +33 -0
- package/dist/components/data-table.js +252 -0
- package/dist/components/dialog.d.ts +11 -0
- package/dist/components/dialog.js +15 -0
- package/dist/components/dropdown-menu.d.ts +7 -0
- package/dist/components/dropdown-menu.js +15 -0
- package/dist/components/echart.d.ts +5 -0
- package/dist/components/echart.js +27 -0
- package/dist/components/input.d.ts +3 -0
- package/dist/components/input.js +5 -0
- package/dist/components/label.d.ts +3 -0
- package/dist/components/label.js +5 -0
- package/dist/components/progress.d.ts +4 -0
- package/dist/components/progress.js +8 -0
- package/dist/components/select.d.ts +11 -0
- package/dist/components/select.js +5 -0
- package/dist/components/separator.d.ts +3 -0
- package/dist/components/separator.js +5 -0
- package/dist/components/service-icon.d.ts +19 -0
- package/dist/components/service-icon.js +49 -0
- package/dist/components/sheet.d.ts +10 -0
- package/dist/components/sheet.js +11 -0
- package/dist/components/skeleton.d.ts +2 -0
- package/dist/components/skeleton.js +4 -0
- package/dist/components/sparkline.d.ts +7 -0
- package/dist/components/sparkline.js +22 -0
- package/dist/components/switch.d.ts +3 -0
- package/dist/components/switch.js +5 -0
- package/dist/components/tabs.d.ts +8 -0
- package/dist/components/tabs.js +15 -0
- package/dist/components/tooltip.d.ts +6 -0
- package/dist/components/tooltip.js +8 -0
- package/dist/components/truncated-cell.d.ts +3 -0
- package/dist/components/truncated-cell.js +22 -0
- package/dist/dashboard.d.ts +12 -0
- package/dist/dashboard.js +13 -0
- package/dist/features/charts.d.ts +7 -0
- package/dist/features/charts.js +4 -0
- package/dist/features/dashboard/dashboard-renderer.d.ts +32 -0
- package/dist/features/dashboard/dashboard-renderer.js +322 -0
- package/dist/features/dashboard/format-role.d.ts +2 -0
- package/dist/features/dashboard/format-role.js +26 -0
- package/dist/features/dashboard/overview-model.d.ts +59 -0
- package/dist/features/dashboard/overview-model.js +170 -0
- package/dist/features/dashboard/overview.d.ts +8 -0
- package/dist/features/dashboard/overview.js +136 -0
- package/dist/features/dashboard/plan-views.d.ts +7 -0
- package/dist/features/dashboard/plan-views.js +29 -0
- package/dist/features/dashboard/rollup.d.ts +31 -0
- package/dist/features/dashboard/rollup.js +45 -0
- package/dist/features/dashboard/timeseries-chart.d.ts +11 -0
- package/dist/features/dashboard/timeseries-chart.js +191 -0
- package/dist/features/dashboard/view-models.d.ts +72 -0
- package/dist/features/dashboard/view-models.js +291 -0
- package/dist/features/documents/doc-models.d.ts +1 -0
- package/dist/features/documents/doc-models.js +17 -0
- package/dist/features/documents/open-file-button.d.ts +6 -0
- package/dist/features/documents/open-file-button.js +16 -0
- package/dist/features/shell/app-shell.d.ts +13 -0
- package/dist/features/shell/app-shell.js +22 -0
- package/dist/features/shell/conn-state.d.ts +10 -0
- package/dist/features/shell/conn-state.js +29 -0
- package/dist/features/shell/service-tabs.d.ts +13 -0
- package/dist/features/shell/service-tabs.js +20 -0
- package/dist/features/shell/sidebar.d.ts +31 -0
- package/dist/features/shell/sidebar.js +31 -0
- package/dist/features/shell/theme-toggle.d.ts +5 -0
- package/dist/features/shell/theme-toggle.js +12 -0
- package/dist/i18n/context.d.ts +16 -0
- package/dist/i18n/context.js +19 -0
- package/dist/i18n/format-context.d.ts +7 -0
- package/dist/i18n/format-context.js +6 -0
- package/dist/i18n/format.d.ts +10 -0
- package/dist/i18n/format.js +31 -0
- package/dist/i18n/index.d.ts +9 -0
- package/dist/i18n/index.js +8 -0
- package/dist/i18n/labels.d.ts +400 -0
- package/dist/i18n/labels.js +828 -0
- package/dist/i18n/plugin-text.d.ts +4 -0
- package/dist/i18n/plugin-text.js +150 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +15 -0
- package/dist/lib/echarts-theme.d.ts +38 -0
- package/dist/lib/echarts-theme.js +59 -0
- package/dist/lib/format.d.ts +4 -0
- package/dist/lib/format.js +18 -0
- package/dist/lib/monthly-buckets.d.ts +9 -0
- package/dist/lib/monthly-buckets.js +32 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +3 -0
- package/dist/primitives.d.ts +28 -0
- package/dist/primitives.js +26 -0
- package/dist/shell.d.ts +8 -0
- package/dist/shell.js +10 -0
- package/dist/theme.css +137 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +6 -0
- package/package.json +78 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
|
|
3
|
+
import { ArrowDown, ArrowUp, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, Columns3, Download, Search } from 'lucide-react';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { useLabels } from '../i18n/context.js';
|
|
6
|
+
import { cn } from '../lib/utils.js';
|
|
7
|
+
import { Checkbox } from './checkbox.js';
|
|
8
|
+
import { formatRows, normalizeTablePrefs, selection as sel, sortRows } from './data-table-model.js';
|
|
9
|
+
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './dropdown-menu.js';
|
|
10
|
+
import { Input } from './input.js';
|
|
11
|
+
// First entry is the default selection (see pageSize seeding below).
|
|
12
|
+
const DEFAULT_PAGE_SIZES = [15, 25, 50, 100];
|
|
13
|
+
// Generic, headless, theme-portable table. TanStack owns sorting/column-order/visibility/pagination;
|
|
14
|
+
// selection (external Set) and grouping (manual collapsible sections over the sorted rows) are layered
|
|
15
|
+
// on top so grouping stays fully controllable. Pure — prefs in via props, export out via callback.
|
|
16
|
+
export const DataTable = ({ columns, rows, getRowId, selection, onSelectionChange, grouped, expandable, paginated, pageSizes = DEFAULT_PAGE_SIZES, initialState, onStateChange, onExport, exportName = 'export', toolbar, toolbarExtra, actionBar, emptyLabel, searchFloating = true }) => {
|
|
17
|
+
const t = useLabels();
|
|
18
|
+
const selectable = Boolean(selection && onSelectionChange);
|
|
19
|
+
const usePaging = Boolean(paginated) && !grouped;
|
|
20
|
+
// Local, ephemeral row expansion (which rows have their detail open) — never persisted, same as group
|
|
21
|
+
// collapse below. `expand` bundles the per-row hooks the rows need; absent when no expandable prop.
|
|
22
|
+
const [expandedRows, setExpandedRows] = useState(new Set());
|
|
23
|
+
const expand = expandable
|
|
24
|
+
? {
|
|
25
|
+
detail: expandable,
|
|
26
|
+
isOpen: (id) => expandedRows.has(id),
|
|
27
|
+
toggle: (id) => setExpandedRows((prev) => {
|
|
28
|
+
const next = new Set(prev);
|
|
29
|
+
if (next.has(id)) {
|
|
30
|
+
next.delete(id);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
next.add(id);
|
|
34
|
+
}
|
|
35
|
+
return next;
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
: undefined;
|
|
39
|
+
// Sanitize persisted prefs against the CURRENT column keys before seeding — a plugin may have added/
|
|
40
|
+
// removed/renamed columns since the prefs were saved. Seeded once; later changes are local state.
|
|
41
|
+
const seed = useMemo(() => normalizeTablePrefs(initialState, columns.map((c) => c.key)), [initialState, columns]);
|
|
42
|
+
// The controlled state the seed maps to. Computed from `seed` so it tracks the persisted prefs, which the
|
|
43
|
+
// host loads asynchronously — the real values arrive AFTER first mount.
|
|
44
|
+
const seedStates = useMemo(() => ({
|
|
45
|
+
sorting: (seed.sort ? [{ id: seed.sort.key, desc: seed.sort.dir === 'desc' }] : []),
|
|
46
|
+
columnOrder: (seed.columnOrder ?? []),
|
|
47
|
+
columnVisibility: (seed.columnVisibility ?? {}),
|
|
48
|
+
pageSize: seed.pageSize ?? pageSizes[0] ?? 15
|
|
49
|
+
}), [seed, pageSizes]);
|
|
50
|
+
const [sorting, setSorting] = useState(seedStates.sorting);
|
|
51
|
+
const [columnOrder, setColumnOrder] = useState(seedStates.columnOrder);
|
|
52
|
+
const [columnVisibility, setColumnVisibility] = useState(seedStates.columnVisibility);
|
|
53
|
+
const [pageSize, setPageSize] = useState(seedStates.pageSize);
|
|
54
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
55
|
+
const [query, setQuery] = useState('');
|
|
56
|
+
// The host owns persistence and may recreate `onStateChange` every render; hold it in a ref so emitting
|
|
57
|
+
// never depends on (and never re-fires from) its identity churning.
|
|
58
|
+
const onStateChangeRef = useRef(onStateChange);
|
|
59
|
+
onStateChangeRef.current = onStateChange;
|
|
60
|
+
// Adopt the seed whenever its CONTENT changes — the persisted prefs load after first mount, so re-seed the
|
|
61
|
+
// controlled state once they arrive, and mark that change programmatic (`skipEmit`) so it is NOT echoed back
|
|
62
|
+
// to the host. Without this, the post-load emit would clobber the just-loaded config with the pre-load
|
|
63
|
+
// default. Compared by content (not object identity) so a host refetch returning the same values is a no-op
|
|
64
|
+
// and never discards an unsaved in-flight edit.
|
|
65
|
+
const seedSig = JSON.stringify(seedStates);
|
|
66
|
+
const appliedSig = useRef(seedSig);
|
|
67
|
+
const skipEmit = useRef(false);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (appliedSig.current === seedSig) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
appliedSig.current = seedSig;
|
|
73
|
+
skipEmit.current = true;
|
|
74
|
+
setSorting(seedStates.sorting);
|
|
75
|
+
setColumnOrder(seedStates.columnOrder);
|
|
76
|
+
setColumnVisibility(seedStates.columnVisibility);
|
|
77
|
+
setPageSize(seedStates.pageSize);
|
|
78
|
+
}, [seedSig, seedStates]);
|
|
79
|
+
// Emit prefs on a USER change (sort/order/visibility/pageSize); the host debounces before persisting. The
|
|
80
|
+
// first run (mount) and any programmatic seed-adopt are skipped, so only real edits persist.
|
|
81
|
+
const firstRun = useRef(true);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (firstRun.current) {
|
|
84
|
+
firstRun.current = false;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (skipEmit.current) {
|
|
88
|
+
skipEmit.current = false;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
onStateChangeRef.current?.({
|
|
92
|
+
sort: sorting[0] ? { key: sorting[0].id, dir: sorting[0].desc ? 'desc' : 'asc' } : undefined,
|
|
93
|
+
columnOrder: columnOrder.length > 0 ? columnOrder : undefined,
|
|
94
|
+
columnVisibility,
|
|
95
|
+
pageSize
|
|
96
|
+
});
|
|
97
|
+
}, [sorting, columnOrder, columnVisibility, pageSize]);
|
|
98
|
+
const tanCols = useMemo(() => columns.map((col) => ({
|
|
99
|
+
id: col.key,
|
|
100
|
+
accessorFn: (row) => (col.sortValue ? col.sortValue(row) : row[col.key]),
|
|
101
|
+
header: col.label,
|
|
102
|
+
enableSorting: col.sortable !== false,
|
|
103
|
+
enableHiding: col.hideable !== false
|
|
104
|
+
})), [columns]);
|
|
105
|
+
const table = useReactTable({
|
|
106
|
+
data: rows,
|
|
107
|
+
columns: tanCols,
|
|
108
|
+
state: { sorting, columnOrder, columnVisibility, ...(usePaging ? { pagination: { pageIndex, pageSize } } : {}) },
|
|
109
|
+
onSortingChange: setSorting,
|
|
110
|
+
onColumnOrderChange: setColumnOrder,
|
|
111
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
112
|
+
getRowId: (row) => getRowId(row),
|
|
113
|
+
getCoreRowModel: getCoreRowModel(),
|
|
114
|
+
getSortedRowModel: getSortedRowModel()
|
|
115
|
+
});
|
|
116
|
+
// The fully-resolved, ordered, visible leaf columns (for export + drag reorder + header render).
|
|
117
|
+
const leafCols = table.getVisibleLeafColumns();
|
|
118
|
+
const visibleModelCols = useMemo(() => leafCols.map((c) => columns.find((dc) => dc.key === c.id)).filter((c) => c != null), [leafCols, columns]);
|
|
119
|
+
// Sorted, search-filtered rows (raw Row objects). Search matches across export values, case-insensitive.
|
|
120
|
+
const filtered = useMemo(() => {
|
|
121
|
+
const sorted = sorting[0]
|
|
122
|
+
? sortRows(rows, { key: sorting[0].id, dir: sorting[0].desc ? 'desc' : 'asc' }, columns)
|
|
123
|
+
: rows;
|
|
124
|
+
const q = query.trim().toLowerCase();
|
|
125
|
+
if (!q) {
|
|
126
|
+
return sorted;
|
|
127
|
+
}
|
|
128
|
+
return sorted.filter((row) => columns.some((c) => {
|
|
129
|
+
const v = c.exportValue ? c.exportValue(row) : row[c.key];
|
|
130
|
+
return v != null && String(v).toLowerCase().includes(q);
|
|
131
|
+
}));
|
|
132
|
+
}, [rows, sorting, query, columns]);
|
|
133
|
+
const allIds = useMemo(() => filtered.map(getRowId), [filtered, getRowId]);
|
|
134
|
+
const pageRows = usePaging ? filtered.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize) : filtered;
|
|
135
|
+
// Master select-all state for the header checkbox: checked when every filtered row is selected,
|
|
136
|
+
// indeterminate when only some are. Clicking selects all filtered rows (across pages/groups), or clears.
|
|
137
|
+
const allSelected = selectable && selection ? sel.allOn(selection, allIds) : false;
|
|
138
|
+
const someSelected = selectable && selection ? !allSelected && allIds.some((id) => selection.has(id)) : false;
|
|
139
|
+
const masterChecked = allSelected ? true : someSelected ? 'indeterminate' : false;
|
|
140
|
+
const doExport = (format) => {
|
|
141
|
+
const blob = formatRows(visibleModelCols, filtered, format);
|
|
142
|
+
onExport?.({ text: blob.text, mime: blob.mime }, `${exportName}.${blob.ext}`);
|
|
143
|
+
};
|
|
144
|
+
// Native HTML5 drag reorder on header cells -> columnOrder.
|
|
145
|
+
const dragId = useRef(null);
|
|
146
|
+
const onDrop = (targetId) => {
|
|
147
|
+
const src = dragId.current;
|
|
148
|
+
dragId.current = null;
|
|
149
|
+
if (!src || src === targetId) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const order = leafCols.map((c) => c.id);
|
|
153
|
+
const from = order.indexOf(src);
|
|
154
|
+
const to = order.indexOf(targetId);
|
|
155
|
+
order.splice(to, 0, order.splice(from, 1)[0]);
|
|
156
|
+
setColumnOrder(order);
|
|
157
|
+
};
|
|
158
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
159
|
+
// The top toolbar is just a collapsed search affordance (an icon that expands into an input, right-aligned)
|
|
160
|
+
// so it costs almost no space; select-all lives in the header-row checkbox; column/export live in the footer.
|
|
161
|
+
const hideableCount = table.getAllLeafColumns().filter((c) => c.getCanHide()).length;
|
|
162
|
+
const showColumnsMenu = Boolean(toolbar?.columns) && hideableCount > 0;
|
|
163
|
+
const searchEnabled = Boolean(toolbar?.search);
|
|
164
|
+
// The search floats up into the right of the section's title row (absolute, negative-top) so it costs the
|
|
165
|
+
// table no vertical space — the data starts at the very top. Sticky offsets therefore ignore it.
|
|
166
|
+
const headTop = 'top-0';
|
|
167
|
+
const groupTop = 'top-9';
|
|
168
|
+
const columnsMenu = showColumnsMenu ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: "text-muted-foreground hover:text-foreground flex cursor-pointer items-center gap-1 outline-none", children: [_jsx(Columns3, { className: "size-3.5" }), " ", t.tableColumns] }), _jsx(DropdownMenuContent, { align: "end", children: table
|
|
169
|
+
.getAllLeafColumns()
|
|
170
|
+
.filter((c) => c.getCanHide())
|
|
171
|
+
.map((c) => {
|
|
172
|
+
const dc = columns.find((x) => x.key === c.id);
|
|
173
|
+
return (_jsx(DropdownMenuCheckboxItem, { checked: c.getIsVisible(), onCheckedChange: (v) => c.toggleVisibility(v), children: dc?.label ?? c.id }, c.id));
|
|
174
|
+
}) })] })) : null;
|
|
175
|
+
const exportMenu = onExport ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: "text-muted-foreground hover:text-foreground flex cursor-pointer items-center gap-1 outline-none", children: [_jsx(Download, { className: "size-3.5" }), " ", t.tableExport] }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuItem, { onSelect: () => doExport('csv'), children: "CSV" }), _jsx(DropdownMenuItem, { onSelect: () => doExport('json'), children: "JSON" })] })] })) : null;
|
|
176
|
+
return (_jsxs("div", { className: "relative space-y-2", children: [searchEnabled ? (_jsx("div", { className: cn('z-30 flex items-center justify-end', searchFloating && 'absolute -top-8 right-0'), children: _jsx(TableSearch, { query: query, onChange: setQuery, placeholder: t.searchPlaceholder }) })) : null, filtered.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-xs", children: emptyLabel ?? t.noRows })) : (_jsxs("table", { className: "w-full text-xs", children: [_jsx("thead", { className: "text-muted-foreground", children: _jsxs("tr", { className: "text-left", children: [expand ? _jsx("th", { className: cn('bg-card sticky z-10 w-6 border-b py-1 pl-0.5', headTop) }) : null, selectable ? (_jsx("th", { className: cn('bg-card sticky z-10 w-6 border-b py-1 pr-3 pl-0.5', headTop), children: _jsx(Checkbox, { checked: masterChecked, onCheckedChange: () => onSelectionChange?.(sel.setGroup(selection, allIds, !allSelected)), "aria-label": t.tableSelectAll(allIds.length) }) })) : null, leafCols.map((c) => {
|
|
177
|
+
const dc = columns.find((x) => x.key === c.id);
|
|
178
|
+
const sorted = c.getIsSorted();
|
|
179
|
+
return (_jsx("th", { draggable: true, onDragStart: () => (dragId.current = c.id), onDragOver: (e) => e.preventDefault(), onDrop: () => onDrop(c.id), className: cn('bg-card sticky z-10 border-b py-1 font-medium select-none last:pr-0', headTop,
|
|
180
|
+
// Right-aligned (numeric) columns get extra trailing room so the figures don't crowd the
|
|
181
|
+
// next column's label/badge.
|
|
182
|
+
dc?.align === 'right' ? 'pr-8 text-right' : 'pr-4', dc?.noWrap && 'whitespace-nowrap', dc?.grow && 'w-full', dc?.minWidth), children: _jsxs("button", { className: "hover:text-foreground inline-flex items-center gap-1", disabled: !c.getCanSort(), onClick: c.getToggleSortingHandler(), children: [dc?.label ?? c.id, c.getCanSort() ? (sorted === 'asc' ? (_jsx(ArrowUp, { className: "size-3" })) : sorted === 'desc' ? (_jsx(ArrowDown, { className: "size-3" })) : (_jsx(ChevronsUpDown, { className: "size-3 opacity-40" }))) : null] }) }, c.id));
|
|
183
|
+
})] }) }), _jsx("tbody", { children: grouped ? (_jsx(Groups, { rows: filtered, grouped: grouped, visibleModelCols: visibleModelCols, selectable: selectable, selection: selection, onSelectionChange: onSelectionChange, getRowId: getRowId, stickyTop: groupTop, expand: expand })) : (pageRows.map((row) => (_jsx(DataRow, { row: row, cols: visibleModelCols, selectable: selectable, checked: selection?.has(getRowId(row)) ?? false, onToggle: () => onSelectionChange?.(sel.toggle(selection, getRowId(row))), expand: expand, rowId: getRowId(row) }, getRowId(row))))) })] })), filtered.length > 0 ? (_jsxs("div", { className: "text-muted-foreground flex items-center gap-2 pt-1 text-xs", children: [_jsxs("div", { className: "flex flex-1 items-center gap-2", children: [toolbarExtra, _jsx("span", { className: "tabular-nums", children: t.tableRowCount(filtered.length) })] }), usePaging ? (_jsxs("div", { className: "flex items-center justify-center gap-2", children: [_jsx("button", { className: "hover:bg-accent disabled:opacity-40 disabled:hover:bg-transparent inline-flex size-7 items-center justify-center rounded-md border", disabled: pageIndex === 0, onClick: () => setPageIndex((i) => i - 1), "aria-label": t.tablePrev, children: _jsx(ChevronLeft, { className: "size-4" }) }), _jsx("span", { className: "border-input inline-flex h-7 min-w-12 items-center justify-center rounded-md border px-2 tabular-nums", children: t.tablePageOf(pageIndex + 1, totalPages) }), _jsx("button", { className: "hover:bg-accent disabled:opacity-40 disabled:hover:bg-transparent inline-flex size-7 items-center justify-center rounded-md border", disabled: pageIndex + 1 >= totalPages, onClick: () => setPageIndex((i) => i + 1), "aria-label": t.tableNext, children: _jsx(ChevronRight, { className: "size-4" }) }), _jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: "hover:bg-accent inline-flex h-7 items-center gap-1 rounded-md border px-2 outline-none", children: [t.tablePerPage(pageSize), " ", _jsx(ChevronDown, { className: "size-3.5 opacity-60" })] }), _jsx(DropdownMenuContent, { align: "center", children: pageSizes.map((n) => (_jsx(DropdownMenuItem, { onSelect: () => {
|
|
184
|
+
setPageSize(n);
|
|
185
|
+
setPageIndex(0);
|
|
186
|
+
}, children: t.tablePerPage(n) }, n))) })] })] })) : null, _jsxs("div", { className: "flex flex-1 items-center justify-end gap-3", children: [columnsMenu, exportMenu] })] })) : null, actionBar ? (_jsx("div", { className: "bg-card sticky bottom-0 flex flex-wrap items-center gap-2 border-t py-3", children: actionBar })) : null] }));
|
|
187
|
+
};
|
|
188
|
+
// Collapsed-by-default search: a magnifier button that expands into an input on click, so the top of the
|
|
189
|
+
// table costs only an icon's width until you actually search. Stays expanded while a query is present;
|
|
190
|
+
// Escape or blurring an empty field collapses it back to the icon.
|
|
191
|
+
const TableSearch = ({ query, onChange, placeholder }) => {
|
|
192
|
+
const [open, setOpen] = useState(false);
|
|
193
|
+
const expanded = open || query.length > 0;
|
|
194
|
+
if (!expanded) {
|
|
195
|
+
return (_jsx("button", { type: "button", onClick: () => setOpen(true), "aria-label": placeholder, className: "text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-7 items-center justify-center rounded-md outline-none", children: _jsx(Search, { className: "size-4" }) }));
|
|
196
|
+
}
|
|
197
|
+
return (_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "text-muted-foreground pointer-events-none absolute top-1/2 left-2 size-3.5 -translate-y-1/2" }), _jsx(Input, { autoFocus: true, value: query, onChange: (e) => onChange(e.target.value), onBlur: () => {
|
|
198
|
+
if (query.length === 0) {
|
|
199
|
+
setOpen(false);
|
|
200
|
+
}
|
|
201
|
+
}, onKeyDown: (e) => {
|
|
202
|
+
if (e.key === 'Escape') {
|
|
203
|
+
onChange('');
|
|
204
|
+
setOpen(false);
|
|
205
|
+
}
|
|
206
|
+
}, placeholder: placeholder, className: "h-7 w-48 pl-7 text-xs" })] }));
|
|
207
|
+
};
|
|
208
|
+
const DataRow = ({ row, cols, selectable, checked, onToggle, expand, rowId }) => {
|
|
209
|
+
const t = useLabels();
|
|
210
|
+
const detail = expand ? expand.detail(row) : null;
|
|
211
|
+
const isOpen = Boolean(expand && rowId != null && expand.isOpen(rowId));
|
|
212
|
+
const colSpan = cols.length + (selectable ? 1 : 0) + (expand ? 1 : 0);
|
|
213
|
+
return (_jsxs(_Fragment, { children: [_jsxs("tr", { className: "border-border/50 hover:bg-muted/30 border-t transition-colors", children: [expand ? (_jsx("td", { className: "w-6 py-1 pl-0.5 align-middle", children: detail != null ? (_jsx("button", { onClick: () => rowId != null && expand.toggle(rowId), "aria-expanded": isOpen, "aria-label": t.toggleRowDetails, className: "text-muted-foreground hover:text-foreground inline-flex", children: isOpen ? _jsx(ChevronDown, { className: "size-3.5" }) : _jsx(ChevronRight, { className: "size-3.5" }) })) : null })) : null, selectable ? (_jsx("td", { className: "py-1 pr-3 pl-0.5 align-middle", children: _jsx(Checkbox, { checked: checked, onCheckedChange: onToggle }) })) : null, cols.map((c) => (_jsx("td", { className: cn('py-1 tabular-nums last:pr-0',
|
|
214
|
+
// Match the header: right-aligned numerics keep extra trailing room from the next column.
|
|
215
|
+
c.align === 'right' ? 'pr-8 text-right' : 'pr-4', c.noWrap && 'whitespace-nowrap',
|
|
216
|
+
// The greedy column takes all leftover width (w-full) without its content forcing the table wider
|
|
217
|
+
// (max-w-0) — its cell truncates to fit. No overflow-clip on the td, so a cell popover still escapes.
|
|
218
|
+
c.grow && 'w-full max-w-0', c.minWidth), children: c.cell ? c.cell(row) : String(row[c.key] ?? '') }, c.key)))] }), isOpen && detail != null ? (_jsx("tr", { className: "border-border/40 border-t", children: _jsx("td", { colSpan: colSpan, className: "bg-muted/20 px-2 py-2", children: detail }) })) : null] }));
|
|
219
|
+
};
|
|
220
|
+
// Partition the (already sorted/filtered) rows into collapsible category sections. Each section header
|
|
221
|
+
// carries a select-all checkbox + count + chevron + optional per-group action. Collapse is local state.
|
|
222
|
+
const Groups = ({ rows, grouped, visibleModelCols, selectable, selection, onSelectionChange, getRowId, stickyTop, expand }) => {
|
|
223
|
+
const [collapsed, setCollapsed] = useState(new Set());
|
|
224
|
+
const order = grouped.order ?? [];
|
|
225
|
+
const keys = [...new Set([...order, ...rows.map(grouped.by)])].filter((k) => rows.some((r) => grouped.by(r) === k));
|
|
226
|
+
const colSpan = visibleModelCols.length + (selectable ? 1 : 0) + (expand ? 1 : 0);
|
|
227
|
+
// A single category adds no information — its header would just be visual noise (and the cramped inline
|
|
228
|
+
// action the user flagged). Render the rows flat; the top toolbar still owns select-all / none.
|
|
229
|
+
if (keys.length <= 1) {
|
|
230
|
+
return (_jsx(_Fragment, { children: rows.map((row) => (_jsx(DataRow, { row: row, cols: visibleModelCols, selectable: selectable, checked: selection?.has(getRowId(row)) ?? false, onToggle: () => onSelectionChange?.(sel.toggle(selection, getRowId(row))), expand: expand, rowId: getRowId(row) }, getRowId(row)))) }));
|
|
231
|
+
}
|
|
232
|
+
return (_jsx(_Fragment, { children: keys.map((key) => {
|
|
233
|
+
const groupRows = rows.filter((r) => grouped.by(r) === key);
|
|
234
|
+
const ids = groupRows.map(getRowId);
|
|
235
|
+
const selectedIds = selection ? ids.filter((id) => selection.has(id)) : [];
|
|
236
|
+
const allOn = selectable && selection ? sel.allOn(selection, ids) : false;
|
|
237
|
+
const isOpen = !collapsed.has(key);
|
|
238
|
+
return (_jsx(GroupSection, { groupKey: key, count: groupRows.length, colSpan: colSpan, stickyTop: stickyTop, isOpen: isOpen, allOn: allOn, selectable: selectable, onToggleOpen: () => setCollapsed((prev) => {
|
|
239
|
+
const next = new Set(prev);
|
|
240
|
+
if (next.has(key)) {
|
|
241
|
+
next.delete(key);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
next.add(key);
|
|
245
|
+
}
|
|
246
|
+
return next;
|
|
247
|
+
}), onToggleAll: () => onSelectionChange?.(sel.setGroup(selection, ids, !allOn)), action: grouped.groupAction?.(key, ids, selectedIds), children: isOpen
|
|
248
|
+
? groupRows.map((row) => (_jsx(DataRow, { row: row, cols: visibleModelCols, selectable: selectable, checked: selection?.has(getRowId(row)) ?? false, onToggle: () => onSelectionChange?.(sel.toggle(selection, getRowId(row))), expand: expand, rowId: getRowId(row) }, getRowId(row))))
|
|
249
|
+
: null }, key));
|
|
250
|
+
}) }));
|
|
251
|
+
};
|
|
252
|
+
const GroupSection = ({ groupKey, count, colSpan, stickyTop, isOpen, allOn, selectable, onToggleOpen, onToggleAll, action, children }) => (_jsxs(_Fragment, { children: [_jsx("tr", { className: cn('bg-muted sticky z-[5] border-y', stickyTop), children: _jsx("td", { colSpan: colSpan, className: "bg-muted py-1.5", children: _jsxs("div", { className: "flex items-center gap-2 pl-0.5 text-xs font-medium", children: [selectable ? (_jsx(Checkbox, { checked: allOn, onCheckedChange: onToggleAll, "aria-label": `Select all ${groupKey}` })) : null, _jsxs("button", { onClick: onToggleOpen, className: "hover:text-foreground inline-flex items-center gap-1.5", "aria-expanded": isOpen, children: [isOpen ? _jsx(ChevronDown, { className: "size-3.5" }) : _jsx(ChevronRight, { className: "size-3.5" }), groupKey, _jsx("span", { className: "text-muted-foreground font-normal", children: count })] }), action ? _jsx("span", { className: "ml-auto", children: action }) : null] }) }) }), children] }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Dialog as DialogPrimitive } from 'radix-ui';
|
|
2
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
3
|
+
export declare const Dialog: import("react").FC<DialogPrimitive.DialogProps>;
|
|
4
|
+
export declare const DialogTrigger: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
5
|
+
export declare const DialogClose: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
export declare const DialogContent: ({ className, children, ...props }: ComponentProps<typeof DialogPrimitive.Content>) => import("react").JSX.Element;
|
|
7
|
+
export declare const DialogHeader: ({ children }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}) => import("react").JSX.Element;
|
|
10
|
+
export declare const DialogTitle: ({ className, ...props }: ComponentProps<typeof DialogPrimitive.Title>) => import("react").JSX.Element;
|
|
11
|
+
export declare const DialogDescription: ({ className, ...props }: ComponentProps<typeof DialogPrimitive.Description>) => import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import { Dialog as DialogPrimitive } from 'radix-ui';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
// A centered modal dialog (radix Dialog) over a dimmed backdrop — for focused, occasional tasks (profile
|
|
6
|
+
// management). Use <Sheet> instead for the side-drawer/inspector pattern. Theme-portable via shadcn tokens.
|
|
7
|
+
export const Dialog = DialogPrimitive.Root;
|
|
8
|
+
export const DialogTrigger = DialogPrimitive.Trigger;
|
|
9
|
+
export const DialogClose = DialogPrimitive.Close;
|
|
10
|
+
export const DialogContent = ({ className, children, ...props }) => (_jsxs(DialogPrimitive.Portal, { children: [_jsx(DialogPrimitive.Overlay, { className: "fixed inset-0 z-50 bg-black/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" }), _jsxs(DialogPrimitive.Content, { className: cn(
|
|
11
|
+
// `calc(100%-2rem)` keeps a gutter so the dialog never bleeds edge-to-edge on a narrow window.
|
|
12
|
+
'bg-background fixed top-1/2 left-1/2 z-50 flex w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 flex-col gap-4 rounded-lg border p-6 shadow-lg', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', className), ...props, children: [children, _jsx(DialogPrimitive.Close, { "aria-label": "Close", className: "text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 absolute top-4 right-4 rounded-sm focus-visible:ring-2 focus-visible:outline-none", children: _jsx(X, { className: "size-4" }) })] })] }));
|
|
13
|
+
export const DialogHeader = ({ children }) => (_jsx("div", { className: "flex flex-col gap-1", children: children }));
|
|
14
|
+
export const DialogTitle = ({ className, ...props }) => (_jsx(DialogPrimitive.Title, { className: cn('text-lg font-semibold tracking-tight', className), ...props }));
|
|
15
|
+
export const DialogDescription = ({ className, ...props }) => (_jsx(DialogPrimitive.Description, { className: cn('text-muted-foreground text-sm', className), ...props }));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
export declare const DropdownMenu: import("react").FC<DropdownMenuPrimitive.DropdownMenuProps>;
|
|
4
|
+
export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<DropdownMenuPrimitive.DropdownMenuTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
5
|
+
export declare const DropdownMenuContent: ({ className, sideOffset, align, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Content>) => import("react").JSX.Element;
|
|
6
|
+
export declare const DropdownMenuItem: ({ className, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Item>) => import("react").JSX.Element;
|
|
7
|
+
export declare const DropdownMenuCheckboxItem: ({ className, children, ...props }: ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) => import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Check } from 'lucide-react';
|
|
3
|
+
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
// Small app-styled dropdown (radix) — outside-click + escape dismiss for free. Used for the DataTable
|
|
6
|
+
// column menu, export menu, and page-size picker. CheckboxItem stays open on toggle (multi-select menus).
|
|
7
|
+
export const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
8
|
+
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
9
|
+
export const DropdownMenuContent = ({ className, sideOffset = 4, align = 'end', ...props }) => (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { sideOffset: sideOffset, align: align, className: cn('bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md', 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0', className), ...props }) }));
|
|
10
|
+
export const DropdownMenuItem = ({ className, ...props }) => (_jsx(DropdownMenuPrimitive.Item, { className: cn('focus:bg-accent flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none select-none', className), ...props }));
|
|
11
|
+
export const DropdownMenuCheckboxItem = ({ className, children, ...props }) => (_jsxs(DropdownMenuPrimitive.CheckboxItem
|
|
12
|
+
// Keep the menu open while toggling several columns in a row.
|
|
13
|
+
, {
|
|
14
|
+
// Keep the menu open while toggling several columns in a row.
|
|
15
|
+
onSelect: (e) => e.preventDefault(), className: cn('focus:bg-accent relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-6 text-xs outline-none select-none', className), ...props, children: [_jsx("span", { className: "absolute left-1 flex size-4 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Check, { className: "size-3.5" }) }) }), children] }));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as echarts from 'echarts';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
// Minimal ECharts wrapper: init once, setOption on change, resize + dispose. Canvas can't ride the CSS
|
|
5
|
+
// cascade, so callers feed it concrete token colors from useEchartsTheme().
|
|
6
|
+
export const EChart = ({ option, height = 200 }) => {
|
|
7
|
+
const containerRef = useRef(null);
|
|
8
|
+
const chartRef = useRef(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!containerRef.current) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const chart = echarts.init(containerRef.current, null, { renderer: 'canvas' });
|
|
14
|
+
chartRef.current = chart;
|
|
15
|
+
const observer = new ResizeObserver(() => chart.resize());
|
|
16
|
+
observer.observe(containerRef.current);
|
|
17
|
+
return () => {
|
|
18
|
+
observer.disconnect();
|
|
19
|
+
chart.dispose();
|
|
20
|
+
chartRef.current = null;
|
|
21
|
+
};
|
|
22
|
+
}, []);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
chartRef.current?.setOption(option, true);
|
|
25
|
+
}, [option]);
|
|
26
|
+
return _jsx("div", { ref: containerRef, style: { width: '100%', height } });
|
|
27
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
const Input = ({ className, type, ...props }) => (_jsx("input", { type: type, "data-slot": "input", className: cn('file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', className), ...props }));
|
|
5
|
+
export { Input };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Label as LabelPrimitive } from 'radix-ui';
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
// A form label (radix) — fades + blocks pointer events when its peer/group is disabled.
|
|
5
|
+
export const Label = ({ className, ...props }) => (_jsx(LabelPrimitive.Root, { "data-slot": "label", className: cn('flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50', className), ...props }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../lib/utils.js';
|
|
3
|
+
// A thin horizontal progress bar. `value` is a 0..1 fraction (clamped). Theme-portable: track is `bg-muted`,
|
|
4
|
+
// fill is `bg-primary`, so it takes the host's palette. Width-driven, no animation.
|
|
5
|
+
export const Progress = ({ value, className }) => {
|
|
6
|
+
const pct = Math.max(0, Math.min(1, value)) * 100;
|
|
7
|
+
return (_jsx("div", { role: "progressbar", "aria-valuenow": Math.round(pct), "aria-valuemin": 0, "aria-valuemax": 100, className: cn('bg-muted h-1.5 w-full overflow-hidden rounded-full', className), children: _jsx("div", { className: "bg-primary h-full rounded-full", style: { width: `${pct}%` } }) }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface SelectOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const Select: ({ id, value, options, onValueChange, placeholder }: {
|
|
6
|
+
id?: string;
|
|
7
|
+
value: string;
|
|
8
|
+
options: SelectOption[];
|
|
9
|
+
onValueChange: (value: string) => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
}) => import("react").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Check, ChevronDown } from 'lucide-react';
|
|
3
|
+
import { Select as SelectPrimitive } from 'radix-ui';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
export const Select = ({ id, value, options, onValueChange, placeholder }) => (_jsxs(SelectPrimitive.Root, { value: value, onValueChange: onValueChange, children: [_jsxs(SelectPrimitive.Trigger, { id: id, className: cn('border-input dark:bg-input/30 flex h-8 w-full items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-1 text-sm whitespace-nowrap shadow-xs outline-none', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] data-[placeholder]:text-muted-foreground'), children: [_jsx(SelectPrimitive.Value, { placeholder: placeholder }), _jsx(SelectPrimitive.Icon, { children: _jsx(ChevronDown, { className: "size-4 opacity-60" }) })] }), _jsx(SelectPrimitive.Portal, { children: _jsx(SelectPrimitive.Content, { position: "popper", sideOffset: 4, className: cn('bg-popover text-popover-foreground relative z-50 max-h-72 min-w-[var(--radix-select-trigger-width)] overflow-hidden rounded-md border shadow-md', 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0'), children: _jsx(SelectPrimitive.Viewport, { className: "p-1", children: options.map((o) => (_jsxs(SelectPrimitive.Item, { value: o.value, className: cn('focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none select-none'), children: [_jsx(SelectPrimitive.ItemText, { children: o.label }), _jsx(SelectPrimitive.ItemIndicator, { className: "absolute right-2 flex items-center", children: _jsx(Check, { className: "size-4" }) })] }, o.value))) }) }) })] }));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Separator as SeparatorPrimitive } from 'radix-ui';
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
// A hairline divider (radix). Horizontal by default; pass orientation="vertical" inside a flex row.
|
|
5
|
+
export const Separator = ({ className, orientation = 'horizontal', decorative = true, ...props }) => (_jsx(SeparatorPrimitive.Root, { "data-slot": "separator", decorative: decorative, orientation: orientation, className: cn('bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px', className), ...props }));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type ServiceIconResolver = (service: {
|
|
3
|
+
id?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
}, size: number) => ReactNode;
|
|
7
|
+
export declare const ServiceIconProvider: ({ resolve, children }: {
|
|
8
|
+
resolve?: ServiceIconResolver;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}) => import("react").JSX.Element;
|
|
11
|
+
export declare const readableOn: (hex: string) => string;
|
|
12
|
+
export declare const ServiceIcon: ({ id, icon, name, color, size, className }: {
|
|
13
|
+
id?: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
color?: string;
|
|
17
|
+
size?: number;
|
|
18
|
+
className?: string;
|
|
19
|
+
}) => import("react").JSX.Element;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
const ServiceIconContext = createContext(undefined);
|
|
5
|
+
// Wrap a tree to override how `<ServiceIcon>` draws: every icon consults `resolve` first, so an embed can map
|
|
6
|
+
// plugin ids to its own logos without threading a prop through the shell. No provider → the monogram default.
|
|
7
|
+
export const ServiceIconProvider = ({ resolve, children }) => (_jsx(ServiceIconContext.Provider, { value: resolve, children: children }));
|
|
8
|
+
// Pick a legible letter color (near-black or white) for a solid brand-color tile, by the brand color's
|
|
9
|
+
// luminance — so the monogram reads on any accent, in either theme. Falls back to white for non-hex colors.
|
|
10
|
+
export const readableOn = (hex) => {
|
|
11
|
+
const m = hex.replace('#', '');
|
|
12
|
+
const full = m.length === 3 ? m.replace(/./g, (c) => c + c) : m;
|
|
13
|
+
if (full.length !== 6) {
|
|
14
|
+
return '#fff';
|
|
15
|
+
}
|
|
16
|
+
const r = parseInt(full.slice(0, 2), 16) / 255;
|
|
17
|
+
const g = parseInt(full.slice(2, 4), 16) / 255;
|
|
18
|
+
const b = parseInt(full.slice(4, 6), 16) / 255;
|
|
19
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
20
|
+
return luminance > 0.62 ? '#15181c' : '#fff';
|
|
21
|
+
};
|
|
22
|
+
// A service's brand mark. We don't ship third-party logos — instead a service renders a brand-colored
|
|
23
|
+
// letter monogram (its name's initial on a solid tile of `meta.color`, with an auto-contrast glyph). A host
|
|
24
|
+
// can override per service via `ServiceIconProvider` (keyed on `id`), or a plugin can supply its OWN `icon`
|
|
25
|
+
// data-URI; absent both, the monogram is the default. Pure + prop-driven, so it renders identically in the
|
|
26
|
+
// app and a snapshot embed. The box is always `size`×`size` so rows stay aligned regardless of which branch
|
|
27
|
+
// renders.
|
|
28
|
+
export const ServiceIcon = ({ id, icon, name, color, size = 16, className }) => {
|
|
29
|
+
const resolved = useContext(ServiceIconContext)?.({ id, name, color }, size);
|
|
30
|
+
if (resolved != null && resolved !== false) {
|
|
31
|
+
return (_jsx("span", { "aria-hidden": "true", className: cn('inline-flex shrink-0 items-center justify-center overflow-hidden', className), style: { width: size, height: size }, children: resolved }));
|
|
32
|
+
}
|
|
33
|
+
if (icon) {
|
|
34
|
+
return (_jsx("img", { src: icon, alt: "", width: size, height: size, className: cn('shrink-0 rounded-sm object-contain', className) }));
|
|
35
|
+
}
|
|
36
|
+
const letter = name?.trim().charAt(0).toUpperCase();
|
|
37
|
+
if (letter) {
|
|
38
|
+
return (_jsx("span", { "aria-hidden": "true", className: cn('inline-flex shrink-0 items-center justify-center rounded-[5px] font-semibold', className), style: {
|
|
39
|
+
width: size,
|
|
40
|
+
height: size,
|
|
41
|
+
fontSize: Math.round(size * 0.62),
|
|
42
|
+
lineHeight: 1,
|
|
43
|
+
backgroundColor: color ?? 'var(--muted)',
|
|
44
|
+
color: color ? readableOn(color) : 'var(--muted-foreground)'
|
|
45
|
+
}, children: letter }));
|
|
46
|
+
}
|
|
47
|
+
const dot = Math.max(6, Math.round(size / 2));
|
|
48
|
+
return (_jsx("span", { className: cn('inline-flex shrink-0 items-center justify-center', className), style: { width: size, height: size }, children: _jsx("span", { className: "rounded-full", style: { width: dot, height: dot, backgroundColor: color ?? 'var(--border)' } }) }));
|
|
49
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Dialog } from 'radix-ui';
|
|
2
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
3
|
+
export declare const Sheet: import("react").FC<Dialog.DialogProps>;
|
|
4
|
+
export declare const SheetTrigger: import("react").ForwardRefExoticComponent<Dialog.DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
5
|
+
export declare const SheetClose: import("react").ForwardRefExoticComponent<Dialog.DialogCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
export declare const SheetContent: ({ className, children, ...props }: ComponentProps<typeof Dialog.Content>) => import("react").JSX.Element;
|
|
7
|
+
export declare const SheetHeader: ({ children }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}) => import("react").JSX.Element;
|
|
10
|
+
export declare const SheetTitle: ({ className, ...props }: ComponentProps<typeof Dialog.Title>) => import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import { Dialog } from 'radix-ui';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
// A right-side slide-over (radix Dialog). Used for the connection drawer; theme-portable via shadcn tokens.
|
|
6
|
+
export const Sheet = Dialog.Root;
|
|
7
|
+
export const SheetTrigger = Dialog.Trigger;
|
|
8
|
+
export const SheetClose = Dialog.Close;
|
|
9
|
+
export const SheetContent = ({ className, children, ...props }) => (_jsxs(Dialog.Portal, { children: [_jsx(Dialog.Overlay, { className: "fixed inset-0 z-50 bg-black/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" }), _jsxs(Dialog.Content, { className: cn('bg-background fixed inset-y-0 right-0 z-50 flex w-full max-w-md flex-col gap-4 overflow-y-auto border-l p-6 shadow-lg', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right', className), ...props, children: [children, _jsx(Dialog.Close, { className: "text-muted-foreground hover:text-foreground absolute top-4 right-4", children: _jsx(X, { className: "size-4" }) })] })] }));
|
|
10
|
+
export const SheetHeader = ({ children }) => (_jsx("div", { className: "flex flex-col gap-1", children: children }));
|
|
11
|
+
export const SheetTitle = ({ className, ...props }) => (_jsx(Dialog.Title, { className: cn('text-lg font-semibold tracking-tight', className), ...props }));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../lib/utils.js';
|
|
3
|
+
// A shimmering placeholder block. Compose several to mirror the shape of the content that's loading.
|
|
4
|
+
export const Skeleton = ({ className, ...props }) => (_jsx("div", { "data-slot": "skeleton", className: cn('bg-muted/60 animate-pulse rounded-md', className), ...props }));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../lib/utils.js';
|
|
3
|
+
// A compact inline trend line for a table cell — a row's per-day series at a glance. Pure SVG (no chart
|
|
4
|
+
// engine), so it's cheap to render once per row. Stroke is `currentColor`, so the line takes the cell's text
|
|
5
|
+
// color (set via className) and stays theme-portable. A series shorter than two finite points renders nothing.
|
|
6
|
+
export const Sparkline = ({ values, width = 72, height = 20, strokeWidth = 1.5, className }) => {
|
|
7
|
+
const points = values.filter((v) => Number.isFinite(v));
|
|
8
|
+
if (points.length < 2) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const min = Math.min(...points);
|
|
12
|
+
const max = Math.max(...points);
|
|
13
|
+
const span = max - min;
|
|
14
|
+
const pad = strokeWidth;
|
|
15
|
+
const usable = height - pad * 2;
|
|
16
|
+
const stepX = width / (points.length - 1);
|
|
17
|
+
// A flat series (all equal) pins to the vertical middle so it reads as a baseline, not a spike.
|
|
18
|
+
const y = (v) => (span === 0 ? height / 2 : pad + (1 - (v - min) / span) * usable);
|
|
19
|
+
const d = points.map((v, i) => `${i === 0 ? 'M' : 'L'}${(i * stepX).toFixed(2)} ${y(v).toFixed(2)}`).join(' ');
|
|
20
|
+
const last = points[points.length - 1];
|
|
21
|
+
return (_jsxs("svg", { width: width, height: height, viewBox: `0 0 ${width} ${height}`, fill: "none", className: cn('text-muted-foreground inline-block overflow-visible align-middle', className), "aria-hidden": true, children: [_jsx("path", { d: d, stroke: "currentColor", strokeWidth: strokeWidth, strokeLinejoin: "round", strokeLinecap: "round" }), _jsx("circle", { cx: width, cy: y(last), r: strokeWidth, fill: "currentColor" })] }));
|
|
22
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Switch as SwitchPrimitive } from 'radix-ui';
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
// On/off toggle (radix). Semantic tokens only, so it inherits the host theme.
|
|
5
|
+
export const Switch = ({ className, ...props }) => (_jsx(SwitchPrimitive.Root, { "data-slot": "switch", className: cn('focus-visible:ring-ring/50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', className), ...props, children: _jsx(SwitchPrimitive.Thumb, { className: "bg-background pointer-events-none block size-4 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0.5" }) }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Tabs as TabsPrimitive } from 'radix-ui';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
export declare const Tabs: ({ className, ...props }: ComponentProps<typeof TabsPrimitive.Root>) => import("react").JSX.Element;
|
|
4
|
+
export declare const TabsList: ({ className, variant, ...props }: ComponentProps<typeof TabsPrimitive.List> & {
|
|
5
|
+
variant?: 'pill' | 'line';
|
|
6
|
+
}) => import("react").JSX.Element;
|
|
7
|
+
export declare const TabsTrigger: ({ className, ...props }: ComponentProps<typeof TabsPrimitive.Trigger>) => import("react").JSX.Element;
|
|
8
|
+
export declare const TabsContent: ({ className, ...props }: ComponentProps<typeof TabsPrimitive.Content>) => import("react").JSX.Element;
|