@pol-studios/ui 1.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.
@@ -0,0 +1,372 @@
1
+ "use client";
2
+
3
+ // src/data/DataTable.tsx
4
+ import { useState, useRef, useEffect } from "react";
5
+ import { Link } from "@tanstack/react-router";
6
+ import { cn } from "@pol-studios/utils";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ function DataTableSkeleton({ variant = "rich" }) {
9
+ const rowHeight = variant === "compact" ? "h-11" : variant === "rich" ? "h-14" : "h-18";
10
+ const rows = variant === "compact" ? 8 : variant === "rich" ? 6 : 4;
11
+ return /* @__PURE__ */ jsx("div", { className: "space-y-0", children: Array.from({ length: rows }).map((_, i) => /* @__PURE__ */ jsxs(
12
+ "div",
13
+ {
14
+ className: cn(
15
+ "flex items-center gap-4 px-4 py-3",
16
+ rowHeight,
17
+ "border-b border-white/5 last:border-b-0"
18
+ ),
19
+ children: [
20
+ /* @__PURE__ */ jsx("div", { className: "w-6 h-6 rounded-lg bg-muted/20 animate-pulse" }),
21
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-2", children: [
22
+ /* @__PURE__ */ jsx("div", { className: "h-4 bg-muted/20 rounded animate-pulse w-3/4" }),
23
+ variant !== "compact" && /* @__PURE__ */ jsx("div", { className: "h-3 bg-muted/10 rounded animate-pulse w-1/2" })
24
+ ] }),
25
+ /* @__PURE__ */ jsx("div", { className: "w-16 h-7 bg-muted/20 rounded-lg animate-pulse" })
26
+ ]
27
+ },
28
+ i
29
+ )) });
30
+ }
31
+ function DataTableEmpty({
32
+ message,
33
+ icon,
34
+ IconComponent
35
+ }) {
36
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center h-full py-12 px-4", children: [
37
+ /* @__PURE__ */ jsx("div", { className: "p-4 rounded-2xl bg-muted/10 mb-4", children: IconComponent && /* @__PURE__ */ jsx(IconComponent, { name: icon, size: "2.5rem", className: "text-muted-foreground" }) }),
38
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-muted-foreground text-center max-w-xs", children: message })
39
+ ] });
40
+ }
41
+ function DataTableHeader({
42
+ title,
43
+ titleIcon,
44
+ itemCount,
45
+ actions,
46
+ IconComponent,
47
+ HeadingComponent
48
+ }) {
49
+ const [showActions, setShowActions] = useState(false);
50
+ const TitleComponent = HeadingComponent || (({ children, className }) => /* @__PURE__ */ jsx("h4", { className, children }));
51
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between w-full pb-4 border-b border-white/10", children: [
52
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 w-full", children: [
53
+ titleIcon && IconComponent && /* @__PURE__ */ jsx("div", { className: "p-2 rounded-lg bg-muted/10", children: /* @__PURE__ */ jsx(IconComponent, { name: titleIcon, size: "1.25rem", className: "text-foreground" }) }),
54
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 w-full", children: [
55
+ /* @__PURE__ */ jsx(TitleComponent, { size: 4, className: "font-semibold", children: title }),
56
+ /* @__PURE__ */ jsx("span", { className: "px-2.5 py-0.5 ml-auto rounded-md bg-muted/20 text-muted-foreground text-xs font-medium", children: itemCount })
57
+ ] })
58
+ ] }),
59
+ actions && actions.length > 0 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
60
+ /* @__PURE__ */ jsx(
61
+ "button",
62
+ {
63
+ onClick: () => setShowActions(!showActions),
64
+ className: cn(
65
+ "p-2 rounded-lg",
66
+ "hover:bg-muted/20",
67
+ "active:scale-95",
68
+ "transition-all duration-200",
69
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sapphire-500"
70
+ ),
71
+ "aria-label": "Actions",
72
+ children: IconComponent && /* @__PURE__ */ jsx(IconComponent, { name: "MoreVertical", size: "1rem", className: "text-muted-foreground" })
73
+ }
74
+ ),
75
+ showActions && /* @__PURE__ */ jsx(
76
+ "div",
77
+ {
78
+ className: cn(
79
+ "absolute right-0 top-full mt-2 z-10",
80
+ "min-w-[160px]",
81
+ "bg-background-50/95 backdrop-blur-xl backdrop-saturate-150",
82
+ "border border-white/10",
83
+ "rounded-xl shadow-2xl shadow-black/20",
84
+ "py-1"
85
+ ),
86
+ children: actions.map((action, i) => /* @__PURE__ */ jsxs(
87
+ "button",
88
+ {
89
+ onClick: () => {
90
+ action.onClick();
91
+ setShowActions(false);
92
+ },
93
+ className: cn(
94
+ "w-full flex items-center gap-3 px-3 py-2",
95
+ "text-sm font-medium text-foreground",
96
+ "hover:bg-muted/20",
97
+ "transition-colors duration-150"
98
+ ),
99
+ children: [
100
+ action.icon && IconComponent && /* @__PURE__ */ jsx(IconComponent, { name: action.icon, size: "0.875rem" }),
101
+ action.label
102
+ ]
103
+ },
104
+ i
105
+ ))
106
+ }
107
+ )
108
+ ] })
109
+ ] });
110
+ }
111
+ function DataTableRow({
112
+ item,
113
+ maxCount,
114
+ variant,
115
+ showMiniChart,
116
+ onItemClick,
117
+ isFocused,
118
+ onFocus,
119
+ IconComponent
120
+ }) {
121
+ const barWidthPercent = maxCount > 0 ? item.count / maxCount * 100 : 0;
122
+ const badgeColors = {
123
+ default: "bg-muted/20 text-foreground",
124
+ sapphire: "bg-sapphire-50/80 text-sapphire-700",
125
+ emerald: "bg-emerald-50/80 text-emerald-700",
126
+ coral: "bg-coral-50/80 text-coral-700",
127
+ amethyst: "bg-amethyst-50/80 text-amethyst-700",
128
+ lapis: "bg-lapis-50/80 text-lapis-700",
129
+ sky: "bg-sky-50/80 text-sky-700"
130
+ };
131
+ const rowHeightClass = variant === "compact" ? "min-h-11" : variant === "rich" ? "min-h-14" : "min-h-18";
132
+ const rowContent = /* @__PURE__ */ jsxs(Fragment, { children: [
133
+ item.icon && IconComponent && /* @__PURE__ */ jsx("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsx(
134
+ "div",
135
+ {
136
+ className: cn(
137
+ "flex items-center justify-center",
138
+ variant === "feature" ? "w-10 h-10" : "w-6 h-6",
139
+ "rounded-lg",
140
+ variant === "feature" && "bg-muted/10"
141
+ ),
142
+ children: /* @__PURE__ */ jsx(
143
+ IconComponent,
144
+ {
145
+ name: item.icon,
146
+ size: variant === "feature" ? "1.5rem" : "1rem",
147
+ className: item.iconColor || "text-muted-foreground"
148
+ }
149
+ )
150
+ }
151
+ ) }),
152
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 flex flex-col justify-center", children: [
153
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(
154
+ "span",
155
+ {
156
+ className: cn(
157
+ "font-medium truncate",
158
+ variant === "compact" ? "text-sm" : "text-base"
159
+ ),
160
+ children: item.label || "(No label)"
161
+ }
162
+ ) }),
163
+ item.secondaryText && variant !== "compact" && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground truncate mt-0.5", children: item.secondaryText }),
164
+ item.tertiaryText && variant === "feature" && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground truncate mt-0.5", children: item.tertiaryText })
165
+ ] }),
166
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [
167
+ showMiniChart && variant !== "compact" && /* @__PURE__ */ jsx("div", { className: "w-20 h-2 bg-muted/20 rounded-full overflow-hidden", children: /* @__PURE__ */ jsx(
168
+ "div",
169
+ {
170
+ className: "h-full bg-gradient-to-r from-sapphire-500 to-sapphire-600 transition-all duration-300",
171
+ style: { width: `${barWidthPercent}%` }
172
+ }
173
+ ) }),
174
+ /* @__PURE__ */ jsx(
175
+ "div",
176
+ {
177
+ className: cn(
178
+ "px-3 py-1 rounded-lg",
179
+ "text-xs font-semibold",
180
+ "min-w-[3.5rem] text-center",
181
+ "transition-transform duration-200",
182
+ "group-hover:scale-105",
183
+ badgeColors[item.badgeVariant || "default"]
184
+ ),
185
+ children: item.count
186
+ }
187
+ ),
188
+ (item.showChevron || item.href) && IconComponent && /* @__PURE__ */ jsx(
189
+ IconComponent,
190
+ {
191
+ name: "ChevronRight",
192
+ size: "1rem",
193
+ className: "text-muted-foreground opacity-50 group-hover:opacity-100 transition-opacity duration-200"
194
+ }
195
+ )
196
+ ] })
197
+ ] });
198
+ const baseClassName = cn(
199
+ "flex items-center gap-4 px-4 py-3",
200
+ rowHeightClass,
201
+ "border-b border-white/5 last:border-b-0",
202
+ "hover:bg-muted/20",
203
+ "active:scale-[0.98]",
204
+ "transition-all duration-200 ease-out",
205
+ "group",
206
+ isFocused && "ring-2 ring-sapphire-500 ring-inset"
207
+ );
208
+ if (item.href) {
209
+ return /* @__PURE__ */ jsx(
210
+ Link,
211
+ {
212
+ ...item.href,
213
+ className: cn(baseClassName, "cursor-pointer focus-visible:outline-none"),
214
+ "aria-label": `${item.label}, ${item.count} items`,
215
+ onFocus,
216
+ tabIndex: 0,
217
+ children: rowContent
218
+ }
219
+ );
220
+ }
221
+ if (onItemClick) {
222
+ return /* @__PURE__ */ jsx(
223
+ "button",
224
+ {
225
+ onClick: () => onItemClick(item),
226
+ className: cn(baseClassName, "cursor-pointer w-full text-left focus-visible:outline-none"),
227
+ "aria-label": `${item.label}, ${item.count} items`,
228
+ onFocus,
229
+ tabIndex: 0,
230
+ children: rowContent
231
+ }
232
+ );
233
+ }
234
+ return /* @__PURE__ */ jsx(
235
+ "div",
236
+ {
237
+ className: baseClassName,
238
+ "aria-label": `${item.label}, ${item.count} items`,
239
+ tabIndex: 0,
240
+ onFocus,
241
+ children: rowContent
242
+ }
243
+ );
244
+ }
245
+ function DataTable({
246
+ title,
247
+ titleIcon,
248
+ data = [],
249
+ emptyMessage = "No data available",
250
+ emptyIcon = "Inbox",
251
+ variant = "rich",
252
+ showMiniChart = true,
253
+ maxCount: providedMaxCount,
254
+ actions,
255
+ onItemClick,
256
+ className,
257
+ isLoading = false,
258
+ IconComponent,
259
+ HeadingComponent
260
+ }) {
261
+ const [focusedIndex, setFocusedIndex] = useState(-1);
262
+ const scrollAreaRef = useRef(null);
263
+ const maxCount = providedMaxCount ?? (data.length > 0 ? Math.max(...data.map((d) => d.count)) : 1);
264
+ const isEmpty = !data || data.length === 0;
265
+ useEffect(() => {
266
+ const handleKeyDown = (e) => {
267
+ if (!scrollAreaRef.current) return;
268
+ switch (e.key) {
269
+ case "ArrowDown":
270
+ e.preventDefault();
271
+ setFocusedIndex((prev) => Math.min(prev + 1, data.length - 1));
272
+ break;
273
+ case "ArrowUp":
274
+ e.preventDefault();
275
+ setFocusedIndex((prev) => Math.max(prev - 1, 0));
276
+ break;
277
+ case "Enter":
278
+ if (focusedIndex >= 0 && focusedIndex < data.length) {
279
+ const item = data[focusedIndex];
280
+ if (onItemClick) {
281
+ onItemClick(item);
282
+ }
283
+ }
284
+ break;
285
+ }
286
+ };
287
+ const scrollArea = scrollAreaRef.current;
288
+ if (scrollArea) {
289
+ scrollArea.addEventListener("keydown", handleKeyDown);
290
+ return () => scrollArea.removeEventListener("keydown", handleKeyDown);
291
+ }
292
+ }, [focusedIndex, data, onItemClick]);
293
+ return /* @__PURE__ */ jsxs(
294
+ "div",
295
+ {
296
+ className: cn(
297
+ // Solid background with soft shadow (no glassmorphism)
298
+ "bg-background",
299
+ "border border-white/10",
300
+ "rounded-2xl p-6",
301
+ "shadow-xl shadow-black/5",
302
+ "h-[50dvh] flex flex-col",
303
+ "transition-shadow duration-300",
304
+ "hover:shadow-2xl",
305
+ className
306
+ ),
307
+ children: [
308
+ /* @__PURE__ */ jsx(
309
+ DataTableHeader,
310
+ {
311
+ title,
312
+ titleIcon,
313
+ itemCount: data.length,
314
+ actions,
315
+ IconComponent,
316
+ HeadingComponent
317
+ }
318
+ ),
319
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-hidden mt-4", children: isLoading ? /* @__PURE__ */ jsx(
320
+ "div",
321
+ {
322
+ ref: scrollAreaRef,
323
+ className: cn(
324
+ "h-full overflow-y-auto",
325
+ "pr-2",
326
+ // Custom scrollbar
327
+ "[&::-webkit-scrollbar]:w-2",
328
+ "[&::-webkit-scrollbar-track]:bg-transparent",
329
+ "[&::-webkit-scrollbar-thumb]:bg-muted/20",
330
+ "[&::-webkit-scrollbar-thumb]:rounded-full",
331
+ "[&::-webkit-scrollbar-thumb:hover]:bg-muted/30"
332
+ ),
333
+ children: /* @__PURE__ */ jsx(DataTableSkeleton, { variant })
334
+ }
335
+ ) : isEmpty ? /* @__PURE__ */ jsx(DataTableEmpty, { message: emptyMessage, icon: emptyIcon, IconComponent }) : /* @__PURE__ */ jsx(
336
+ "div",
337
+ {
338
+ ref: scrollAreaRef,
339
+ className: cn(
340
+ "h-full overflow-y-auto",
341
+ "pr-2",
342
+ // Custom scrollbar
343
+ "[&::-webkit-scrollbar]:w-2",
344
+ "[&::-webkit-scrollbar-track]:bg-transparent",
345
+ "[&::-webkit-scrollbar-thumb]:bg-muted/20",
346
+ "[&::-webkit-scrollbar-thumb]:rounded-full",
347
+ "[&::-webkit-scrollbar-thumb:hover]:bg-muted/30"
348
+ ),
349
+ tabIndex: 0,
350
+ children: /* @__PURE__ */ jsx("div", { className: "space-y-0", children: data.map((item, index) => /* @__PURE__ */ jsx(
351
+ DataTableRow,
352
+ {
353
+ item,
354
+ maxCount,
355
+ variant,
356
+ showMiniChart,
357
+ onItemClick,
358
+ isFocused: focusedIndex === index,
359
+ onFocus: () => setFocusedIndex(index),
360
+ IconComponent
361
+ },
362
+ `${item.label}-${index}`
363
+ )) })
364
+ }
365
+ ) })
366
+ ]
367
+ }
368
+ );
369
+ }
370
+ export {
371
+ DataTable
372
+ };