@cfast/ui 0.0.1 → 0.2.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.
Files changed (51) hide show
  1. package/README.md +23 -23
  2. package/dist/chunk-PWBG6CGF.js +1400 -0
  3. package/dist/{permission-gate-DVmY42oz.d.ts → client-CIx8_tmv.d.ts} +617 -2
  4. package/dist/client.d.ts +4 -617
  5. package/dist/client.js +6 -8
  6. package/dist/index.d.ts +52 -5
  7. package/dist/index.js +17 -13
  8. package/llms.txt +159 -0
  9. package/package.json +25 -41
  10. package/LICENSE +0 -21
  11. package/dist/chunk-755IRYDN.js +0 -941
  12. package/dist/chunk-7SNK37GF.js +0 -418
  13. package/dist/chunk-ASMYTWTR.js +0 -356
  14. package/dist/chunk-B2XXH5V4.js +0 -66
  15. package/dist/chunk-BQMXYYEV.js +0 -348
  16. package/dist/chunk-DTKBXCTU.js +0 -211
  17. package/dist/chunk-EYIBATYR.js +0 -33
  18. package/dist/chunk-FPZAQ2YQ.js +0 -474
  19. package/dist/chunk-G2OU4BYC.js +0 -205
  20. package/dist/chunk-JEGEIQ3R.js +0 -925
  21. package/dist/chunk-JUNLQJ6H.js +0 -1013
  22. package/dist/chunk-NRGMW3JA.js +0 -906
  23. package/dist/chunk-Q6FPL2OJ.js +0 -1086
  24. package/dist/chunk-QHWAGKNW.js +0 -456
  25. package/dist/chunk-QZT62CGJ.js +0 -924
  26. package/dist/chunk-RDTUEOLK.js +0 -486
  27. package/dist/chunk-RESL4IJJ.js +0 -112
  28. package/dist/chunk-UDCWQUTR.js +0 -221
  29. package/dist/chunk-UE7PZOIJ.js +0 -11
  30. package/dist/chunk-UTZTHGNE.js +0 -84
  31. package/dist/chunk-UVRXMOX5.js +0 -439
  32. package/dist/chunk-XFD3N2D4.js +0 -161
  33. package/dist/client-CXIHCQtA.d.ts +0 -274
  34. package/dist/joy.d.ts +0 -199
  35. package/dist/joy.js +0 -1150
  36. package/dist/permission-gate-apt9T9Mu.d.ts +0 -1256
  37. package/dist/types-1bAiH2uK.d.ts +0 -392
  38. package/dist/types-BX6u5sAd.d.ts +0 -403
  39. package/dist/types-BpdY7w5l.d.ts +0 -403
  40. package/dist/types-BrepeVp8.d.ts +0 -403
  41. package/dist/types-BvAqMZhn.d.ts +0 -403
  42. package/dist/types-C74nSscq.d.ts +0 -403
  43. package/dist/types-DD1Cpx8F.d.ts +0 -403
  44. package/dist/types-DHUhQwJn.d.ts +0 -403
  45. package/dist/types-DZSJNt_M.d.ts +0 -392
  46. package/dist/types-DaaJiIjW.d.ts +0 -391
  47. package/dist/types-LUpWJwps.d.ts +0 -403
  48. package/dist/types-a7zVU6WE.d.ts +0 -394
  49. package/dist/types-biJTHMcH.d.ts +0 -403
  50. package/dist/types-ow_qSEYJ.d.ts +0 -392
  51. package/dist/types-wnLasZaB.d.ts +0 -1234
@@ -0,0 +1,1400 @@
1
+ // src/plugin.tsx
2
+ import { createContext, useContext } from "react";
3
+
4
+ // src/headless-defaults.tsx
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ var headlessDefaults = {
7
+ // Actions
8
+ button: ({ children, onClick, disabled, loading, type }) => /* @__PURE__ */ jsx("button", { onClick, disabled: disabled || loading, type: type ?? "button", children: loading ? "Loading..." : children }),
9
+ tooltip: ({ children, title }) => /* @__PURE__ */ jsx("span", { title, children }),
10
+ confirmDialog: ({ open, onClose, onConfirm, title, description, confirmLabel, cancelLabel }) => open ? /* @__PURE__ */ jsxs("dialog", { open: true, children: [
11
+ /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("strong", { children: title }) }),
12
+ description ? /* @__PURE__ */ jsx("p", { children: description }) : null,
13
+ /* @__PURE__ */ jsxs("div", { children: [
14
+ /* @__PURE__ */ jsx("button", { onClick: onClose, children: cancelLabel ?? "Cancel" }),
15
+ /* @__PURE__ */ jsx("button", { onClick: onConfirm, children: confirmLabel ?? "Confirm" })
16
+ ] })
17
+ ] }) : null,
18
+ // Data display
19
+ table: ({ children }) => /* @__PURE__ */ jsx("table", { children }),
20
+ tableHead: ({ children }) => /* @__PURE__ */ jsx("thead", { children }),
21
+ tableBody: ({ children }) => /* @__PURE__ */ jsx("tbody", { children }),
22
+ tableRow: ({ children, onClick }) => /* @__PURE__ */ jsx("tr", { onClick, children }),
23
+ tableCell: ({ children, header, sortable, sortDirection, onSort }) => {
24
+ const Tag = header ? "th" : "td";
25
+ return /* @__PURE__ */ jsxs(
26
+ Tag,
27
+ {
28
+ onClick: sortable ? onSort : void 0,
29
+ style: sortable ? { cursor: "pointer" } : void 0,
30
+ children: [
31
+ children,
32
+ sortable && sortDirection ? sortDirection === "asc" ? " \u2191" : " \u2193" : null
33
+ ]
34
+ }
35
+ );
36
+ },
37
+ chip: ({ children, size }) => /* @__PURE__ */ jsx(
38
+ "span",
39
+ {
40
+ style: {
41
+ display: "inline-block",
42
+ padding: size === "sm" ? "1px 6px" : "2px 8px",
43
+ borderRadius: "12px",
44
+ fontSize: size === "sm" ? "12px" : "14px",
45
+ backgroundColor: "#eee"
46
+ },
47
+ children
48
+ }
49
+ ),
50
+ // Layout
51
+ appShell: ({ children, sidebar, header }) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", minHeight: "100vh" }, children: [
52
+ sidebar ? /* @__PURE__ */ jsx("nav", { children: sidebar }) : null,
53
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
54
+ header ?? null,
55
+ /* @__PURE__ */ jsx("main", { children })
56
+ ] })
57
+ ] }),
58
+ sidebar: ({ children }) => /* @__PURE__ */ jsx("aside", { style: { width: "240px", borderRight: "1px solid #ddd" }, children }),
59
+ pageContainer: ({ children, title, actions }) => /* @__PURE__ */ jsxs("div", { children: [
60
+ title || actions ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
61
+ title ? /* @__PURE__ */ jsx("h1", { children: title }) : null,
62
+ actions ?? null
63
+ ] }) : null,
64
+ children
65
+ ] }),
66
+ breadcrumb: ({ items }) => /* @__PURE__ */ jsx("nav", { "aria-label": "breadcrumb", children: items.map((item, i) => /* @__PURE__ */ jsxs("span", { children: [
67
+ i > 0 ? " / " : null,
68
+ item.to ? /* @__PURE__ */ jsx("a", { href: item.to, children: item.label }) : item.label
69
+ ] }, i)) }),
70
+ // Feedback
71
+ toast: ({ children }) => /* @__PURE__ */ jsx("div", { children }),
72
+ alert: ({ children, color }) => /* @__PURE__ */ jsx(
73
+ "div",
74
+ {
75
+ role: "alert",
76
+ style: {
77
+ padding: "8px 12px",
78
+ borderRadius: "4px",
79
+ backgroundColor: color === "danger" ? "#fee" : color === "success" ? "#efe" : color === "warning" ? "#ffe" : "#f5f5f5"
80
+ },
81
+ children
82
+ }
83
+ ),
84
+ // File
85
+ dropZone: ({ children, isDragOver, onClick, onDrop, onDragOver, onDragLeave }) => /* @__PURE__ */ jsx(
86
+ "div",
87
+ {
88
+ onClick,
89
+ onDrop: (e) => {
90
+ e.preventDefault();
91
+ onDrop(e.dataTransfer.files);
92
+ },
93
+ onDragOver: (e) => {
94
+ e.preventDefault();
95
+ onDragOver(e);
96
+ },
97
+ onDragLeave,
98
+ style: {
99
+ border: `2px dashed ${isDragOver ? "#4caf50" : "#ccc"}`,
100
+ borderRadius: "8px",
101
+ padding: "32px",
102
+ textAlign: "center",
103
+ cursor: "pointer"
104
+ },
105
+ children
106
+ }
107
+ )
108
+ };
109
+
110
+ // src/plugin.tsx
111
+ import { jsx as jsx2 } from "react/jsx-runtime";
112
+ var UIPluginContext = createContext(null);
113
+ function createUIPlugin(config) {
114
+ return { components: config.components };
115
+ }
116
+ function UIPluginProvider({
117
+ plugin,
118
+ children
119
+ }) {
120
+ return /* @__PURE__ */ jsx2(UIPluginContext.Provider, { value: plugin, children });
121
+ }
122
+ function useUIPlugin() {
123
+ return useContext(UIPluginContext);
124
+ }
125
+ function useComponent(slot) {
126
+ const plugin = useUIPlugin();
127
+ const merged = { ...headlessDefaults, ...plugin?.components };
128
+ return merged[slot];
129
+ }
130
+
131
+ // src/hooks/use-confirm.ts
132
+ import { createContext as createContext2, useContext as useContext2, useCallback } from "react";
133
+ var ConfirmContext = createContext2(null);
134
+ function useConfirm() {
135
+ const ctx = useContext2(ConfirmContext);
136
+ if (!ctx) {
137
+ throw new Error("useConfirm must be used within a <ConfirmProvider>");
138
+ }
139
+ return useCallback(
140
+ (options) => ctx.confirm(options),
141
+ [ctx]
142
+ );
143
+ }
144
+
145
+ // src/hooks/use-toast.ts
146
+ import { createContext as createContext3, useContext as useContext3, useCallback as useCallback2 } from "react";
147
+ var ToastContext = createContext3(null);
148
+ function useToast() {
149
+ const ctx = useContext3(ToastContext);
150
+ if (!ctx) {
151
+ throw new Error("useToast must be used within a <ToastProvider>");
152
+ }
153
+ const show = useCallback2(
154
+ (options) => ctx.show(options),
155
+ [ctx]
156
+ );
157
+ const success = useCallback2(
158
+ (message, description) => ctx.show({ message, type: "success", description }),
159
+ [ctx]
160
+ );
161
+ const error = useCallback2(
162
+ (message, description) => ctx.show({ message, type: "error", description }),
163
+ [ctx]
164
+ );
165
+ const info = useCallback2(
166
+ (message, description) => ctx.show({ message, type: "info", description }),
167
+ [ctx]
168
+ );
169
+ const warning = useCallback2(
170
+ (message, description) => ctx.show({ message, type: "warning", description }),
171
+ [ctx]
172
+ );
173
+ return { show, success, error, info, warning };
174
+ }
175
+
176
+ // src/hooks/use-action-toast.ts
177
+ import { useEffect, useRef } from "react";
178
+ import { useActions } from "@cfast/actions/client";
179
+ function useActionToast(descriptor, config) {
180
+ const actions = useActions(descriptor);
181
+ const toast = useToast();
182
+ const prevDataRef = useRef({});
183
+ useEffect(() => {
184
+ for (const [name, cfg] of Object.entries(config)) {
185
+ const actionFn = actions[name];
186
+ if (!actionFn) continue;
187
+ const result = actionFn();
188
+ const prevData = prevDataRef.current[name];
189
+ if (result.data !== void 0 && result.data !== prevData) {
190
+ prevDataRef.current[name] = result.data;
191
+ if (cfg.success) {
192
+ toast.success(cfg.success);
193
+ }
194
+ }
195
+ if (result.error !== void 0 && result.error !== prevData) {
196
+ prevDataRef.current[name] = result.error;
197
+ if (cfg.error) {
198
+ toast.error(cfg.error);
199
+ }
200
+ }
201
+ }
202
+ });
203
+ }
204
+
205
+ // src/components/permission-gate.tsx
206
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
207
+ function PermissionGate({
208
+ action,
209
+ children,
210
+ fallback
211
+ }) {
212
+ if (action.invisible) {
213
+ return null;
214
+ }
215
+ if (!action.permitted) {
216
+ return fallback ? /* @__PURE__ */ jsx3(Fragment, { children: fallback }) : null;
217
+ }
218
+ return /* @__PURE__ */ jsx3(Fragment, { children });
219
+ }
220
+
221
+ // src/components/action-button.tsx
222
+ import { jsx as jsx4 } from "react/jsx-runtime";
223
+ function ActionButton({
224
+ action,
225
+ children,
226
+ whenForbidden = "disable",
227
+ confirmation: _confirmation,
228
+ ...buttonProps
229
+ }) {
230
+ const Button = useComponent("button");
231
+ if (action.invisible) {
232
+ return null;
233
+ }
234
+ if (!action.permitted && whenForbidden === "hide") {
235
+ return null;
236
+ }
237
+ const disabled = !action.permitted && whenForbidden === "disable";
238
+ return /* @__PURE__ */ jsx4(
239
+ Button,
240
+ {
241
+ ...buttonProps,
242
+ onClick: () => action.submit(),
243
+ disabled,
244
+ loading: action.pending,
245
+ children
246
+ }
247
+ );
248
+ }
249
+
250
+ // src/components/confirm-provider.tsx
251
+ import { useState, useCallback as useCallback3, useRef as useRef2 } from "react";
252
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
253
+ function ConfirmProvider({ children }) {
254
+ const [state, setState] = useState(null);
255
+ const ConfirmDialog = useComponent("confirmDialog");
256
+ const resolveRef = useRef2(null);
257
+ const confirm = useCallback3((options) => {
258
+ return new Promise((resolve) => {
259
+ resolveRef.current = resolve;
260
+ setState({ ...options, resolve });
261
+ });
262
+ }, []);
263
+ const handleClose = useCallback3(() => {
264
+ resolveRef.current?.(false);
265
+ resolveRef.current = null;
266
+ setState(null);
267
+ }, []);
268
+ const handleConfirm = useCallback3(() => {
269
+ resolveRef.current?.(true);
270
+ resolveRef.current = null;
271
+ setState(null);
272
+ }, []);
273
+ return /* @__PURE__ */ jsxs2(ConfirmContext.Provider, { value: { confirm }, children: [
274
+ children,
275
+ state ? /* @__PURE__ */ jsx5(
276
+ ConfirmDialog,
277
+ {
278
+ open: true,
279
+ onClose: handleClose,
280
+ onConfirm: handleConfirm,
281
+ title: state.title,
282
+ description: state.description,
283
+ confirmLabel: state.confirmLabel,
284
+ cancelLabel: state.cancelLabel,
285
+ variant: state.variant
286
+ }
287
+ ) : null
288
+ ] });
289
+ }
290
+
291
+ // src/components/form-status.tsx
292
+ import { jsx as jsx6 } from "react/jsx-runtime";
293
+ function FormStatus({ data }) {
294
+ const Alert = useComponent("alert");
295
+ if (!data) return null;
296
+ const elements = [];
297
+ if (data.success) {
298
+ elements.push(
299
+ /* @__PURE__ */ jsx6(Alert, { color: "success", children: data.success }, "success")
300
+ );
301
+ }
302
+ if (data.error) {
303
+ elements.push(
304
+ /* @__PURE__ */ jsx6(Alert, { color: "danger", children: data.error }, "error")
305
+ );
306
+ }
307
+ if (data.fieldErrors) {
308
+ const errorMessages = Object.entries(data.fieldErrors).flatMap(
309
+ ([field, errors]) => errors.map((err) => `${field}: ${err}`)
310
+ );
311
+ if (errorMessages.length > 0) {
312
+ elements.push(
313
+ /* @__PURE__ */ jsx6(Alert, { color: "danger", children: /* @__PURE__ */ jsx6("ul", { style: { margin: 0, paddingLeft: "16px" }, children: errorMessages.map((msg, i) => /* @__PURE__ */ jsx6("li", { children: msg }, i)) }) }, "field-errors")
314
+ );
315
+ }
316
+ }
317
+ if (elements.length === 0) return null;
318
+ return /* @__PURE__ */ jsx6("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: elements });
319
+ }
320
+
321
+ // src/components/avatar-with-initials.tsx
322
+ import { jsx as jsx7 } from "react/jsx-runtime";
323
+ function getInitials(name) {
324
+ return name.split(" ").map((part) => part[0]).join("").toUpperCase().slice(0, 2);
325
+ }
326
+ var sizeMap = { sm: 32, md: 40, lg: 56 };
327
+ function AvatarWithInitials({
328
+ src,
329
+ name,
330
+ size = "md"
331
+ }) {
332
+ const px = sizeMap[size];
333
+ if (src) {
334
+ return /* @__PURE__ */ jsx7(
335
+ "img",
336
+ {
337
+ src,
338
+ alt: name,
339
+ style: {
340
+ width: px,
341
+ height: px,
342
+ borderRadius: "50%",
343
+ objectFit: "cover"
344
+ }
345
+ }
346
+ );
347
+ }
348
+ return /* @__PURE__ */ jsx7(
349
+ "span",
350
+ {
351
+ "aria-label": name,
352
+ style: {
353
+ display: "inline-flex",
354
+ alignItems: "center",
355
+ justifyContent: "center",
356
+ width: px,
357
+ height: px,
358
+ borderRadius: "50%",
359
+ backgroundColor: "#ddd",
360
+ fontSize: px * 0.4,
361
+ fontWeight: "bold"
362
+ },
363
+ children: getInitials(name)
364
+ }
365
+ );
366
+ }
367
+
368
+ // src/components/role-badge.tsx
369
+ import { jsx as jsx8 } from "react/jsx-runtime";
370
+ var defaultColors = {
371
+ admin: "danger",
372
+ editor: "primary",
373
+ author: "success",
374
+ reader: "neutral"
375
+ };
376
+ function RoleBadge({ role, colors }) {
377
+ const Chip = useComponent("chip");
378
+ const colorMap = colors ? { ...defaultColors, ...Object.fromEntries(
379
+ Object.entries(colors).map(([k, v]) => [k, v])
380
+ ) } : defaultColors;
381
+ const chipColor = colorMap[role] ?? "neutral";
382
+ return /* @__PURE__ */ jsx8(Chip, { color: chipColor, variant: "soft", size: "sm", children: role });
383
+ }
384
+
385
+ // src/components/impersonation-banner.tsx
386
+ import { useCurrentUser } from "@cfast/auth/client";
387
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
388
+ function ImpersonationBanner({
389
+ stopAction = "/admin/stop-impersonation"
390
+ }) {
391
+ const user = useCurrentUser();
392
+ const Alert = useComponent("alert");
393
+ const Button = useComponent("button");
394
+ if (!user?.isImpersonating) {
395
+ return null;
396
+ }
397
+ return /* @__PURE__ */ jsx9(Alert, { color: "warning", children: /* @__PURE__ */ jsxs3(
398
+ "div",
399
+ {
400
+ style: {
401
+ display: "flex",
402
+ alignItems: "center",
403
+ justifyContent: "center",
404
+ gap: "12px"
405
+ },
406
+ children: [
407
+ /* @__PURE__ */ jsx9("strong", { children: `Viewing as ${user.name} (${user.email})` }),
408
+ /* @__PURE__ */ jsx9("form", { method: "post", action: stopAction, children: /* @__PURE__ */ jsx9(Button, { type: "submit", variant: "outlined", size: "sm", children: "Stop Impersonating" }) })
409
+ ]
410
+ }
411
+ ) });
412
+ }
413
+
414
+ // src/record-access.ts
415
+ function getField(obj, key) {
416
+ if (typeof obj !== "object" || obj === null) {
417
+ return void 0;
418
+ }
419
+ return obj[key];
420
+ }
421
+ function getRecordId(obj) {
422
+ const id = getField(obj, "id");
423
+ if (typeof id === "string" || typeof id === "number") {
424
+ return id;
425
+ }
426
+ return 0;
427
+ }
428
+
429
+ // src/components/data-table.tsx
430
+ import { useState as useState2, useCallback as useCallback4 } from "react";
431
+ import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
432
+ function normalizeColumns(columns) {
433
+ if (!columns) return [];
434
+ return columns.map((col) => {
435
+ if (typeof col === "string") {
436
+ return {
437
+ key: col,
438
+ label: col.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim(),
439
+ sortable: true
440
+ };
441
+ }
442
+ return col;
443
+ });
444
+ }
445
+ function DataTable({
446
+ data,
447
+ columns: columnsProp,
448
+ selectable = false,
449
+ selectedRows: externalSelectedRows,
450
+ onSelectionChange,
451
+ onRowClick,
452
+ getRowId,
453
+ emptyMessage = "No data"
454
+ }) {
455
+ const Table = useComponent("table");
456
+ const TableHead = useComponent("tableHead");
457
+ const TableBody = useComponent("tableBody");
458
+ const TableRow = useComponent("tableRow");
459
+ const TableCell = useComponent("tableCell");
460
+ const columns = normalizeColumns(columnsProp);
461
+ const [sortKey, setSortKey] = useState2(null);
462
+ const [sortDir, setSortDir] = useState2("asc");
463
+ const [internalSelected, setInternalSelected] = useState2(/* @__PURE__ */ new Set());
464
+ const selectedSet = externalSelectedRows ? new Set(externalSelectedRows.map((r) => (getRowId ?? defaultGetId)(r))) : internalSelected;
465
+ const handleSort = useCallback4((key) => {
466
+ if (sortKey === key) {
467
+ setSortDir((d) => d === "asc" ? "desc" : "asc");
468
+ } else {
469
+ setSortKey(key);
470
+ setSortDir("asc");
471
+ }
472
+ }, [sortKey]);
473
+ const toggleRow = useCallback4((id) => {
474
+ if (onSelectionChange) {
475
+ const row = data.items.find((r) => (getRowId ?? defaultGetId)(r) === id);
476
+ if (!row) return;
477
+ const current = externalSelectedRows ?? [];
478
+ const isSelected = current.some((r) => (getRowId ?? defaultGetId)(r) === id);
479
+ onSelectionChange(isSelected ? current.filter((r) => (getRowId ?? defaultGetId)(r) !== id) : [...current, row]);
480
+ } else {
481
+ setInternalSelected((prev) => {
482
+ const next = new Set(prev);
483
+ if (next.has(id)) next.delete(id);
484
+ else next.add(id);
485
+ return next;
486
+ });
487
+ }
488
+ }, [data.items, externalSelectedRows, onSelectionChange, getRowId]);
489
+ if (data.items.length === 0 && !data.isLoading) {
490
+ return /* @__PURE__ */ jsx10("div", { style: { textAlign: "center", padding: "32px", color: "#666" }, children: emptyMessage });
491
+ }
492
+ return /* @__PURE__ */ jsxs4(Table, { hoverRow: true, children: [
493
+ /* @__PURE__ */ jsx10(TableHead, { children: /* @__PURE__ */ jsxs4(TableRow, { children: [
494
+ selectable ? /* @__PURE__ */ jsx10(TableCell, { header: true, children: "" }) : null,
495
+ columns.map((col) => /* @__PURE__ */ jsx10(
496
+ TableCell,
497
+ {
498
+ header: true,
499
+ sortable: col.sortable !== false,
500
+ sortDirection: sortKey === col.key ? sortDir : null,
501
+ onSort: () => handleSort(col.key),
502
+ children: col.label ?? col.key
503
+ },
504
+ col.key
505
+ ))
506
+ ] }) }),
507
+ /* @__PURE__ */ jsx10(TableBody, { children: data.items.map((row) => {
508
+ const id = (getRowId ?? defaultGetId)(row);
509
+ const isSelected = selectedSet.has(id);
510
+ return /* @__PURE__ */ jsxs4(
511
+ TableRow,
512
+ {
513
+ selected: isSelected,
514
+ onClick: onRowClick ? () => onRowClick(row) : void 0,
515
+ children: [
516
+ selectable ? /* @__PURE__ */ jsx10(TableCell, { children: /* @__PURE__ */ jsx10(
517
+ "input",
518
+ {
519
+ type: "checkbox",
520
+ checked: isSelected,
521
+ onChange: () => toggleRow(id)
522
+ }
523
+ ) }) : null,
524
+ columns.map((col) => {
525
+ const value = getField(row, col.key);
526
+ return /* @__PURE__ */ jsx10(TableCell, { children: col.render ? col.render(value, row) : String(value ?? "") }, col.key);
527
+ })
528
+ ]
529
+ },
530
+ String(id)
531
+ );
532
+ }) })
533
+ ] });
534
+ }
535
+ function defaultGetId(row) {
536
+ return getRecordId(row);
537
+ }
538
+
539
+ // src/components/filter-bar.tsx
540
+ import { useCallback as useCallback5 } from "react";
541
+ import { useSearchParams, useNavigate, useLocation } from "react-router";
542
+ import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
543
+ function FilterBar({
544
+ filters,
545
+ searchable
546
+ }) {
547
+ const [searchParams] = useSearchParams();
548
+ const navigate = useNavigate();
549
+ const location = useLocation();
550
+ const updateParam = useCallback5(
551
+ (key, value) => {
552
+ const params = new URLSearchParams(searchParams);
553
+ if (value === null || value === "") {
554
+ params.delete(key);
555
+ } else {
556
+ params.set(key, value);
557
+ }
558
+ params.delete("page");
559
+ params.delete("cursor");
560
+ navigate(`${location.pathname}?${params.toString()}`);
561
+ },
562
+ [searchParams, navigate, location.pathname]
563
+ );
564
+ return /* @__PURE__ */ jsxs5(
565
+ "div",
566
+ {
567
+ style: {
568
+ display: "flex",
569
+ gap: "8px",
570
+ flexWrap: "wrap",
571
+ alignItems: "center",
572
+ marginBottom: "16px"
573
+ },
574
+ children: [
575
+ searchable && searchable.length > 0 ? /* @__PURE__ */ jsx11(
576
+ "input",
577
+ {
578
+ type: "search",
579
+ placeholder: `Search ${searchable.join(", ")}...`,
580
+ value: searchParams.get("q") ?? "",
581
+ onChange: (e) => updateParam("q", e.target.value || null),
582
+ style: { padding: "6px 10px", border: "1px solid #ccc", borderRadius: "4px" }
583
+ }
584
+ ) : null,
585
+ filters.map((filter) => /* @__PURE__ */ jsx11(
586
+ FilterInput,
587
+ {
588
+ filter,
589
+ value: searchParams.get(filter.column) ?? "",
590
+ onChange: (value) => updateParam(filter.column, value || null)
591
+ },
592
+ filter.column
593
+ ))
594
+ ]
595
+ }
596
+ );
597
+ }
598
+ function FilterInput({
599
+ filter,
600
+ value,
601
+ onChange
602
+ }) {
603
+ const label = filter.label ?? filter.column;
604
+ switch (filter.type) {
605
+ case "select":
606
+ case "boolean":
607
+ return /* @__PURE__ */ jsxs5(
608
+ "select",
609
+ {
610
+ value,
611
+ onChange: (e) => onChange(e.target.value),
612
+ "aria-label": label,
613
+ children: [
614
+ /* @__PURE__ */ jsx11("option", { value: "", children: `All ${label}` }),
615
+ (filter.options ?? []).map((opt) => /* @__PURE__ */ jsx11("option", { value: String(opt.value), children: opt.label }, String(opt.value)))
616
+ ]
617
+ }
618
+ );
619
+ case "text":
620
+ default:
621
+ return /* @__PURE__ */ jsx11(
622
+ "input",
623
+ {
624
+ type: "text",
625
+ placeholder: filter.placeholder ?? label,
626
+ value,
627
+ onChange: (e) => onChange(e.target.value),
628
+ "aria-label": label,
629
+ style: { padding: "6px 10px", border: "1px solid #ccc", borderRadius: "4px" }
630
+ }
631
+ );
632
+ }
633
+ }
634
+
635
+ // src/components/bulk-action-bar.tsx
636
+ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
637
+ function BulkActionBar({
638
+ selectedCount,
639
+ actions,
640
+ onAction,
641
+ onClearSelection
642
+ }) {
643
+ const Button = useComponent("button");
644
+ if (selectedCount === 0) return null;
645
+ return /* @__PURE__ */ jsxs6(
646
+ "div",
647
+ {
648
+ style: {
649
+ display: "flex",
650
+ alignItems: "center",
651
+ gap: "8px",
652
+ padding: "8px 16px",
653
+ backgroundColor: "#f0f4ff",
654
+ borderRadius: "4px",
655
+ marginBottom: "8px"
656
+ },
657
+ children: [
658
+ /* @__PURE__ */ jsx12("span", { children: `${selectedCount} selected` }),
659
+ actions.map((action) => {
660
+ const Icon = action.icon;
661
+ return /* @__PURE__ */ jsx12(
662
+ Button,
663
+ {
664
+ onClick: () => onAction(action),
665
+ variant: "soft",
666
+ size: "sm",
667
+ startDecorator: Icon ? /* @__PURE__ */ jsx12(Icon, { className: "bulk-action-icon" }) : void 0,
668
+ children: action.label
669
+ },
670
+ action.label
671
+ );
672
+ }),
673
+ /* @__PURE__ */ jsx12(
674
+ Button,
675
+ {
676
+ onClick: onClearSelection,
677
+ variant: "plain",
678
+ size: "sm",
679
+ children: "Clear"
680
+ }
681
+ )
682
+ ]
683
+ }
684
+ );
685
+ }
686
+
687
+ // src/fields/date-field.tsx
688
+ import { jsx as jsx13 } from "react/jsx-runtime";
689
+ var rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
690
+ function getRelativeTime(date) {
691
+ const now = Date.now();
692
+ const diffMs = date.getTime() - now;
693
+ const diffSec = Math.round(diffMs / 1e3);
694
+ const diffMin = Math.round(diffSec / 60);
695
+ const diffHour = Math.round(diffMin / 60);
696
+ const diffDay = Math.round(diffHour / 24);
697
+ if (Math.abs(diffSec) < 60) return rtf.format(diffSec, "second");
698
+ if (Math.abs(diffMin) < 60) return rtf.format(diffMin, "minute");
699
+ if (Math.abs(diffHour) < 24) return rtf.format(diffHour, "hour");
700
+ if (Math.abs(diffDay) < 30) return rtf.format(diffDay, "day");
701
+ const diffMonth = Math.round(diffDay / 30);
702
+ if (Math.abs(diffMonth) < 12) return rtf.format(diffMonth, "month");
703
+ return rtf.format(Math.round(diffDay / 365), "year");
704
+ }
705
+ function formatDate(date, format, locale) {
706
+ switch (format) {
707
+ case "relative":
708
+ return getRelativeTime(date);
709
+ case "long":
710
+ return new Intl.DateTimeFormat(locale, {
711
+ dateStyle: "long"
712
+ }).format(date);
713
+ case "datetime":
714
+ return new Intl.DateTimeFormat(locale, {
715
+ dateStyle: "medium",
716
+ timeStyle: "short"
717
+ }).format(date);
718
+ case "short":
719
+ default:
720
+ return new Intl.DateTimeFormat(locale, {
721
+ dateStyle: "medium"
722
+ }).format(date);
723
+ }
724
+ }
725
+ function DateField({
726
+ value,
727
+ format = "short",
728
+ locale = "en"
729
+ }) {
730
+ if (value == null) {
731
+ return /* @__PURE__ */ jsx13("span", { children: "\u2014" });
732
+ }
733
+ const date = value instanceof Date ? value : typeof value === "string" || typeof value === "number" ? new Date(value) : new Date(String(value));
734
+ if (Number.isNaN(date.getTime())) {
735
+ return /* @__PURE__ */ jsx13("span", { children: "Invalid date" });
736
+ }
737
+ return /* @__PURE__ */ jsx13("time", { dateTime: date.toISOString(), children: formatDate(date, format, locale) });
738
+ }
739
+
740
+ // src/fields/boolean-field.tsx
741
+ import { jsx as jsx14 } from "react/jsx-runtime";
742
+ function BooleanField({
743
+ value,
744
+ trueLabel = "Yes",
745
+ falseLabel = "No",
746
+ trueColor = "success",
747
+ falseColor = "neutral"
748
+ }) {
749
+ const Chip = useComponent("chip");
750
+ if (value == null) {
751
+ return /* @__PURE__ */ jsx14("span", { children: "\u2014" });
752
+ }
753
+ const boolValue = Boolean(value);
754
+ const color = boolValue ? trueColor : falseColor;
755
+ const chipColor = isChipColor(color) ? color : "neutral";
756
+ return /* @__PURE__ */ jsx14(
757
+ Chip,
758
+ {
759
+ color: chipColor,
760
+ variant: "soft",
761
+ size: "sm",
762
+ children: boolValue ? trueLabel : falseLabel
763
+ }
764
+ );
765
+ }
766
+ var CHIP_COLORS = /* @__PURE__ */ new Set(["success", "neutral", "danger", "primary", "warning"]);
767
+ function isChipColor(value) {
768
+ return CHIP_COLORS.has(value);
769
+ }
770
+
771
+ // src/fields/number-field.tsx
772
+ import { jsx as jsx15 } from "react/jsx-runtime";
773
+ function NumberField({
774
+ value,
775
+ locale = "en",
776
+ currency,
777
+ decimals
778
+ }) {
779
+ if (value == null) {
780
+ return /* @__PURE__ */ jsx15("span", { children: "\u2014" });
781
+ }
782
+ const num = typeof value === "number" ? value : Number(value);
783
+ if (Number.isNaN(num)) {
784
+ return /* @__PURE__ */ jsx15("span", { children: "\u2014" });
785
+ }
786
+ const options = {};
787
+ if (currency) {
788
+ options.style = "currency";
789
+ options.currency = currency;
790
+ }
791
+ if (decimals !== void 0) {
792
+ options.minimumFractionDigits = decimals;
793
+ options.maximumFractionDigits = decimals;
794
+ }
795
+ const formatted = new Intl.NumberFormat(locale, options).format(num);
796
+ return /* @__PURE__ */ jsx15("span", { children: formatted });
797
+ }
798
+
799
+ // src/fields/text-field.tsx
800
+ import { jsx as jsx16 } from "react/jsx-runtime";
801
+ function TextField({
802
+ value,
803
+ maxLength
804
+ }) {
805
+ if (value == null) {
806
+ return /* @__PURE__ */ jsx16("span", { children: "\u2014" });
807
+ }
808
+ const text = String(value);
809
+ const display = maxLength && text.length > maxLength ? `${text.slice(0, maxLength)}\u2026` : text;
810
+ if (maxLength && text.length > maxLength) {
811
+ return /* @__PURE__ */ jsx16("span", { title: text, children: display });
812
+ }
813
+ return /* @__PURE__ */ jsx16("span", { children: display });
814
+ }
815
+
816
+ // src/fields/email-field.tsx
817
+ import { jsx as jsx17 } from "react/jsx-runtime";
818
+ function EmailField({ value }) {
819
+ if (value == null) {
820
+ return /* @__PURE__ */ jsx17("span", { children: "\u2014" });
821
+ }
822
+ const email = String(value);
823
+ return /* @__PURE__ */ jsx17("a", { href: `mailto:${email}`, children: email });
824
+ }
825
+
826
+ // src/fields/json-field.tsx
827
+ import { useState as useState3 } from "react";
828
+ import { jsx as jsx18, jsxs as jsxs7 } from "react/jsx-runtime";
829
+ function JsonField({
830
+ value,
831
+ collapsed = false
832
+ }) {
833
+ const [isCollapsed, setIsCollapsed] = useState3(collapsed);
834
+ if (value == null) {
835
+ return /* @__PURE__ */ jsx18("span", { children: "\u2014" });
836
+ }
837
+ const formatted = JSON.stringify(value, null, 2);
838
+ if (isCollapsed) {
839
+ const preview = JSON.stringify(value);
840
+ const short = preview.length > 60 ? `${preview.slice(0, 60)}\u2026` : preview;
841
+ return /* @__PURE__ */ jsxs7("span", { children: [
842
+ /* @__PURE__ */ jsx18("code", { children: short }),
843
+ " ",
844
+ /* @__PURE__ */ jsx18(
845
+ "button",
846
+ {
847
+ onClick: () => setIsCollapsed(false),
848
+ style: { border: "none", background: "none", cursor: "pointer", color: "#666", fontSize: "12px" },
849
+ children: "expand"
850
+ }
851
+ )
852
+ ] });
853
+ }
854
+ return /* @__PURE__ */ jsx18(
855
+ "pre",
856
+ {
857
+ style: {
858
+ margin: 0,
859
+ fontSize: "13px",
860
+ backgroundColor: "#f5f5f5",
861
+ padding: "8px",
862
+ borderRadius: "4px",
863
+ overflow: "auto"
864
+ },
865
+ children: /* @__PURE__ */ jsx18("code", { children: formatted })
866
+ }
867
+ );
868
+ }
869
+
870
+ // src/fields/field-for-column.ts
871
+ function fieldForColumn(column) {
872
+ const { dataType, name } = column;
873
+ const dt = dataType.toLowerCase();
874
+ if (dt === "boolean" || dt === "integer" && name.startsWith("is")) {
875
+ return BooleanField;
876
+ }
877
+ if (dt.includes("timestamp") || dt.includes("date") || dt.includes("datetime")) {
878
+ return DateField;
879
+ }
880
+ if (dt === "integer" || dt === "real" || dt === "numeric" || dt.includes("int") || dt.includes("float") || dt.includes("double") || dt.includes("decimal")) {
881
+ return NumberField;
882
+ }
883
+ if (name === "email" || name.endsWith("Email")) {
884
+ return EmailField;
885
+ }
886
+ if (dt === "json" || dt === "jsonb" || dt === "blob") {
887
+ return JsonField;
888
+ }
889
+ return TextField;
890
+ }
891
+ function fieldsForTable(table) {
892
+ const result = {};
893
+ for (const [key, col] of Object.entries(table)) {
894
+ if (isColumnMeta(col)) {
895
+ result[key] = fieldForColumn(col);
896
+ }
897
+ }
898
+ return result;
899
+ }
900
+ function isColumnMeta(value) {
901
+ return typeof value === "object" && value !== null && "dataType" in value && typeof value.dataType === "string" && "name" in value && typeof value.name === "string";
902
+ }
903
+
904
+ // src/hooks/use-column-inference.ts
905
+ import { useMemo } from "react";
906
+ function useColumnInference(table, columns) {
907
+ return useMemo(() => {
908
+ if (!table) return [];
909
+ const result = [];
910
+ for (const [key, col] of Object.entries(table)) {
911
+ if (columns && !columns.includes(key)) continue;
912
+ if (!col || typeof col !== "object" || !("dataType" in col) || !("name" in col)) continue;
913
+ const meta = col;
914
+ const field = fieldForColumn(meta);
915
+ const label = key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
916
+ result.push({
917
+ key,
918
+ label,
919
+ sortable: true,
920
+ field
921
+ });
922
+ }
923
+ if (columns) {
924
+ result.sort((a, b) => columns.indexOf(a.key) - columns.indexOf(b.key));
925
+ }
926
+ return result;
927
+ }, [table, columns]);
928
+ }
929
+
930
+ // src/components/drop-zone.tsx
931
+ import { useState as useState4, useCallback as useCallback6, useRef as useRef3 } from "react";
932
+ import { jsx as jsx19, jsxs as jsxs8 } from "react/jsx-runtime";
933
+ function DropZone({
934
+ upload,
935
+ multiple = false,
936
+ children
937
+ }) {
938
+ const DropZoneSlot = useComponent("dropZone");
939
+ const [isDragOver, setIsDragOver] = useState4(false);
940
+ const inputRef = useRef3(null);
941
+ const handleFiles = useCallback6(
942
+ (files) => {
943
+ if (!files || files.length === 0) return;
944
+ if (multiple) {
945
+ for (let i = 0; i < files.length; i++) {
946
+ upload.start(files[i]);
947
+ }
948
+ } else {
949
+ upload.start(files[0]);
950
+ }
951
+ },
952
+ [upload, multiple]
953
+ );
954
+ const handleDrop = useCallback6(
955
+ (files) => {
956
+ setIsDragOver(false);
957
+ handleFiles(files);
958
+ },
959
+ [handleFiles]
960
+ );
961
+ const handleClick = useCallback6(() => {
962
+ inputRef.current?.click();
963
+ }, []);
964
+ const handleDragOver = useCallback6((_e) => {
965
+ setIsDragOver(true);
966
+ }, []);
967
+ const handleDragLeave = useCallback6(() => {
968
+ setIsDragOver(false);
969
+ }, []);
970
+ const defaultContent = upload.isUploading ? `Uploading... ${upload.progress}%` : upload.error ?? upload.validationError ?? "Drop files here or click to browse";
971
+ return /* @__PURE__ */ jsxs8("div", { children: [
972
+ /* @__PURE__ */ jsx19(
973
+ DropZoneSlot,
974
+ {
975
+ isDragOver,
976
+ isInvalid: !!(upload.error || upload.validationError),
977
+ onClick: handleClick,
978
+ onDrop: handleDrop,
979
+ onDragOver: handleDragOver,
980
+ onDragLeave: handleDragLeave,
981
+ accept: upload.accept,
982
+ children: children ?? defaultContent
983
+ }
984
+ ),
985
+ /* @__PURE__ */ jsx19(
986
+ "input",
987
+ {
988
+ ref: inputRef,
989
+ type: "file",
990
+ accept: upload.accept,
991
+ multiple,
992
+ style: { display: "none" },
993
+ onChange: (e) => handleFiles(e.target.files)
994
+ }
995
+ )
996
+ ] });
997
+ }
998
+
999
+ // src/components/image-preview.tsx
1000
+ import { jsx as jsx20 } from "react/jsx-runtime";
1001
+ function ImagePreview({
1002
+ fileKey,
1003
+ src,
1004
+ getUrl,
1005
+ width = 200,
1006
+ height = 200,
1007
+ fallback,
1008
+ alt = "Image preview"
1009
+ }) {
1010
+ const resolvedSrc = src ?? (fileKey && getUrl ? getUrl(fileKey) : null);
1011
+ if (!resolvedSrc) {
1012
+ return fallback ? /* @__PURE__ */ jsx20("div", { children: fallback }) : /* @__PURE__ */ jsx20(
1013
+ "div",
1014
+ {
1015
+ style: {
1016
+ width,
1017
+ height,
1018
+ backgroundColor: "#f5f5f5",
1019
+ display: "flex",
1020
+ alignItems: "center",
1021
+ justifyContent: "center",
1022
+ borderRadius: "4px",
1023
+ color: "#999"
1024
+ },
1025
+ children: "No image"
1026
+ }
1027
+ );
1028
+ }
1029
+ return /* @__PURE__ */ jsx20(
1030
+ "img",
1031
+ {
1032
+ src: resolvedSrc,
1033
+ alt,
1034
+ style: {
1035
+ width,
1036
+ height,
1037
+ objectFit: "cover",
1038
+ borderRadius: "4px"
1039
+ }
1040
+ }
1041
+ );
1042
+ }
1043
+
1044
+ // src/components/file-list.tsx
1045
+ import { jsx as jsx21, jsxs as jsxs9 } from "react/jsx-runtime";
1046
+ function formatBytes(bytes) {
1047
+ if (bytes < 1024) return `${bytes} B`;
1048
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1049
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1050
+ }
1051
+ function FileList({
1052
+ files,
1053
+ onDownload
1054
+ }) {
1055
+ if (files.length === 0) {
1056
+ return /* @__PURE__ */ jsx21("div", { style: { color: "#999" }, children: "No files" });
1057
+ }
1058
+ return /* @__PURE__ */ jsx21(
1059
+ "ul",
1060
+ {
1061
+ style: { listStyle: "none", padding: 0, margin: 0 },
1062
+ "data-testid": "file-list",
1063
+ children: files.map((file) => /* @__PURE__ */ jsx21(
1064
+ FileListItem,
1065
+ {
1066
+ file,
1067
+ onDownload
1068
+ },
1069
+ file.key
1070
+ ))
1071
+ }
1072
+ );
1073
+ }
1074
+ function FileListItem({
1075
+ file,
1076
+ onDownload
1077
+ }) {
1078
+ return /* @__PURE__ */ jsxs9(
1079
+ "li",
1080
+ {
1081
+ style: {
1082
+ display: "flex",
1083
+ alignItems: "center",
1084
+ gap: "8px",
1085
+ padding: "8px 0",
1086
+ borderBottom: "1px solid #eee"
1087
+ },
1088
+ children: [
1089
+ /* @__PURE__ */ jsx21("span", { style: { flex: 1 }, children: file.name }),
1090
+ file.size != null ? /* @__PURE__ */ jsx21("span", { style: { color: "#666", fontSize: "0.85em" }, children: formatBytes(file.size) }) : null,
1091
+ onDownload ? /* @__PURE__ */ jsx21(
1092
+ "button",
1093
+ {
1094
+ onClick: () => onDownload(file),
1095
+ style: {
1096
+ background: "none",
1097
+ border: "none",
1098
+ cursor: "pointer",
1099
+ color: "#1976d2",
1100
+ textDecoration: "underline"
1101
+ },
1102
+ children: "Download"
1103
+ }
1104
+ ) : file.url ? /* @__PURE__ */ jsx21(
1105
+ "a",
1106
+ {
1107
+ href: file.url,
1108
+ download: file.name,
1109
+ style: { color: "#1976d2", textDecoration: "underline" },
1110
+ children: "Download"
1111
+ }
1112
+ ) : null
1113
+ ]
1114
+ }
1115
+ );
1116
+ }
1117
+
1118
+ // src/components/page-container.tsx
1119
+ import { Fragment as Fragment2, jsx as jsx22, jsxs as jsxs10 } from "react/jsx-runtime";
1120
+ function PageContainer({
1121
+ title,
1122
+ breadcrumb,
1123
+ actions,
1124
+ tabs: _tabs,
1125
+ children
1126
+ }) {
1127
+ const PageContainerSlot = useComponent("pageContainer");
1128
+ const Breadcrumb = useComponent("breadcrumb");
1129
+ return /* @__PURE__ */ jsxs10(Fragment2, { children: [
1130
+ breadcrumb && breadcrumb.length > 0 ? /* @__PURE__ */ jsx22(Breadcrumb, { items: breadcrumb }) : null,
1131
+ /* @__PURE__ */ jsx22(PageContainerSlot, { title, actions, children })
1132
+ ] });
1133
+ }
1134
+
1135
+ // src/hooks/use-action-status.ts
1136
+ import { useActions as useActions2 } from "@cfast/actions/client";
1137
+ function useActionStatus(descriptor) {
1138
+ const actions = useActions2(descriptor);
1139
+ const name = descriptor.actionNames[0];
1140
+ return actions[name]();
1141
+ }
1142
+
1143
+ // src/components/empty-state.tsx
1144
+ import { jsx as jsx23, jsxs as jsxs11 } from "react/jsx-runtime";
1145
+ function EmptyState({
1146
+ title,
1147
+ description,
1148
+ createAction,
1149
+ createLabel = "Create",
1150
+ icon: Icon
1151
+ }) {
1152
+ const Button = useComponent("button");
1153
+ if (!createAction) {
1154
+ return /* @__PURE__ */ jsxs11("div", { style: { textAlign: "center", padding: "48px 16px" }, children: [
1155
+ Icon ? /* @__PURE__ */ jsx23(Icon, { className: "empty-state-icon" }) : null,
1156
+ /* @__PURE__ */ jsx23("h3", { style: { margin: "16px 0 8px" }, children: title }),
1157
+ description ? /* @__PURE__ */ jsx23("p", { style: { color: "#666" }, children: description }) : null
1158
+ ] });
1159
+ }
1160
+ return /* @__PURE__ */ jsx23(
1161
+ EmptyStateWithAction,
1162
+ {
1163
+ title,
1164
+ description,
1165
+ createAction,
1166
+ createLabel,
1167
+ icon: Icon,
1168
+ Button
1169
+ }
1170
+ );
1171
+ }
1172
+ function EmptyStateWithAction({
1173
+ title,
1174
+ description,
1175
+ createAction,
1176
+ createLabel,
1177
+ icon: Icon,
1178
+ Button
1179
+ }) {
1180
+ const status = useActionStatus(createAction);
1181
+ if (status.invisible) {
1182
+ return /* @__PURE__ */ jsx23("div", { style: { textAlign: "center", padding: "48px 16px" }, children: /* @__PURE__ */ jsx23("h3", { style: { margin: "16px 0 8px" }, children: "Nothing here yet" }) });
1183
+ }
1184
+ return /* @__PURE__ */ jsxs11("div", { style: { textAlign: "center", padding: "48px 16px" }, children: [
1185
+ Icon ? /* @__PURE__ */ jsx23(Icon, { className: "empty-state-icon" }) : null,
1186
+ /* @__PURE__ */ jsx23("h3", { style: { margin: "16px 0 8px" }, children: title }),
1187
+ description ? /* @__PURE__ */ jsx23("p", { style: { color: "#666" }, children: description }) : null,
1188
+ status.permitted ? /* @__PURE__ */ jsx23("div", { style: { marginTop: "16px" }, children: /* @__PURE__ */ jsx23(Button, { onClick: () => status.submit(), loading: status.pending, children: createLabel }) }) : null
1189
+ ] });
1190
+ }
1191
+
1192
+ // src/components/list-view.tsx
1193
+ import { useState as useState5, useCallback as useCallback7 } from "react";
1194
+ import { jsx as jsx24, jsxs as jsxs12 } from "react/jsx-runtime";
1195
+ function ListView({
1196
+ title,
1197
+ data,
1198
+ table: _table,
1199
+ columns,
1200
+ actions: _actions,
1201
+ filters,
1202
+ searchable,
1203
+ createAction,
1204
+ createLabel = "Create",
1205
+ selectable = false,
1206
+ bulkActions,
1207
+ breadcrumb
1208
+ }) {
1209
+ const [selectedRows, setSelectedRows] = useState5([]);
1210
+ const handleBulkAction = useCallback7(
1211
+ (action) => {
1212
+ if (action.handler) {
1213
+ action.handler(selectedRows);
1214
+ }
1215
+ },
1216
+ [selectedRows]
1217
+ );
1218
+ const clearSelection = useCallback7(() => {
1219
+ setSelectedRows([]);
1220
+ }, []);
1221
+ const createButton = createAction ? /* @__PURE__ */ jsx24(CreateButton, { action: createAction, label: createLabel }) : null;
1222
+ return /* @__PURE__ */ jsx24(PageContainer, { title, breadcrumb, actions: createButton, children: /* @__PURE__ */ jsxs12("div", { children: [
1223
+ filters && filters.length > 0 ? /* @__PURE__ */ jsx24(FilterBar, { filters, searchable }) : null,
1224
+ selectable && bulkActions && bulkActions.length > 0 ? /* @__PURE__ */ jsx24(
1225
+ BulkActionBar,
1226
+ {
1227
+ selectedCount: selectedRows.length,
1228
+ actions: bulkActions,
1229
+ onAction: handleBulkAction,
1230
+ onClearSelection: clearSelection
1231
+ }
1232
+ ) : null,
1233
+ data.items.length === 0 && !data.isLoading ? /* @__PURE__ */ jsx24(
1234
+ EmptyState,
1235
+ {
1236
+ title: `No ${title.toLowerCase()} found`,
1237
+ description: filters ? "Try adjusting your filters" : void 0,
1238
+ createAction,
1239
+ createLabel
1240
+ }
1241
+ ) : /* @__PURE__ */ jsx24(
1242
+ DataTable,
1243
+ {
1244
+ data,
1245
+ columns,
1246
+ selectable,
1247
+ selectedRows: selectable ? selectedRows : void 0,
1248
+ onSelectionChange: selectable ? (rows) => setSelectedRows(rows) : void 0
1249
+ }
1250
+ ),
1251
+ data.totalPages && data.totalPages > 1 && data.goToPage ? /* @__PURE__ */ jsxs12(
1252
+ "div",
1253
+ {
1254
+ style: {
1255
+ display: "flex",
1256
+ justifyContent: "center",
1257
+ gap: "8px",
1258
+ marginTop: "16px"
1259
+ },
1260
+ children: [
1261
+ /* @__PURE__ */ jsx24(
1262
+ "button",
1263
+ {
1264
+ disabled: data.currentPage === 1,
1265
+ onClick: () => data.goToPage?.(Math.max(1, (data.currentPage ?? 1) - 1)),
1266
+ children: "Previous"
1267
+ }
1268
+ ),
1269
+ /* @__PURE__ */ jsx24("span", { children: `Page ${data.currentPage ?? 1} of ${data.totalPages}` }),
1270
+ /* @__PURE__ */ jsx24(
1271
+ "button",
1272
+ {
1273
+ disabled: data.currentPage === data.totalPages,
1274
+ onClick: () => data.goToPage?.(
1275
+ Math.min(data.totalPages ?? 1, (data.currentPage ?? 1) + 1)
1276
+ ),
1277
+ children: "Next"
1278
+ }
1279
+ )
1280
+ ]
1281
+ }
1282
+ ) : null,
1283
+ data.hasMore && data.loadMore ? /* @__PURE__ */ jsx24("div", { style: { textAlign: "center", marginTop: "16px" }, children: /* @__PURE__ */ jsx24("button", { onClick: data.loadMore, children: "Load more" }) }) : null
1284
+ ] }) });
1285
+ }
1286
+ function CreateButton({ action, label }) {
1287
+ const status = useActionStatus(action);
1288
+ return /* @__PURE__ */ jsx24(ActionButton, { action: status, variant: "solid", color: "primary", children: label });
1289
+ }
1290
+
1291
+ // src/components/detail-view.tsx
1292
+ import { jsx as jsx25, jsxs as jsxs13 } from "react/jsx-runtime";
1293
+ function normalizeFields(fields) {
1294
+ if (!fields) return [];
1295
+ return fields.map((col) => {
1296
+ if (typeof col === "string") {
1297
+ return {
1298
+ key: col,
1299
+ label: col.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim()
1300
+ };
1301
+ }
1302
+ return col;
1303
+ });
1304
+ }
1305
+ function DetailView({
1306
+ title,
1307
+ table,
1308
+ record,
1309
+ fields: fieldsProp,
1310
+ exclude,
1311
+ breadcrumb
1312
+ }) {
1313
+ const fields = normalizeFields(fieldsProp);
1314
+ const displayFields = fields.length > 0 ? fields : inferFieldsFromRecord(record, exclude);
1315
+ return /* @__PURE__ */ jsx25(PageContainer, { title, breadcrumb, children: /* @__PURE__ */ jsx25(
1316
+ "div",
1317
+ {
1318
+ style: {
1319
+ display: "grid",
1320
+ gridTemplateColumns: "1fr 1fr",
1321
+ gap: "16px"
1322
+ },
1323
+ children: displayFields.map((field) => {
1324
+ const value = getField(record, field.key);
1325
+ const FieldComponent = field.render ? null : resolveFieldComponent(field.key, table);
1326
+ return /* @__PURE__ */ jsxs13("div", { children: [
1327
+ /* @__PURE__ */ jsx25(
1328
+ "div",
1329
+ {
1330
+ style: {
1331
+ fontSize: "0.85em",
1332
+ color: "#666",
1333
+ marginBottom: "4px",
1334
+ fontWeight: 600
1335
+ },
1336
+ children: field.label ?? field.key
1337
+ }
1338
+ ),
1339
+ /* @__PURE__ */ jsx25("div", { children: field.render ? field.render(value, record) : FieldComponent ? /* @__PURE__ */ jsx25(FieldComponent, { value }) : String(value ?? "\u2014") })
1340
+ ] }, field.key);
1341
+ })
1342
+ }
1343
+ ) });
1344
+ }
1345
+ function inferFieldsFromRecord(record, exclude) {
1346
+ if (!record || typeof record !== "object") return [];
1347
+ return Object.keys(record).filter((key) => !exclude || !exclude.includes(key)).map((key) => ({
1348
+ key,
1349
+ label: key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim()
1350
+ }));
1351
+ }
1352
+ function resolveFieldComponent(_key, table) {
1353
+ if (!table || typeof table !== "object") return null;
1354
+ const col = getField(table, _key);
1355
+ if (!col || typeof col !== "object" || !("dataType" in col) || typeof col.dataType !== "string" || !("name" in col) || typeof col.name !== "string") {
1356
+ return null;
1357
+ }
1358
+ return fieldForColumn({ dataType: col.dataType, name: col.name });
1359
+ }
1360
+
1361
+ export {
1362
+ createUIPlugin,
1363
+ UIPluginProvider,
1364
+ useUIPlugin,
1365
+ useComponent,
1366
+ useConfirm,
1367
+ ToastContext,
1368
+ useToast,
1369
+ useActionToast,
1370
+ PermissionGate,
1371
+ ActionButton,
1372
+ ConfirmProvider,
1373
+ FormStatus,
1374
+ getInitials,
1375
+ AvatarWithInitials,
1376
+ RoleBadge,
1377
+ ImpersonationBanner,
1378
+ getField,
1379
+ getRecordId,
1380
+ DataTable,
1381
+ FilterBar,
1382
+ BulkActionBar,
1383
+ DateField,
1384
+ BooleanField,
1385
+ NumberField,
1386
+ TextField,
1387
+ EmailField,
1388
+ JsonField,
1389
+ fieldForColumn,
1390
+ fieldsForTable,
1391
+ useColumnInference,
1392
+ DropZone,
1393
+ ImagePreview,
1394
+ FileList,
1395
+ PageContainer,
1396
+ useActionStatus,
1397
+ EmptyState,
1398
+ ListView,
1399
+ DetailView
1400
+ };