@olympusoss/canvas 2.20.2 → 4.0.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/package.json +41 -177
- package/src/cn.ts +3 -0
- package/src/index.ts +12 -603
- package/src/theme.ts +41 -0
- package/src/tokens.ts +11 -0
- package/styles/base.css +17 -0
- package/styles/canvas.css +69 -52
- package/styles/components/alert.css +66 -0
- package/styles/components/app-shell.css +46 -0
- package/styles/components/avatar.css +15 -0
- package/styles/components/badge.css +83 -0
- package/styles/components/breadcrumb.css +35 -0
- package/styles/components/button-group.css +23 -0
- package/styles/components/button.css +107 -0
- package/styles/components/calendar.css +73 -0
- package/styles/components/card.css +58 -0
- package/styles/components/checkbox.css +55 -0
- package/styles/components/code-block.css +18 -0
- package/styles/components/combobox.css +75 -0
- package/styles/components/command.css +94 -0
- package/styles/components/data-table.css +142 -0
- package/styles/components/dialog.css +72 -0
- package/styles/components/dropdown.css +54 -0
- package/styles/components/empty-state.css +17 -0
- package/styles/components/field.css +27 -0
- package/styles/components/filter-panel.css +58 -0
- package/styles/components/form.css +27 -0
- package/styles/components/icon.css +8 -0
- package/styles/components/input-group.css +45 -0
- package/styles/components/input.css +56 -0
- package/styles/components/kbd.css +15 -0
- package/styles/components/page-header.css +52 -0
- package/styles/components/pagination.css +48 -0
- package/styles/components/popover.css +14 -0
- package/styles/components/radio.css +28 -0
- package/styles/components/row-menu.css +69 -0
- package/styles/components/section-card.css +49 -0
- package/styles/components/select.css +57 -0
- package/styles/components/separator.css +32 -0
- package/styles/components/sheet.css +70 -0
- package/styles/components/sidebar.css +146 -0
- package/styles/components/skeleton.css +32 -0
- package/styles/components/spinner.css +26 -0
- package/styles/components/stat-card.css +71 -0
- package/styles/components/stepper.css +63 -0
- package/styles/components/switch.css +45 -0
- package/styles/components/tabs.css +40 -0
- package/styles/components/textarea.css +31 -0
- package/styles/components/toast.css +95 -0
- package/styles/components/tooltip.css +53 -0
- package/styles/components/topbar.css +24 -0
- package/styles/components/typography.css +105 -0
- package/styles/patterns/backdrops.css +35 -0
- package/styles/patterns/density.css +66 -0
- package/styles/patterns/focus.css +38 -0
- package/styles/patterns/glass.css +85 -0
- package/styles/patterns/high-contrast.css +70 -0
- package/styles/patterns/reduced-motion.css +12 -0
- package/styles/patterns/scrollbar.css +10 -0
- package/styles/reset.css +89 -0
- package/styles/tokens/colors.css +106 -0
- package/styles/tokens/motion.css +33 -0
- package/styles/tokens/radius.css +10 -0
- package/styles/tokens/shadows.css +35 -0
- package/styles/tokens/spacing.css +19 -0
- package/styles/tokens/typography.css +6 -0
- package/styles/tokens/z-index.css +12 -0
- package/tsconfig.json +20 -21
- package/README.md +0 -60
- package/src/components/atoms/README.md +0 -11
- package/src/components/atoms/aspect-ratio.tsx +0 -32
- package/src/components/atoms/avatar.tsx +0 -98
- package/src/components/atoms/badge.tsx +0 -44
- package/src/components/atoms/brand-mark.tsx +0 -74
- package/src/components/atoms/button.tsx +0 -105
- package/src/components/atoms/checkbox.tsx +0 -63
- package/src/components/atoms/flex-box.tsx +0 -105
- package/src/components/atoms/icon.tsx +0 -34
- package/src/components/atoms/input.tsx +0 -92
- package/src/components/atoms/label.tsx +0 -41
- package/src/components/atoms/logo.tsx +0 -89
- package/src/components/atoms/progress.tsx +0 -55
- package/src/components/atoms/radio-group.tsx +0 -122
- package/src/components/atoms/scroll-area.tsx +0 -106
- package/src/components/atoms/section.tsx +0 -48
- package/src/components/atoms/separator.tsx +0 -45
- package/src/components/atoms/skeleton.tsx +0 -17
- package/src/components/atoms/slider.tsx +0 -93
- package/src/components/atoms/spinner.tsx +0 -47
- package/src/components/atoms/switch.tsx +0 -60
- package/src/components/atoms/textarea.tsx +0 -78
- package/src/components/atoms/toggle.tsx +0 -80
- package/src/components/charts/activity-heatmap.tsx +0 -186
- package/src/components/charts/axes.tsx +0 -21
- package/src/components/charts/chart-container.tsx +0 -254
- package/src/components/charts/chart-legend.tsx +0 -67
- package/src/components/charts/chart-tooltip.tsx +0 -161
- package/src/components/charts/chart-types.tsx +0 -49
- package/src/components/charts/containers.tsx +0 -11
- package/src/components/charts/data.tsx +0 -16
- package/src/components/charts/details.tsx +0 -25
- package/src/components/charts/dot-pulse.tsx +0 -61
- package/src/components/charts/gauge.tsx +0 -106
- package/src/components/charts/grids.tsx +0 -8
- package/src/components/charts/index.ts +0 -62
- package/src/components/charts/labeled-bar-list.tsx +0 -85
- package/src/components/charts/metric-breakdown.tsx +0 -316
- package/src/components/charts/references.tsx +0 -8
- package/src/components/charts/service-health-list.tsx +0 -85
- package/src/components/charts/sparkline-area.tsx +0 -80
- package/src/components/charts/sparkline.tsx +0 -52
- package/src/components/charts/stacked-bar.tsx +0 -104
- package/src/components/charts/text.tsx +0 -10
- package/src/components/charts/world-heat-map-inner.tsx +0 -317
- package/src/components/charts/world-heat-map.tsx +0 -184
- package/src/components/molecules/README.md +0 -12
- package/src/components/molecules/action-bar.tsx +0 -73
- package/src/components/molecules/activity-item.tsx +0 -74
- package/src/components/molecules/alert.tsx +0 -86
- package/src/components/molecules/animated-background.tsx +0 -92
- package/src/components/molecules/auth-shell.tsx +0 -95
- package/src/components/molecules/brand-lockup.tsx +0 -48
- package/src/components/molecules/breadcrumb.tsx +0 -157
- package/src/components/molecules/button-group.tsx +0 -104
- package/src/components/molecules/calendar.tsx +0 -217
- package/src/components/molecules/card.tsx +0 -102
- package/src/components/molecules/client-brand.tsx +0 -95
- package/src/components/molecules/code-block.tsx +0 -86
- package/src/components/molecules/countdown-button.tsx +0 -92
- package/src/components/molecules/empty-state.tsx +0 -56
- package/src/components/molecules/error-state.tsx +0 -42
- package/src/components/molecules/field-display.tsx +0 -35
- package/src/components/molecules/input-otp.tsx +0 -74
- package/src/components/molecules/launcher-card.tsx +0 -152
- package/src/components/molecules/loading-state.tsx +0 -36
- package/src/components/molecules/notification-item.tsx +0 -67
- package/src/components/molecules/notification-list.tsx +0 -45
- package/src/components/molecules/number-badge.tsx +0 -53
- package/src/components/molecules/or-separator.tsx +0 -38
- package/src/components/molecules/page-header.tsx +0 -88
- package/src/components/molecules/page-tabs.tsx +0 -94
- package/src/components/molecules/pagination.tsx +0 -150
- package/src/components/molecules/password-input.tsx +0 -83
- package/src/components/molecules/password-strength-meter.tsx +0 -104
- package/src/components/molecules/phone-input.tsx +0 -200
- package/src/components/molecules/search-bar.tsx +0 -64
- package/src/components/molecules/secret-field.tsx +0 -158
- package/src/components/molecules/section-card.tsx +0 -91
- package/src/components/molecules/social-buttons.tsx +0 -165
- package/src/components/molecules/stat-card.tsx +0 -100
- package/src/components/molecules/status-badge.tsx +0 -42
- package/src/components/molecules/stepper.tsx +0 -96
- package/src/components/molecules/table.tsx +0 -157
- package/src/components/molecules/terminal.tsx +0 -74
- package/src/components/molecules/toggle-group.tsx +0 -145
- package/src/components/molecules/tooltip.tsx +0 -155
- package/src/components/molecules/user-avatar-chip.tsx +0 -71
- package/src/components/organisms/README.md +0 -14
- package/src/components/organisms/accordion.tsx +0 -154
- package/src/components/organisms/alert-dialog.tsx +0 -277
- package/src/components/organisms/carousel.tsx +0 -244
- package/src/components/organisms/collapsible.tsx +0 -69
- package/src/components/organisms/command.tsx +0 -144
- package/src/components/organisms/context-menu.tsx +0 -339
- package/src/components/organisms/dashboard-grid.tsx +0 -369
- package/src/components/organisms/data-table.tsx +0 -330
- package/src/components/organisms/dialog.tsx +0 -312
- package/src/components/organisms/drawer.tsx +0 -123
- package/src/components/organisms/dropdown-menu.tsx +0 -440
- package/src/components/organisms/editors/code-editor.tsx +0 -144
- package/src/components/organisms/editors/index.ts +0 -4
- package/src/components/organisms/editors/markdown-editor.tsx +0 -153
- package/src/components/organisms/editors/markdown-renderer.ts +0 -27
- package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
- package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
- package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
- package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
- package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
- package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
- package/src/components/organisms/error-boundary.tsx +0 -61
- package/src/components/organisms/form.tsx +0 -174
- package/src/components/organisms/hover-card.tsx +0 -115
- package/src/components/organisms/menubar.tsx +0 -498
- package/src/components/organisms/navbar.tsx +0 -104
- package/src/components/organisms/navigation-menu.tsx +0 -235
- package/src/components/organisms/popover.tsx +0 -149
- package/src/components/organisms/resizable.tsx +0 -58
- package/src/components/organisms/schema-form.tsx +0 -232
- package/src/components/organisms/select.tsx +0 -309
- package/src/components/organisms/sheet.tsx +0 -265
- package/src/components/organisms/sidebar.tsx +0 -1040
- package/src/components/organisms/sonner.tsx +0 -96
- package/src/components/organisms/tabs.tsx +0 -133
- package/src/components/organisms/theme-provider.tsx +0 -101
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/lib/portal-container.tsx +0 -35
- package/src/lib/utils.ts +0 -6
- package/src/native.ts +0 -23
- package/src/tokens/colors.ts +0 -91
- package/src/tokens/index.ts +0 -3
- package/src/tokens/spacing.ts +0 -55
- package/src/tokens/typography.ts +0 -27
- package/styles/dashboard-grid.css +0 -47
- package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
- package/styles/glass.css +0 -175
- package/styles/leaflet.css +0 -13
- package/styles/tokens.css +0 -317
- package/tailwind.config.ts +0 -70
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { GripVertical, Trash2 } from "lucide-react";
|
|
4
|
-
import * as React from "react";
|
|
5
|
-
import type { Layout, Layouts } from "react-grid-layout";
|
|
6
|
-
import GridLayout from "react-grid-layout";
|
|
7
|
-
|
|
8
|
-
import { cn } from "../../lib/utils";
|
|
9
|
-
|
|
10
|
-
// We bypass `WidthProvider` and feed `width` manually via our own
|
|
11
|
-
// ResizeObserver. WidthProvider defaults its initial width to 1280px and
|
|
12
|
-
// updates only on `window.resize`, which never fires for grids inside
|
|
13
|
-
// iframes, modals, or any other container that resizes independently of the
|
|
14
|
-
// window. The ResizeObserver below fires correctly in all those contexts.
|
|
15
|
-
const ResponsiveGridLayout = GridLayout.Responsive;
|
|
16
|
-
|
|
17
|
-
/* ---------- Types ---------- */
|
|
18
|
-
|
|
19
|
-
export interface DashboardItem {
|
|
20
|
-
/** Stable widget id. Matches react-grid-layout's `i` field. */
|
|
21
|
-
i: string;
|
|
22
|
-
/** Column position (0-indexed). */
|
|
23
|
-
x: number;
|
|
24
|
-
/** Row position (0-indexed). */
|
|
25
|
-
y: number;
|
|
26
|
-
/** Width in columns. */
|
|
27
|
-
w: number;
|
|
28
|
-
/** Height in row units (`rowHeight` pixels each). */
|
|
29
|
-
h: number;
|
|
30
|
-
/** When true, this item never moves and can't be resized. */
|
|
31
|
-
static?: boolean;
|
|
32
|
-
/** Minimum width in columns. */
|
|
33
|
-
minW?: number;
|
|
34
|
-
/** Minimum height in row units. */
|
|
35
|
-
minH?: number;
|
|
36
|
-
/** Maximum width in columns. */
|
|
37
|
-
maxW?: number;
|
|
38
|
-
/** Maximum height in row units. */
|
|
39
|
-
maxH?: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type DashboardGridBreakpoint = "lg" | "md" | "sm" | "xs" | "xxs";
|
|
43
|
-
|
|
44
|
-
export interface DashboardGridProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
45
|
-
/** Controlled item list. The component is purely controlled — pass changes to `onItemsChange`. */
|
|
46
|
-
items: DashboardItem[];
|
|
47
|
-
/** Fired with the next item snapshot whenever a drag or resize completes. */
|
|
48
|
-
onItemsChange?: (next: DashboardItem[]) => void;
|
|
49
|
-
/** Render the inner widget for an item. The wrapper is owned by the grid. */
|
|
50
|
-
renderItem: (item: DashboardItem) => React.ReactNode;
|
|
51
|
-
/** When true, drag handles + resize edges are active. Default `false`. */
|
|
52
|
-
editing?: boolean;
|
|
53
|
-
/** Pixel height of one row unit. Default `80`. */
|
|
54
|
-
rowHeight?: number;
|
|
55
|
-
/** Column count per breakpoint. Default `{ lg:12, md:8, sm:4, xs:2, xxs:1 }`. */
|
|
56
|
-
cols?: Partial<Record<DashboardGridBreakpoint, number>>;
|
|
57
|
-
/** Pixel breakpoint widths. Default react-grid-layout's standard set. */
|
|
58
|
-
breakpoints?: Partial<Record<DashboardGridBreakpoint, number>>;
|
|
59
|
-
/** Gutter between items in pixels (`[x, y]`). Default `[16, 16]` — kept even on both axes
|
|
60
|
-
* so rows and columns share the same spacing. */
|
|
61
|
-
margin?: [number, number];
|
|
62
|
-
/** Rendered when `items.length === 0`. */
|
|
63
|
-
emptyState?: React.ReactNode;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const DEFAULT_COLS: Record<DashboardGridBreakpoint, number> = {
|
|
67
|
-
lg: 12,
|
|
68
|
-
md: 8,
|
|
69
|
-
sm: 4,
|
|
70
|
-
xs: 2,
|
|
71
|
-
xxs: 1,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const DEFAULT_BREAKPOINTS: Record<DashboardGridBreakpoint, number> = {
|
|
75
|
-
lg: 1200,
|
|
76
|
-
md: 996,
|
|
77
|
-
sm: 768,
|
|
78
|
-
xs: 480,
|
|
79
|
-
xxs: 0,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const ITEM_KEYS: Array<keyof DashboardItem> = [
|
|
83
|
-
"i",
|
|
84
|
-
"x",
|
|
85
|
-
"y",
|
|
86
|
-
"w",
|
|
87
|
-
"h",
|
|
88
|
-
"static",
|
|
89
|
-
"minW",
|
|
90
|
-
"minH",
|
|
91
|
-
"maxW",
|
|
92
|
-
"maxH",
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
/** @internal Exported for unit testing only. */
|
|
96
|
-
export function toLibLayout(items: DashboardItem[]): Layout[] {
|
|
97
|
-
return items.map((item) => {
|
|
98
|
-
const out: Layout = { i: item.i, x: item.x, y: item.y, w: item.w, h: item.h };
|
|
99
|
-
if (item.static !== undefined) out.static = item.static;
|
|
100
|
-
if (item.minW !== undefined) out.minW = item.minW;
|
|
101
|
-
if (item.minH !== undefined) out.minH = item.minH;
|
|
102
|
-
if (item.maxW !== undefined) out.maxW = item.maxW;
|
|
103
|
-
if (item.maxH !== undefined) out.maxH = item.maxH;
|
|
104
|
-
return out;
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** @internal Exported for unit testing only. Returns true when the merged
|
|
109
|
-
* snapshot's positional fields match the prev input — used to skip spurious
|
|
110
|
-
* onItemsChange calls react-grid-layout fires on initial mount. */
|
|
111
|
-
export function isSameLayout(prev: DashboardItem[], next: DashboardItem[]): boolean {
|
|
112
|
-
if (next.length !== prev.length) return false;
|
|
113
|
-
return next.every((n, idx) => {
|
|
114
|
-
const p = prev[idx];
|
|
115
|
-
return n.i === p.i && n.x === p.x && n.y === p.y && n.w === p.w && n.h === p.h;
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** @internal Exported for unit testing only. Scales a single item's `x` and `w`
|
|
120
|
-
* proportionally from one breakpoint's column count to another. Preserves `y`,
|
|
121
|
-
* `h`, and all constraint fields untouched.
|
|
122
|
-
*
|
|
123
|
-
* Rounding rules:
|
|
124
|
-
* - `w`: `Math.round(w * ratio)`, clamped to `[1, toCols]`. Items never become
|
|
125
|
-
* zero-width and never exceed the target column count.
|
|
126
|
-
* - `x`: `Math.floor(x * ratio)`, clamped to `[0, toCols - 1]`. Items always
|
|
127
|
-
* start at a valid column. RGL's vertical compactor handles any wraps that
|
|
128
|
-
* result.
|
|
129
|
-
*/
|
|
130
|
-
export function scaleItem(item: DashboardItem, fromCols: number, toCols: number): DashboardItem {
|
|
131
|
-
if (fromCols === toCols) return item;
|
|
132
|
-
const ratio = toCols / fromCols;
|
|
133
|
-
return {
|
|
134
|
-
...item,
|
|
135
|
-
x: Math.min(Math.max(0, toCols - 1), Math.max(0, Math.floor(item.x * ratio))),
|
|
136
|
-
w: Math.max(1, Math.min(toCols, Math.round(item.w * ratio))),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** @internal Exported for unit testing only. */
|
|
141
|
-
export function mergeLibLayout(prev: DashboardItem[], next: Layout[]): DashboardItem[] {
|
|
142
|
-
const byKey = new Map(prev.map((p) => [p.i, p]));
|
|
143
|
-
return next.map((n) => {
|
|
144
|
-
const base = byKey.get(n.i) ?? {};
|
|
145
|
-
const merged: DashboardItem = {
|
|
146
|
-
...base,
|
|
147
|
-
i: n.i,
|
|
148
|
-
x: n.x,
|
|
149
|
-
y: n.y,
|
|
150
|
-
w: n.w,
|
|
151
|
-
h: n.h,
|
|
152
|
-
};
|
|
153
|
-
// Preserve constraints + static flag from the lib's snapshot.
|
|
154
|
-
if (n.static !== undefined) merged.static = n.static;
|
|
155
|
-
if (n.minW !== undefined) merged.minW = n.minW;
|
|
156
|
-
if (n.minH !== undefined) merged.minH = n.minH;
|
|
157
|
-
if (n.maxW !== undefined) merged.maxW = n.maxW;
|
|
158
|
-
if (n.maxH !== undefined) merged.maxH = n.maxH;
|
|
159
|
-
// Drop fields the lib zeroed out compared to the original.
|
|
160
|
-
for (const key of ITEM_KEYS) {
|
|
161
|
-
if (key in merged && merged[key] === undefined) {
|
|
162
|
-
delete merged[key];
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return merged;
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Drag-to-reorder, drag-to-resize widget grid backed by `react-grid-layout`.
|
|
171
|
-
* Fully controlled: pass `items` + `onItemsChange`. Toggle `editing` to gate
|
|
172
|
-
* drag/resize affordances behind a customize-mode UX. The 12-col responsive
|
|
173
|
-
* grid auto-packs collisions and adapts column count to viewport via the
|
|
174
|
-
* `breakpoints` map.
|
|
175
|
-
*
|
|
176
|
-
* `items` is consumed in **lg-coords** (the `cols.lg` column count, default 12).
|
|
177
|
-
* Per-breakpoint layouts are derived automatically by scaling each item's `x`
|
|
178
|
-
* and `w` proportionally to the target breakpoint's column count via
|
|
179
|
-
* `scaleItem`. This is the documented `react-grid-layout` pattern — each
|
|
180
|
-
* breakpoint needs its own layout array, not a single layout fanned out
|
|
181
|
-
* (fanning produces the cascading staircase bug at smaller breakpoints because
|
|
182
|
-
* RGL clamps `w` but preserves `x`).
|
|
183
|
-
*
|
|
184
|
-
* **Edit at lg.** Drags performed at smaller breakpoints update only that
|
|
185
|
-
* breakpoint's layout (per RGL); the canonical lg layout doesn't reflect those
|
|
186
|
-
* edits, so they revert when the viewport resizes back to lg. Persist your
|
|
187
|
-
* customize-mode UX at the lg breakpoint.
|
|
188
|
-
*
|
|
189
|
-
* Consumers must import the lib's stylesheet once at app entry:
|
|
190
|
-
*
|
|
191
|
-
* import "@olympusoss/canvas/styles/dashboard-grid.css";
|
|
192
|
-
*
|
|
193
|
-
* That sheet pulls in `react-grid-layout/css/styles.css` and
|
|
194
|
-
* `react-resizable/css/styles.css`, plus a small canvas-token override that
|
|
195
|
-
* recolours the placeholder + resize handles.
|
|
196
|
-
*/
|
|
197
|
-
export const DashboardGrid = React.forwardRef<HTMLDivElement, DashboardGridProps>(
|
|
198
|
-
(
|
|
199
|
-
{
|
|
200
|
-
items,
|
|
201
|
-
onItemsChange,
|
|
202
|
-
renderItem,
|
|
203
|
-
editing = false,
|
|
204
|
-
rowHeight = 80,
|
|
205
|
-
cols,
|
|
206
|
-
breakpoints,
|
|
207
|
-
margin = [16, 16],
|
|
208
|
-
emptyState,
|
|
209
|
-
className,
|
|
210
|
-
...props
|
|
211
|
-
},
|
|
212
|
-
ref,
|
|
213
|
-
) => {
|
|
214
|
-
const resolvedCols = { ...DEFAULT_COLS, ...cols };
|
|
215
|
-
const resolvedBreakpoints = { ...DEFAULT_BREAKPOINTS, ...breakpoints };
|
|
216
|
-
|
|
217
|
-
// Track the wrapper's width via ResizeObserver and pass it explicitly to
|
|
218
|
-
// react-grid-layout's Responsive component. See the `ResponsiveGridLayout`
|
|
219
|
-
// comment above for why we don't use WidthProvider.
|
|
220
|
-
//
|
|
221
|
-
// `useLayoutEffect` measures synchronously after DOM mount but before
|
|
222
|
-
// paint, so the first paint already has the correct width in real
|
|
223
|
-
// browsers (no flicker). In SSR / jsdom (no real layout), clientWidth
|
|
224
|
-
// is 0 — the fallback below kicks in.
|
|
225
|
-
const wrapperRef = React.useRef<HTMLDivElement | null>(null);
|
|
226
|
-
const [measuredWidth, setMeasuredWidth] = React.useState<number | undefined>(undefined);
|
|
227
|
-
// SSR-safe: useLayoutEffect warns when called in node — fall back to useEffect.
|
|
228
|
-
// jsdom always defines `window`, so the SSR branch is unreachable in tests.
|
|
229
|
-
/* v8 ignore start */
|
|
230
|
-
const useIsoLayoutEffect =
|
|
231
|
-
typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
|
|
232
|
-
/* v8 ignore stop */
|
|
233
|
-
useIsoLayoutEffect(() => {
|
|
234
|
-
const el = wrapperRef.current;
|
|
235
|
-
/* v8 ignore next — `el` is always set after mount; defensive guard */
|
|
236
|
-
if (!el) return;
|
|
237
|
-
if (el.clientWidth > 0) setMeasuredWidth(el.clientWidth);
|
|
238
|
-
}, []);
|
|
239
|
-
React.useEffect(() => {
|
|
240
|
-
const el = wrapperRef.current;
|
|
241
|
-
if (!el || typeof ResizeObserver === "undefined") return;
|
|
242
|
-
const obs = new ResizeObserver((entries) => {
|
|
243
|
-
const w = entries[0]?.contentRect.width ?? el.clientWidth;
|
|
244
|
-
if (w > 0) setMeasuredWidth(w);
|
|
245
|
-
});
|
|
246
|
-
obs.observe(el);
|
|
247
|
-
return () => obs.disconnect();
|
|
248
|
-
}, []);
|
|
249
|
-
|
|
250
|
-
// Merge the forwarded ref with our internal wrapperRef so callers still
|
|
251
|
-
// get their ref set, while we keep a handle for ResizeObserver.
|
|
252
|
-
const setMergedRef = React.useCallback(
|
|
253
|
-
(node: HTMLDivElement | null) => {
|
|
254
|
-
wrapperRef.current = node;
|
|
255
|
-
if (typeof ref === "function") ref(node);
|
|
256
|
-
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
257
|
-
},
|
|
258
|
-
[ref],
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
// Generate one layout PER breakpoint by scaling each item's `x` and `w`
|
|
262
|
-
// proportionally to that breakpoint's column count. Treats `lg` as the
|
|
263
|
-
// canonical source — `items` is consumed as `lg`-coords. Anything else is
|
|
264
|
-
// derived. This is the documented react-grid-layout pattern (their own
|
|
265
|
-
// demos hand-roll per-breakpoint layouts) — fanning a single layout into
|
|
266
|
-
// every breakpoint causes RGL to clamp `w` to `cols`, leaving wide items
|
|
267
|
-
// stacked diagonally with their original `x` offsets ("staircase" bug).
|
|
268
|
-
const lgCols = resolvedCols.lg;
|
|
269
|
-
const libLayouts = React.useMemo<Layouts>(
|
|
270
|
-
() => ({
|
|
271
|
-
lg: toLibLayout(items),
|
|
272
|
-
md: toLibLayout(items.map((it) => scaleItem(it, lgCols, resolvedCols.md))),
|
|
273
|
-
sm: toLibLayout(items.map((it) => scaleItem(it, lgCols, resolvedCols.sm))),
|
|
274
|
-
xs: toLibLayout(items.map((it) => scaleItem(it, lgCols, resolvedCols.xs))),
|
|
275
|
-
xxs: toLibLayout(items.map((it) => scaleItem(it, lgCols, resolvedCols.xxs))),
|
|
276
|
-
}),
|
|
277
|
-
[items, lgCols, resolvedCols.md, resolvedCols.sm, resolvedCols.xs, resolvedCols.xxs],
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
const handleLayoutChange = React.useCallback(
|
|
281
|
-
(currentLayout: Layout[], allLayouts: Layouts) => {
|
|
282
|
-
if (!onItemsChange) return;
|
|
283
|
-
// Merge from `allLayouts.lg` so we always feed lg-coord items back into
|
|
284
|
-
// state — even when the user is interacting at a smaller breakpoint, the
|
|
285
|
-
// canonical `items` shape stays in lg-coords. (Caveat: drags performed
|
|
286
|
-
// at sm/xs/xxs only edit that breakpoint's layout; the lg layout doesn't
|
|
287
|
-
// reflect those edits — drag at lg for the persisted change.)
|
|
288
|
-
const lgLayout = allLayouts?.lg ?? currentLayout;
|
|
289
|
-
const next = mergeLibLayout(items, lgLayout);
|
|
290
|
-
if (isSameLayout(items, next)) return;
|
|
291
|
-
onItemsChange(next);
|
|
292
|
-
},
|
|
293
|
-
[items, onItemsChange],
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
if (items.length === 0 && emptyState !== undefined) {
|
|
297
|
-
return (
|
|
298
|
-
<div
|
|
299
|
-
ref={setMergedRef}
|
|
300
|
-
className={cn("w-full", className)}
|
|
301
|
-
data-dashboard-grid-editing={editing ? "true" : "false"}
|
|
302
|
-
{...props}
|
|
303
|
-
>
|
|
304
|
-
{emptyState}
|
|
305
|
-
</div>
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return (
|
|
310
|
-
<div
|
|
311
|
-
ref={setMergedRef}
|
|
312
|
-
className={cn("w-full", className)}
|
|
313
|
-
data-dashboard-grid-editing={editing ? "true" : "false"}
|
|
314
|
-
{...props}
|
|
315
|
-
>
|
|
316
|
-
<ResponsiveGridLayout
|
|
317
|
-
// Fallback width (1024) is used only when there's no real layout
|
|
318
|
-
// to measure (SSR, jsdom, first paint in some edge cases). In
|
|
319
|
-
// real browsers `useLayoutEffect` sets the actual width before
|
|
320
|
-
// first paint.
|
|
321
|
-
width={measuredWidth ?? 1024}
|
|
322
|
-
layouts={libLayouts}
|
|
323
|
-
cols={resolvedCols}
|
|
324
|
-
breakpoints={resolvedBreakpoints}
|
|
325
|
-
rowHeight={rowHeight}
|
|
326
|
-
margin={margin}
|
|
327
|
-
isDraggable={editing}
|
|
328
|
-
isResizable={editing}
|
|
329
|
-
draggableHandle=".dashboard-grid-handle"
|
|
330
|
-
onLayoutChange={handleLayoutChange}
|
|
331
|
-
compactType="vertical"
|
|
332
|
-
preventCollision={false}
|
|
333
|
-
>
|
|
334
|
-
{items.map((item) => (
|
|
335
|
-
<div key={item.i} className="group/dashboard-grid-item relative h-full overflow-hidden">
|
|
336
|
-
{/* Inner wrapper forces the rendered widget to fill the cell —
|
|
337
|
-
consumers shouldn't have to add `h-full` to every card just to
|
|
338
|
-
make rows align. Kept separate from the drag handle so the
|
|
339
|
-
`*:h-full *:w-full` rule doesn't blow up the absolute handle. */}
|
|
340
|
-
<div className="h-full w-full *:h-full *:w-full">{renderItem(item)}</div>
|
|
341
|
-
{editing && (
|
|
342
|
-
<>
|
|
343
|
-
<div
|
|
344
|
-
role="button"
|
|
345
|
-
tabIndex={0}
|
|
346
|
-
className="dashboard-grid-handle absolute right-2 top-2 z-10 flex h-6 w-6 cursor-grab items-center justify-center rounded-md border border-border bg-background/80 text-muted-foreground shadow-sm backdrop-blur-sm transition-colors hover:bg-accent hover:text-foreground active:cursor-grabbing"
|
|
347
|
-
aria-label={`Drag ${item.i}`}
|
|
348
|
-
>
|
|
349
|
-
<GripVertical className="h-3.5 w-3.5" />
|
|
350
|
-
</div>
|
|
351
|
-
<button
|
|
352
|
-
type="button"
|
|
353
|
-
onClick={() => onItemsChange?.(items.filter((it) => it.i !== item.i))}
|
|
354
|
-
className="absolute bottom-2 left-1/2 z-10 flex h-7 -translate-x-1/2 items-center gap-1.5 rounded-md border border-border bg-background/80 px-2.5 text-xs font-medium text-destructive shadow-sm backdrop-blur-sm transition-colors hover:bg-destructive hover:text-destructive-foreground"
|
|
355
|
-
aria-label={`Remove ${item.i}`}
|
|
356
|
-
>
|
|
357
|
-
<Trash2 className="h-3.5 w-3.5" />
|
|
358
|
-
Remove
|
|
359
|
-
</button>
|
|
360
|
-
</>
|
|
361
|
-
)}
|
|
362
|
-
</div>
|
|
363
|
-
))}
|
|
364
|
-
</ResponsiveGridLayout>
|
|
365
|
-
</div>
|
|
366
|
-
);
|
|
367
|
-
},
|
|
368
|
-
);
|
|
369
|
-
DashboardGrid.displayName = "DashboardGrid";
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type ColumnDef,
|
|
5
|
-
type ColumnFiltersState,
|
|
6
|
-
flexRender,
|
|
7
|
-
getCoreRowModel,
|
|
8
|
-
getFilteredRowModel,
|
|
9
|
-
getPaginationRowModel,
|
|
10
|
-
getSortedRowModel,
|
|
11
|
-
type RowSelectionState,
|
|
12
|
-
type SortingState,
|
|
13
|
-
useReactTable,
|
|
14
|
-
} from "@tanstack/react-table";
|
|
15
|
-
import { Plus, RefreshCw, Search } from "lucide-react";
|
|
16
|
-
import * as React from "react";
|
|
17
|
-
|
|
18
|
-
import { cn } from "../../lib/utils";
|
|
19
|
-
import { Button } from "../atoms/button";
|
|
20
|
-
import { Checkbox } from "../atoms/checkbox";
|
|
21
|
-
import { Input } from "../atoms/input";
|
|
22
|
-
import { LoadingState } from "../molecules/loading-state";
|
|
23
|
-
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../molecules/table";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Column descriptor. Accepts both the legacy API
|
|
27
|
-
* (`field`/`headerName`/`renderCell`/`minWidth`/`maxWidth`/`sortable`)
|
|
28
|
-
* and the TanStack-style API (`accessorKey`/`id`/`header`/`cell`).
|
|
29
|
-
* Internally columns are normalized to TanStack `ColumnDef`.
|
|
30
|
-
*/
|
|
31
|
-
export interface DataTableColumn<TData = unknown> {
|
|
32
|
-
field?: keyof TData | string;
|
|
33
|
-
headerName?: string;
|
|
34
|
-
width?: number;
|
|
35
|
-
minWidth?: number;
|
|
36
|
-
maxWidth?: number;
|
|
37
|
-
flex?: number;
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
-
renderCell?: (value: any, row: any) => React.ReactNode;
|
|
40
|
-
sortable?: boolean;
|
|
41
|
-
accessorKey?: keyof TData | string;
|
|
42
|
-
id?: string;
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
-
header?: React.ReactNode | ((ctx: any) => React.ReactNode);
|
|
45
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
-
cell?: (ctx: { row: { original: TData }; getValue: () => any }) => React.ReactNode;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface DataTableProps<TData> {
|
|
50
|
-
columns: DataTableColumn<TData>[];
|
|
51
|
-
data: TData[];
|
|
52
|
-
className?: string;
|
|
53
|
-
emptyMessage?: string;
|
|
54
|
-
pageSize?: number;
|
|
55
|
-
/** Row key field. Defaults to "id". */
|
|
56
|
-
keyField?: string;
|
|
57
|
-
loading?: boolean;
|
|
58
|
-
searchable?: boolean;
|
|
59
|
-
searchKey?: string;
|
|
60
|
-
searchValue?: string;
|
|
61
|
-
onSearchChange?: (v: string) => void;
|
|
62
|
-
searchPlaceholder?: string;
|
|
63
|
-
onRowClick?: (row: TData) => void;
|
|
64
|
-
onRefresh?: () => void;
|
|
65
|
-
onAdd?: () => void;
|
|
66
|
-
addButtonText?: string;
|
|
67
|
-
selectable?: boolean;
|
|
68
|
-
selectedKeys?: Set<string>;
|
|
69
|
-
onSelectionChange?: (keys: Set<string>) => void;
|
|
70
|
-
pagination?: boolean;
|
|
71
|
-
pageSizeOptions?: number[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function normalizeColumn<TData>(col: DataTableColumn<TData>): ColumnDef<TData, unknown> {
|
|
75
|
-
const id = col.id ?? (col.field as string | undefined) ?? (col.accessorKey as string | undefined);
|
|
76
|
-
const accessorKey = col.accessorKey ?? col.field;
|
|
77
|
-
const header = col.headerName ?? col.header;
|
|
78
|
-
const cell =
|
|
79
|
-
col.cell ??
|
|
80
|
-
(col.renderCell
|
|
81
|
-
? (ctx: { row: { original: TData }; getValue: () => unknown }) =>
|
|
82
|
-
col.renderCell!(ctx.getValue(), ctx.row.original)
|
|
83
|
-
: undefined);
|
|
84
|
-
|
|
85
|
-
const def: Record<string, unknown> = {};
|
|
86
|
-
/* c8 ignore next -- false branch: callers always supply id/accessorKey/field */
|
|
87
|
-
if (id) def.id = String(id);
|
|
88
|
-
/* c8 ignore next -- false branch: callers always supply accessorKey/field */
|
|
89
|
-
if (accessorKey) def.accessorKey = String(accessorKey);
|
|
90
|
-
/* c8 ignore next -- false branch: callers always supply header/headerName */
|
|
91
|
-
if (header !== undefined) def.header = header;
|
|
92
|
-
if (cell) def.cell = cell;
|
|
93
|
-
if (col.sortable !== undefined) def.enableSorting = col.sortable;
|
|
94
|
-
const widthHint = col.minWidth ?? col.width;
|
|
95
|
-
if (widthHint !== undefined) {
|
|
96
|
-
def.size = widthHint;
|
|
97
|
-
def.minSize = widthHint;
|
|
98
|
-
}
|
|
99
|
-
if (col.maxWidth !== undefined) def.maxSize = col.maxWidth;
|
|
100
|
-
return def as unknown as ColumnDef<TData, unknown>;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function DataTable<TData>({
|
|
104
|
-
columns,
|
|
105
|
-
data,
|
|
106
|
-
className,
|
|
107
|
-
emptyMessage = "No results.",
|
|
108
|
-
pageSize = 10,
|
|
109
|
-
keyField = "id",
|
|
110
|
-
loading = false,
|
|
111
|
-
searchable = false,
|
|
112
|
-
searchKey,
|
|
113
|
-
searchValue,
|
|
114
|
-
onSearchChange,
|
|
115
|
-
searchPlaceholder = "Search...",
|
|
116
|
-
onRowClick,
|
|
117
|
-
onRefresh,
|
|
118
|
-
onAdd,
|
|
119
|
-
addButtonText = "Add",
|
|
120
|
-
selectable = false,
|
|
121
|
-
selectedKeys,
|
|
122
|
-
onSelectionChange,
|
|
123
|
-
pagination = true,
|
|
124
|
-
pageSizeOptions: _pageSizeOptions,
|
|
125
|
-
}: DataTableProps<TData>) {
|
|
126
|
-
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
127
|
-
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
|
128
|
-
const [localSearch, setLocalSearch] = React.useState<string>(searchValue ?? "");
|
|
129
|
-
const currentSearch = searchValue ?? localSearch;
|
|
130
|
-
|
|
131
|
-
const normalizedColumns = React.useMemo(() => columns.map((c) => normalizeColumn(c)), [columns]);
|
|
132
|
-
|
|
133
|
-
const tableColumns = React.useMemo<ColumnDef<TData, unknown>[]>(() => {
|
|
134
|
-
if (!selectable) return normalizedColumns;
|
|
135
|
-
const selectionCol: ColumnDef<TData, unknown> = {
|
|
136
|
-
id: "__select__",
|
|
137
|
-
enableSorting: false,
|
|
138
|
-
header: ({ table }) => (
|
|
139
|
-
<Checkbox
|
|
140
|
-
checked={
|
|
141
|
-
table.getIsAllPageRowsSelected() ||
|
|
142
|
-
(table.getIsSomePageRowsSelected() && "indeterminate")
|
|
143
|
-
}
|
|
144
|
-
onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
|
|
145
|
-
aria-label="Select all"
|
|
146
|
-
/>
|
|
147
|
-
),
|
|
148
|
-
cell: ({ row }) => (
|
|
149
|
-
<Checkbox
|
|
150
|
-
checked={row.getIsSelected()}
|
|
151
|
-
onCheckedChange={(v) => row.toggleSelected(!!v)}
|
|
152
|
-
aria-label="Select row"
|
|
153
|
-
onClick={(e) => e.stopPropagation()}
|
|
154
|
-
/>
|
|
155
|
-
),
|
|
156
|
-
};
|
|
157
|
-
return [selectionCol, ...normalizedColumns];
|
|
158
|
-
}, [normalizedColumns, selectable]);
|
|
159
|
-
|
|
160
|
-
const rowSelection = React.useMemo<RowSelectionState>(() => {
|
|
161
|
-
if (!selectable || !selectedKeys) return {};
|
|
162
|
-
const out: RowSelectionState = {};
|
|
163
|
-
selectedKeys.forEach((k) => {
|
|
164
|
-
out[k] = true;
|
|
165
|
-
});
|
|
166
|
-
return out;
|
|
167
|
-
}, [selectable, selectedKeys]);
|
|
168
|
-
|
|
169
|
-
React.useEffect(() => {
|
|
170
|
-
if (searchKey && currentSearch !== undefined) {
|
|
171
|
-
setColumnFilters([{ id: searchKey, value: currentSearch }]);
|
|
172
|
-
}
|
|
173
|
-
}, [searchKey, currentSearch]);
|
|
174
|
-
|
|
175
|
-
const getRowId = React.useCallback(
|
|
176
|
-
/* c8 ignore next -- fallback to "" only triggers if a row is missing keyField, which callers guarantee */
|
|
177
|
-
(row: TData) => String((row as Record<string, unknown>)[keyField] ?? ""),
|
|
178
|
-
[keyField],
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const table = useReactTable<TData>({
|
|
182
|
-
data,
|
|
183
|
-
columns: tableColumns,
|
|
184
|
-
getRowId,
|
|
185
|
-
getCoreRowModel: getCoreRowModel(),
|
|
186
|
-
...(pagination ? { getPaginationRowModel: getPaginationRowModel() } : {}),
|
|
187
|
-
getSortedRowModel: getSortedRowModel(),
|
|
188
|
-
getFilteredRowModel: getFilteredRowModel(),
|
|
189
|
-
onSortingChange: setSorting,
|
|
190
|
-
onColumnFiltersChange: setColumnFilters,
|
|
191
|
-
onRowSelectionChange: (updater) => {
|
|
192
|
-
if (!onSelectionChange) return;
|
|
193
|
-
/* c8 ignore next -- TanStack always calls this with a function updater; the direct-object path is never exercised */
|
|
194
|
-
const newState = typeof updater === "function" ? updater(rowSelection) : updater;
|
|
195
|
-
onSelectionChange(new Set(Object.keys(newState).filter((k) => newState[k])));
|
|
196
|
-
},
|
|
197
|
-
state: { sorting, columnFilters, rowSelection },
|
|
198
|
-
...(pagination ? { initialState: { pagination: { pageSize } } } : {}),
|
|
199
|
-
enableRowSelection: !!selectable,
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const hasToolbar = searchable || onRefresh || onAdd;
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<div className={cn("space-y-4", className)}>
|
|
206
|
-
{hasToolbar && (
|
|
207
|
-
<div className="flex items-center justify-between gap-4">
|
|
208
|
-
{searchable ? (
|
|
209
|
-
<div className="relative flex-1 max-w-sm">
|
|
210
|
-
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
211
|
-
<Input
|
|
212
|
-
placeholder={searchPlaceholder}
|
|
213
|
-
value={currentSearch}
|
|
214
|
-
onChange={(e) => {
|
|
215
|
-
const v = e.target.value;
|
|
216
|
-
setLocalSearch(v);
|
|
217
|
-
onSearchChange?.(v);
|
|
218
|
-
}}
|
|
219
|
-
className="pl-8"
|
|
220
|
-
/>
|
|
221
|
-
</div>
|
|
222
|
-
) : (
|
|
223
|
-
<div />
|
|
224
|
-
)}
|
|
225
|
-
<div className="flex items-center gap-2">
|
|
226
|
-
{onRefresh && (
|
|
227
|
-
<Button variant="outline" size="sm" onClick={onRefresh}>
|
|
228
|
-
<RefreshCw className="h-4 w-4" />
|
|
229
|
-
</Button>
|
|
230
|
-
)}
|
|
231
|
-
{onAdd && (
|
|
232
|
-
<Button size="sm" onClick={onAdd}>
|
|
233
|
-
<Plus className="mr-1 h-4 w-4" /> {addButtonText}
|
|
234
|
-
</Button>
|
|
235
|
-
)}
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
)}
|
|
239
|
-
|
|
240
|
-
<div data-slot="data-table" className="rounded-md border border-border">
|
|
241
|
-
<Table>
|
|
242
|
-
<TableHeader>
|
|
243
|
-
{table.getHeaderGroups().map((headerGroup) => (
|
|
244
|
-
<TableRow key={headerGroup.id}>
|
|
245
|
-
{headerGroup.headers.map((header) => {
|
|
246
|
-
const colDef = header.column.columnDef as ColumnDef<TData, unknown> & {
|
|
247
|
-
minSize?: number;
|
|
248
|
-
maxSize?: number;
|
|
249
|
-
};
|
|
250
|
-
return (
|
|
251
|
-
<TableHead
|
|
252
|
-
key={header.id}
|
|
253
|
-
style={{
|
|
254
|
-
minWidth: colDef.minSize,
|
|
255
|
-
maxWidth: colDef.maxSize,
|
|
256
|
-
}}
|
|
257
|
-
>
|
|
258
|
-
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
259
|
-
</TableHead>
|
|
260
|
-
);
|
|
261
|
-
})}
|
|
262
|
-
</TableRow>
|
|
263
|
-
))}
|
|
264
|
-
</TableHeader>
|
|
265
|
-
<TableBody>
|
|
266
|
-
{loading ? (
|
|
267
|
-
<TableRow>
|
|
268
|
-
<TableCell colSpan={tableColumns.length} className="h-24 p-0">
|
|
269
|
-
<LoadingState />
|
|
270
|
-
</TableCell>
|
|
271
|
-
</TableRow>
|
|
272
|
-
) : table.getRowModel().rows?.length ? (
|
|
273
|
-
table.getRowModel().rows.map((row) => (
|
|
274
|
-
<TableRow
|
|
275
|
-
key={row.id}
|
|
276
|
-
data-state={row.getIsSelected() ? "selected" : undefined}
|
|
277
|
-
className={onRowClick ? "cursor-pointer" : undefined}
|
|
278
|
-
onClick={() => onRowClick?.(row.original)}
|
|
279
|
-
>
|
|
280
|
-
{row.getVisibleCells().map((cell) => (
|
|
281
|
-
<TableCell key={cell.id}>
|
|
282
|
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
283
|
-
</TableCell>
|
|
284
|
-
))}
|
|
285
|
-
</TableRow>
|
|
286
|
-
))
|
|
287
|
-
) : (
|
|
288
|
-
<TableRow>
|
|
289
|
-
<TableCell
|
|
290
|
-
colSpan={tableColumns.length}
|
|
291
|
-
className="h-24 text-center text-muted-foreground"
|
|
292
|
-
>
|
|
293
|
-
{emptyMessage}
|
|
294
|
-
</TableCell>
|
|
295
|
-
</TableRow>
|
|
296
|
-
)}
|
|
297
|
-
</TableBody>
|
|
298
|
-
</Table>
|
|
299
|
-
</div>
|
|
300
|
-
|
|
301
|
-
{pagination && table.getPageCount() > 1 && (
|
|
302
|
-
<div className="flex items-center justify-between">
|
|
303
|
-
<p className="text-sm text-muted-foreground">
|
|
304
|
-
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
305
|
-
</p>
|
|
306
|
-
<div className="flex gap-2">
|
|
307
|
-
<Button
|
|
308
|
-
variant="outline"
|
|
309
|
-
size="sm"
|
|
310
|
-
onClick={() => table.previousPage()}
|
|
311
|
-
disabled={!table.getCanPreviousPage()}
|
|
312
|
-
>
|
|
313
|
-
Previous
|
|
314
|
-
</Button>
|
|
315
|
-
<Button
|
|
316
|
-
variant="outline"
|
|
317
|
-
size="sm"
|
|
318
|
-
onClick={() => table.nextPage()}
|
|
319
|
-
disabled={!table.getCanNextPage()}
|
|
320
|
-
>
|
|
321
|
-
Next
|
|
322
|
-
</Button>
|
|
323
|
-
</div>
|
|
324
|
-
</div>
|
|
325
|
-
)}
|
|
326
|
-
</div>
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
export { DataTable };
|