@hexclave/dashboard-ui-components 1.0.2 → 1.0.5
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/dist/components/button.d.ts +4 -4
- package/dist/components/pill-toggle.js +2 -2
- package/dist/components/pill-toggle.js.map +1 -1
- package/dist/dashboard-ui-components.global.js +7447 -9242
- package/dist/dashboard-ui-components.global.js.map +4 -4
- package/dist/esm/components/button.d.ts +4 -4
- package/dist/esm/components/pill-toggle.js +3 -3
- package/dist/esm/components/pill-toggle.js.map +1 -1
- package/package.json +4 -3
- package/src/components/alert.tsx +120 -0
- package/src/components/analytics-chart/analytics-chart-pie.tsx +369 -0
- package/src/components/analytics-chart/analytics-chart.tsx +1585 -0
- package/src/components/analytics-chart/default-analytics-chart-tooltip.tsx +265 -0
- package/src/components/analytics-chart/format.ts +101 -0
- package/src/components/analytics-chart/index.ts +75 -0
- package/src/components/analytics-chart/palette.ts +68 -0
- package/src/components/analytics-chart/render-data-series.tsx +169 -0
- package/src/components/analytics-chart/state.ts +165 -0
- package/src/components/analytics-chart/strings.ts +72 -0
- package/src/components/analytics-chart/types.ts +220 -0
- package/src/components/badge.tsx +108 -0
- package/src/components/button.tsx +104 -0
- package/src/components/card.tsx +263 -0
- package/src/components/chart-card.tsx +101 -0
- package/src/components/chart-container.tsx +155 -0
- package/src/components/chart-legend.tsx +65 -0
- package/src/components/chart-theme.tsx +52 -0
- package/src/components/chart-tooltip.tsx +165 -0
- package/src/components/cursor-blast-effect.tsx +334 -0
- package/src/components/data-grid/data-grid-sizing.ts +51 -0
- package/src/components/data-grid/data-grid-toolbar.tsx +373 -0
- package/src/components/data-grid/data-grid.test.tsx +366 -0
- package/src/components/data-grid/data-grid.tsx +1220 -0
- package/src/components/data-grid/index.ts +64 -0
- package/src/components/data-grid/state.ts +235 -0
- package/src/components/data-grid/strings.ts +45 -0
- package/src/components/data-grid/types.ts +401 -0
- package/src/components/data-grid/use-data-source.ts +413 -0
- package/src/components/data-grid/use-url-state.test.tsx +88 -0
- package/src/components/data-grid/use-url-state.ts +298 -0
- package/src/components/dialog.tsx +207 -0
- package/src/components/edit-mode.tsx +17 -0
- package/src/components/empty-state.tsx +64 -0
- package/src/components/input.tsx +85 -0
- package/src/components/metric-card.tsx +142 -0
- package/src/components/pill-toggle.tsx +159 -0
- package/src/components/progress-bar.tsx +82 -0
- package/src/components/separator.tsx +36 -0
- package/src/components/skeleton.tsx +30 -0
- package/src/components/table.tsx +113 -0
- package/src/components/tabs.tsx +214 -0
- package/src/index.ts +69 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@hexclave/ui";
|
|
4
|
+
import {
|
|
5
|
+
Check,
|
|
6
|
+
DownloadSimple,
|
|
7
|
+
Eye,
|
|
8
|
+
EyeSlash,
|
|
9
|
+
MagnifyingGlass,
|
|
10
|
+
X,
|
|
11
|
+
} from "@phosphor-icons/react";
|
|
12
|
+
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
13
|
+
import type {
|
|
14
|
+
DataGridColumnDef,
|
|
15
|
+
DataGridDateDisplay,
|
|
16
|
+
DataGridStrings,
|
|
17
|
+
DataGridToolbarContext,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
// ─── Popover primitive ───────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function usePopover() {
|
|
23
|
+
const [open, setOpen] = useState(false);
|
|
24
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
if (!open) return;
|
|
28
|
+
const handler = (e: MouseEvent) => {
|
|
29
|
+
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
30
|
+
setOpen(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
document.addEventListener("mousedown", handler);
|
|
34
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
35
|
+
}, [open]);
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (!open) return;
|
|
39
|
+
const handler = (e: KeyboardEvent) => {
|
|
40
|
+
if (e.key === "Escape") setOpen(false);
|
|
41
|
+
};
|
|
42
|
+
document.addEventListener("keydown", handler);
|
|
43
|
+
return () => document.removeEventListener("keydown", handler);
|
|
44
|
+
}, [open]);
|
|
45
|
+
|
|
46
|
+
return { open, setOpen, ref };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function PopoverPanel({
|
|
50
|
+
children,
|
|
51
|
+
className,
|
|
52
|
+
popoverRef,
|
|
53
|
+
}: {
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
className?: string;
|
|
56
|
+
popoverRef: React.Ref<HTMLDivElement>;
|
|
57
|
+
}) {
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
ref={popoverRef}
|
|
61
|
+
className={cn(
|
|
62
|
+
"absolute top-full left-0 mt-1 z-50",
|
|
63
|
+
"bg-popover text-popover-foreground rounded-xl shadow-lg",
|
|
64
|
+
"ring-1 ring-black/[0.08] dark:ring-white/[0.1]",
|
|
65
|
+
"backdrop-blur-xl",
|
|
66
|
+
className,
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Quick search ────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function QuickSearch({
|
|
77
|
+
value,
|
|
78
|
+
onChange,
|
|
79
|
+
placeholder,
|
|
80
|
+
}: {
|
|
81
|
+
value: string;
|
|
82
|
+
onChange: (value: string) => void;
|
|
83
|
+
placeholder: string;
|
|
84
|
+
}) {
|
|
85
|
+
return (
|
|
86
|
+
<div className="relative flex min-w-0 flex-1 items-center sm:flex-initial">
|
|
87
|
+
<MagnifyingGlass className="absolute left-2.5 h-3.5 w-3.5 text-muted-foreground/50 pointer-events-none" />
|
|
88
|
+
<input
|
|
89
|
+
type="text"
|
|
90
|
+
className={cn(
|
|
91
|
+
"h-8 w-full sm:w-52 pl-8 pr-7 rounded-xl text-xs",
|
|
92
|
+
"bg-background",
|
|
93
|
+
"border border-black/[0.08] dark:border-white/[0.08]",
|
|
94
|
+
"placeholder:text-muted-foreground/40",
|
|
95
|
+
"focus:outline-none focus:ring-1 focus:ring-foreground/[0.1]",
|
|
96
|
+
"transition-all duration-150",
|
|
97
|
+
)}
|
|
98
|
+
placeholder={placeholder}
|
|
99
|
+
value={value}
|
|
100
|
+
onChange={(e) => onChange(e.target.value)}
|
|
101
|
+
/>
|
|
102
|
+
{value && (
|
|
103
|
+
<button
|
|
104
|
+
className="absolute right-2 text-muted-foreground/40 hover:text-muted-foreground"
|
|
105
|
+
onClick={() => onChange("")}
|
|
106
|
+
aria-label="Clear search"
|
|
107
|
+
>
|
|
108
|
+
<X className="h-3 w-3" />
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Toolbar button ──────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
function ToolbarButton({
|
|
118
|
+
children,
|
|
119
|
+
onClick,
|
|
120
|
+
active,
|
|
121
|
+
title,
|
|
122
|
+
className: extraClassName,
|
|
123
|
+
}: {
|
|
124
|
+
children: React.ReactNode;
|
|
125
|
+
onClick?: () => void;
|
|
126
|
+
active?: boolean;
|
|
127
|
+
title?: string;
|
|
128
|
+
className?: string;
|
|
129
|
+
}) {
|
|
130
|
+
return (
|
|
131
|
+
<button
|
|
132
|
+
className={cn(
|
|
133
|
+
"relative flex items-center justify-center rounded-lg text-xs font-medium",
|
|
134
|
+
"h-7 w-7",
|
|
135
|
+
"transition-colors duration-75",
|
|
136
|
+
active
|
|
137
|
+
? "bg-foreground/[0.06] text-foreground"
|
|
138
|
+
: "text-muted-foreground hover:text-foreground hover:bg-foreground/[0.04]",
|
|
139
|
+
extraClassName,
|
|
140
|
+
)}
|
|
141
|
+
onClick={onClick}
|
|
142
|
+
title={title}
|
|
143
|
+
>
|
|
144
|
+
{children}
|
|
145
|
+
</button>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Column manager ──────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
function ColumnManager<TRow>({
|
|
152
|
+
columns,
|
|
153
|
+
visibility,
|
|
154
|
+
onChange,
|
|
155
|
+
strings,
|
|
156
|
+
dateDisplay,
|
|
157
|
+
onDateDisplayChange,
|
|
158
|
+
hasDateColumns,
|
|
159
|
+
}: {
|
|
160
|
+
columns: readonly DataGridColumnDef<TRow>[];
|
|
161
|
+
visibility: Record<string, boolean>;
|
|
162
|
+
onChange: (visibility: Record<string, boolean>) => void;
|
|
163
|
+
strings: DataGridStrings;
|
|
164
|
+
dateDisplay: DataGridDateDisplay;
|
|
165
|
+
onDateDisplayChange: (mode: DataGridDateDisplay) => void;
|
|
166
|
+
hasDateColumns: boolean;
|
|
167
|
+
}) {
|
|
168
|
+
const hideableColumns = useMemo(
|
|
169
|
+
() => columns.filter((c) => c.hideable !== false),
|
|
170
|
+
[columns],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const toggleColumn = (id: string) => {
|
|
174
|
+
const current = visibility[id] !== false;
|
|
175
|
+
onChange({ ...visibility, [id]: !current });
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const showAll = () => {
|
|
179
|
+
const next = { ...visibility };
|
|
180
|
+
for (const col of hideableColumns) next[col.id] = true;
|
|
181
|
+
onChange(next);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const hideAll = () => {
|
|
185
|
+
const next = { ...visibility };
|
|
186
|
+
for (const col of hideableColumns) next[col.id] = false;
|
|
187
|
+
onChange(next);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div className="p-2 min-w-[240px] max-w-[300px]">
|
|
192
|
+
<div className="max-h-[280px] overflow-y-auto space-y-0.5">
|
|
193
|
+
{hideableColumns.map((col) => {
|
|
194
|
+
const visible = visibility[col.id] !== false;
|
|
195
|
+
return (
|
|
196
|
+
<button
|
|
197
|
+
key={col.id}
|
|
198
|
+
className={cn(
|
|
199
|
+
"flex items-center gap-2 w-full px-2.5 py-1.5 rounded-lg text-xs",
|
|
200
|
+
"hover:bg-foreground/[0.06] transition-colors duration-75",
|
|
201
|
+
visible ? "text-foreground" : "text-muted-foreground/50",
|
|
202
|
+
)}
|
|
203
|
+
onClick={() => toggleColumn(col.id)}
|
|
204
|
+
>
|
|
205
|
+
{visible ? (
|
|
206
|
+
<Eye className="h-3.5 w-3.5 flex-shrink-0 text-blue-500" />
|
|
207
|
+
) : (
|
|
208
|
+
<EyeSlash className="h-3.5 w-3.5 flex-shrink-0" />
|
|
209
|
+
)}
|
|
210
|
+
<span className="truncate text-left">
|
|
211
|
+
{typeof col.header === "string" ? col.header : col.id}
|
|
212
|
+
</span>
|
|
213
|
+
{visible && <Check className="h-3 w-3 ml-auto flex-shrink-0 text-blue-500" />}
|
|
214
|
+
</button>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
217
|
+
</div>
|
|
218
|
+
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-foreground/[0.06]">
|
|
219
|
+
<button className="text-[10px] text-muted-foreground hover:text-foreground font-medium uppercase tracking-wider transition-colors duration-75" onClick={showAll}>
|
|
220
|
+
{strings.showAll}
|
|
221
|
+
</button>
|
|
222
|
+
<span className="text-muted-foreground/20">|</span>
|
|
223
|
+
<button className="text-[10px] text-muted-foreground hover:text-foreground font-medium uppercase tracking-wider transition-colors duration-75" onClick={hideAll}>
|
|
224
|
+
{strings.hideAll}
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{/* Date format toggle — only rendered when at least one column
|
|
229
|
+
uses `type: "date"` or `"dateTime"`. Toggling writes to
|
|
230
|
+
`state.dateDisplay` and the grid re-renders every date cell. */}
|
|
231
|
+
{hasDateColumns && (
|
|
232
|
+
<div className="mt-2 pt-2 border-t border-foreground/[0.06]">
|
|
233
|
+
<div className="flex items-center justify-between gap-2 px-1">
|
|
234
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
235
|
+
{strings.dateFormat}
|
|
236
|
+
</span>
|
|
237
|
+
<div className="inline-flex items-center gap-0.5 rounded-lg bg-foreground/[0.04] p-0.5">
|
|
238
|
+
<button
|
|
239
|
+
className={cn(
|
|
240
|
+
"px-2 py-0.5 rounded-md text-[11px] font-medium transition-colors duration-75",
|
|
241
|
+
dateDisplay === "relative"
|
|
242
|
+
? "bg-background text-foreground shadow-sm ring-1 ring-foreground/[0.06]"
|
|
243
|
+
: "text-muted-foreground hover:text-foreground",
|
|
244
|
+
)}
|
|
245
|
+
onClick={() => onDateDisplayChange("relative")}
|
|
246
|
+
>
|
|
247
|
+
{strings.dateFormatRelative}
|
|
248
|
+
</button>
|
|
249
|
+
<button
|
|
250
|
+
className={cn(
|
|
251
|
+
"px-2 py-0.5 rounded-md text-[11px] font-medium transition-colors duration-75",
|
|
252
|
+
dateDisplay === "absolute"
|
|
253
|
+
? "bg-background text-foreground shadow-sm ring-1 ring-foreground/[0.06]"
|
|
254
|
+
: "text-muted-foreground hover:text-foreground",
|
|
255
|
+
)}
|
|
256
|
+
onClick={() => onDateDisplayChange("absolute")}
|
|
257
|
+
>
|
|
258
|
+
{strings.dateFormatAbsolute}
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Main toolbar ────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
export function DataGridToolbar<TRow>({
|
|
271
|
+
ctx,
|
|
272
|
+
extra,
|
|
273
|
+
extraLeading,
|
|
274
|
+
hideQuickSearch,
|
|
275
|
+
}: {
|
|
276
|
+
ctx: DataGridToolbarContext<TRow>;
|
|
277
|
+
/** Extra content rendered inside the toolbar row, to the left of the
|
|
278
|
+
* built-in columns / export actions. Use this to add table-specific
|
|
279
|
+
* affordances (refresh, custom toggles, row counts) without giving up
|
|
280
|
+
* the default actions. */
|
|
281
|
+
extra?: React.ReactNode;
|
|
282
|
+
/** Extra content rendered at the START of the toolbar row — occupies
|
|
283
|
+
* the same position as the built-in quick search (after it, if the
|
|
284
|
+
* quick search is visible). Use this together with `hideQuickSearch`
|
|
285
|
+
* to fully replace the quick search with a custom input, e.g. an
|
|
286
|
+
* AI-powered search bar. */
|
|
287
|
+
extraLeading?: React.ReactNode;
|
|
288
|
+
/** Whether to hide the built-in quick-search input. When `true`,
|
|
289
|
+
* callers are expected to provide their own search UI via
|
|
290
|
+
* `extraLeading`. */
|
|
291
|
+
hideQuickSearch?: boolean;
|
|
292
|
+
}) {
|
|
293
|
+
const { state, onChange, columns, strings, exportCsv } = ctx;
|
|
294
|
+
|
|
295
|
+
const columnPopover = usePopover();
|
|
296
|
+
|
|
297
|
+
const updateVisibility = useCallback(
|
|
298
|
+
(visibility: Record<string, boolean>) => {
|
|
299
|
+
onChange((s) => ({ ...s, columnVisibility: visibility }));
|
|
300
|
+
},
|
|
301
|
+
[onChange],
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const updateDateDisplay = useCallback(
|
|
305
|
+
(mode: DataGridDateDisplay) => {
|
|
306
|
+
onChange((s) => ({ ...s, dateDisplay: mode }));
|
|
307
|
+
},
|
|
308
|
+
[onChange],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const updateQuickSearch = useCallback(
|
|
312
|
+
(value: string) => {
|
|
313
|
+
onChange((s) => ({
|
|
314
|
+
...s,
|
|
315
|
+
quickSearch: value,
|
|
316
|
+
// Reset to first page whenever the search text changes,
|
|
317
|
+
// otherwise you can end up on a page index that no longer
|
|
318
|
+
// exists in the filtered / refetched result set.
|
|
319
|
+
pagination: { ...s.pagination, pageIndex: 0 },
|
|
320
|
+
}));
|
|
321
|
+
},
|
|
322
|
+
[onChange],
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const hasDateColumns = useMemo(
|
|
326
|
+
() => columns.some((c) => c.type === "date" || c.type === "dateTime"),
|
|
327
|
+
[columns],
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div className="flex w-full min-w-0 flex-col gap-2 px-2.5 py-2.5 border-b border-foreground/[0.06] sm:flex-row sm:items-center sm:gap-2">
|
|
332
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
|
333
|
+
{!hideQuickSearch && (
|
|
334
|
+
<QuickSearch
|
|
335
|
+
value={state.quickSearch}
|
|
336
|
+
onChange={updateQuickSearch}
|
|
337
|
+
placeholder={strings.searchPlaceholder}
|
|
338
|
+
/>
|
|
339
|
+
)}
|
|
340
|
+
{extraLeading}
|
|
341
|
+
{extra}
|
|
342
|
+
</div>
|
|
343
|
+
<div className="flex shrink-0 items-center justify-end gap-2">
|
|
344
|
+
<div className="relative shrink-0" ref={columnPopover.ref}>
|
|
345
|
+
<ToolbarButton
|
|
346
|
+
onClick={() => columnPopover.setOpen(!columnPopover.open)}
|
|
347
|
+
active={columnPopover.open}
|
|
348
|
+
title={strings.columns}
|
|
349
|
+
>
|
|
350
|
+
<Eye className="h-3.5 w-3.5" />
|
|
351
|
+
</ToolbarButton>
|
|
352
|
+
{columnPopover.open && (
|
|
353
|
+
<PopoverPanel popoverRef={columnPopover.ref} className="right-0 left-auto">
|
|
354
|
+
<ColumnManager
|
|
355
|
+
columns={columns}
|
|
356
|
+
visibility={state.columnVisibility}
|
|
357
|
+
onChange={updateVisibility}
|
|
358
|
+
strings={strings}
|
|
359
|
+
dateDisplay={state.dateDisplay}
|
|
360
|
+
onDateDisplayChange={updateDateDisplay}
|
|
361
|
+
hasDateColumns={hasDateColumns}
|
|
362
|
+
/>
|
|
363
|
+
</PopoverPanel>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<ToolbarButton onClick={exportCsv} title={strings.export}>
|
|
368
|
+
<DownloadSimple className="h-3.5 w-3.5" />
|
|
369
|
+
</ToolbarButton>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|