@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
@@ -1,925 +0,0 @@
1
- import {
2
- fieldForColumn,
3
- getField,
4
- getRecordId,
5
- useActionStatus,
6
- useComponent,
7
- useToast
8
- } from "./chunk-RDTUEOLK.js";
9
-
10
- // src/hooks/use-confirm.ts
11
- import { createContext, useContext, useCallback } from "react";
12
- var ConfirmContext = createContext(null);
13
- function useConfirm() {
14
- const ctx = useContext(ConfirmContext);
15
- if (!ctx) {
16
- throw new Error("useConfirm must be used within a <ConfirmProvider>");
17
- }
18
- return useCallback(
19
- (options) => ctx.confirm(options),
20
- [ctx]
21
- );
22
- }
23
-
24
- // src/hooks/use-action-toast.ts
25
- import { useEffect, useRef } from "react";
26
- import { useActions } from "@cfast/actions/client";
27
- function useActionToast(descriptor, config) {
28
- const actions = useActions(descriptor);
29
- const toast = useToast();
30
- const prevDataRef = useRef({});
31
- useEffect(() => {
32
- for (const [name, cfg] of Object.entries(config)) {
33
- const actionFn = actions[name];
34
- if (!actionFn) continue;
35
- const result = actionFn();
36
- const prevData = prevDataRef.current[name];
37
- if (result.data !== void 0 && result.data !== prevData) {
38
- prevDataRef.current[name] = result.data;
39
- if (cfg.success) {
40
- toast.success(cfg.success);
41
- }
42
- }
43
- if (result.error !== void 0 && result.error !== prevData) {
44
- prevDataRef.current[name] = result.error;
45
- if (cfg.error) {
46
- toast.error(cfg.error);
47
- }
48
- }
49
- }
50
- });
51
- }
52
-
53
- // src/components/action-button.tsx
54
- import { jsx } from "react/jsx-runtime";
55
- function ActionButton({
56
- action,
57
- children,
58
- whenForbidden = "disable",
59
- confirmation: _confirmation,
60
- ...buttonProps
61
- }) {
62
- const Button = useComponent("button");
63
- if (action.invisible) {
64
- return null;
65
- }
66
- if (!action.permitted && whenForbidden === "hide") {
67
- return null;
68
- }
69
- const disabled = !action.permitted && whenForbidden === "disable";
70
- return /* @__PURE__ */ jsx(
71
- Button,
72
- {
73
- ...buttonProps,
74
- onClick: () => action.submit(),
75
- disabled,
76
- loading: action.pending,
77
- children
78
- }
79
- );
80
- }
81
-
82
- // src/components/confirm-provider.tsx
83
- import { useState, useCallback as useCallback2, useRef as useRef2 } from "react";
84
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
85
- function ConfirmProvider({ children }) {
86
- const [state, setState] = useState(null);
87
- const ConfirmDialog = useComponent("confirmDialog");
88
- const resolveRef = useRef2(null);
89
- const confirm = useCallback2((options) => {
90
- return new Promise((resolve) => {
91
- resolveRef.current = resolve;
92
- setState({ ...options, resolve });
93
- });
94
- }, []);
95
- const handleClose = useCallback2(() => {
96
- resolveRef.current?.(false);
97
- resolveRef.current = null;
98
- setState(null);
99
- }, []);
100
- const handleConfirm = useCallback2(() => {
101
- resolveRef.current?.(true);
102
- resolveRef.current = null;
103
- setState(null);
104
- }, []);
105
- return /* @__PURE__ */ jsxs(ConfirmContext.Provider, { value: { confirm }, children: [
106
- children,
107
- state ? /* @__PURE__ */ jsx2(
108
- ConfirmDialog,
109
- {
110
- open: true,
111
- onClose: handleClose,
112
- onConfirm: handleConfirm,
113
- title: state.title,
114
- description: state.description,
115
- confirmLabel: state.confirmLabel,
116
- cancelLabel: state.cancelLabel,
117
- variant: state.variant
118
- }
119
- ) : null
120
- ] });
121
- }
122
-
123
- // src/components/form-status.tsx
124
- import { jsx as jsx3 } from "react/jsx-runtime";
125
- function FormStatus({ data }) {
126
- const Alert = useComponent("alert");
127
- if (!data) return null;
128
- const elements = [];
129
- if (data.success) {
130
- elements.push(
131
- /* @__PURE__ */ jsx3(Alert, { color: "success", children: data.success }, "success")
132
- );
133
- }
134
- if (data.error) {
135
- elements.push(
136
- /* @__PURE__ */ jsx3(Alert, { color: "danger", children: data.error }, "error")
137
- );
138
- }
139
- if (data.fieldErrors) {
140
- const errorMessages = Object.entries(data.fieldErrors).flatMap(
141
- ([field, errors]) => errors.map((err) => `${field}: ${err}`)
142
- );
143
- if (errorMessages.length > 0) {
144
- elements.push(
145
- /* @__PURE__ */ jsx3(Alert, { color: "danger", children: /* @__PURE__ */ jsx3("ul", { style: { margin: 0, paddingLeft: "16px" }, children: errorMessages.map((msg, i) => /* @__PURE__ */ jsx3("li", { children: msg }, i)) }) }, "field-errors")
146
- );
147
- }
148
- }
149
- if (elements.length === 0) return null;
150
- return /* @__PURE__ */ jsx3("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: elements });
151
- }
152
-
153
- // src/components/role-badge.tsx
154
- import { jsx as jsx4 } from "react/jsx-runtime";
155
- var defaultColors = {
156
- admin: "danger",
157
- editor: "primary",
158
- author: "success",
159
- reader: "neutral"
160
- };
161
- function RoleBadge({ role, colors }) {
162
- const Chip = useComponent("chip");
163
- const colorMap = colors ? { ...defaultColors, ...Object.fromEntries(
164
- Object.entries(colors).map(([k, v]) => [k, v])
165
- ) } : defaultColors;
166
- const chipColor = colorMap[role] ?? "neutral";
167
- return /* @__PURE__ */ jsx4(Chip, { color: chipColor, variant: "soft", size: "sm", children: role });
168
- }
169
-
170
- // src/components/impersonation-banner.tsx
171
- import { useCurrentUser } from "@cfast/auth/client";
172
- import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
173
- function ImpersonationBanner({
174
- stopAction = "/admin/stop-impersonation"
175
- }) {
176
- const user = useCurrentUser();
177
- const Alert = useComponent("alert");
178
- const Button = useComponent("button");
179
- if (!user?.isImpersonating) {
180
- return null;
181
- }
182
- return /* @__PURE__ */ jsx5(Alert, { color: "warning", children: /* @__PURE__ */ jsxs2(
183
- "div",
184
- {
185
- style: {
186
- display: "flex",
187
- alignItems: "center",
188
- justifyContent: "center",
189
- gap: "12px"
190
- },
191
- children: [
192
- /* @__PURE__ */ jsx5("strong", { children: `Viewing as ${user.name} (${user.email})` }),
193
- /* @__PURE__ */ jsx5("form", { method: "post", action: stopAction, children: /* @__PURE__ */ jsx5(Button, { type: "submit", variant: "outlined", size: "sm", children: "Stop Impersonating" }) })
194
- ]
195
- }
196
- ) });
197
- }
198
-
199
- // src/components/data-table.tsx
200
- import { useState as useState2, useCallback as useCallback3 } from "react";
201
- import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
202
- function normalizeColumns(columns) {
203
- if (!columns) return [];
204
- return columns.map((col) => {
205
- if (typeof col === "string") {
206
- return {
207
- key: col,
208
- label: col.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim(),
209
- sortable: true
210
- };
211
- }
212
- return col;
213
- });
214
- }
215
- function DataTable({
216
- data,
217
- columns: columnsProp,
218
- selectable = false,
219
- selectedRows: externalSelectedRows,
220
- onSelectionChange,
221
- onRowClick,
222
- getRowId,
223
- emptyMessage = "No data"
224
- }) {
225
- const Table = useComponent("table");
226
- const TableHead = useComponent("tableHead");
227
- const TableBody = useComponent("tableBody");
228
- const TableRow = useComponent("tableRow");
229
- const TableCell = useComponent("tableCell");
230
- const columns = normalizeColumns(columnsProp);
231
- const [sortKey, setSortKey] = useState2(null);
232
- const [sortDir, setSortDir] = useState2("asc");
233
- const [internalSelected, setInternalSelected] = useState2(/* @__PURE__ */ new Set());
234
- const selectedSet = externalSelectedRows ? new Set(externalSelectedRows.map((r) => (getRowId ?? defaultGetId)(r))) : internalSelected;
235
- const handleSort = useCallback3((key) => {
236
- if (sortKey === key) {
237
- setSortDir((d) => d === "asc" ? "desc" : "asc");
238
- } else {
239
- setSortKey(key);
240
- setSortDir("asc");
241
- }
242
- }, [sortKey]);
243
- const toggleRow = useCallback3((id) => {
244
- if (onSelectionChange) {
245
- const row = data.items.find((r) => (getRowId ?? defaultGetId)(r) === id);
246
- if (!row) return;
247
- const current = externalSelectedRows ?? [];
248
- const isSelected = current.some((r) => (getRowId ?? defaultGetId)(r) === id);
249
- onSelectionChange(isSelected ? current.filter((r) => (getRowId ?? defaultGetId)(r) !== id) : [...current, row]);
250
- } else {
251
- setInternalSelected((prev) => {
252
- const next = new Set(prev);
253
- if (next.has(id)) next.delete(id);
254
- else next.add(id);
255
- return next;
256
- });
257
- }
258
- }, [data.items, externalSelectedRows, onSelectionChange, getRowId]);
259
- if (data.items.length === 0 && !data.isLoading) {
260
- return /* @__PURE__ */ jsx6("div", { style: { textAlign: "center", padding: "32px", color: "#666" }, children: emptyMessage });
261
- }
262
- return /* @__PURE__ */ jsxs3(Table, { hoverRow: true, children: [
263
- /* @__PURE__ */ jsx6(TableHead, { children: /* @__PURE__ */ jsxs3(TableRow, { children: [
264
- selectable ? /* @__PURE__ */ jsx6(TableCell, { header: true, children: "" }) : null,
265
- columns.map((col) => /* @__PURE__ */ jsx6(
266
- TableCell,
267
- {
268
- header: true,
269
- sortable: col.sortable !== false,
270
- sortDirection: sortKey === col.key ? sortDir : null,
271
- onSort: () => handleSort(col.key),
272
- children: col.label ?? col.key
273
- },
274
- col.key
275
- ))
276
- ] }) }),
277
- /* @__PURE__ */ jsx6(TableBody, { children: data.items.map((row) => {
278
- const id = (getRowId ?? defaultGetId)(row);
279
- const isSelected = selectedSet.has(id);
280
- return /* @__PURE__ */ jsxs3(
281
- TableRow,
282
- {
283
- selected: isSelected,
284
- onClick: onRowClick ? () => onRowClick(row) : void 0,
285
- children: [
286
- selectable ? /* @__PURE__ */ jsx6(TableCell, { children: /* @__PURE__ */ jsx6(
287
- "input",
288
- {
289
- type: "checkbox",
290
- checked: isSelected,
291
- onChange: () => toggleRow(id)
292
- }
293
- ) }) : null,
294
- columns.map((col) => {
295
- const value = getField(row, col.key);
296
- return /* @__PURE__ */ jsx6(TableCell, { children: col.render ? col.render(value, row) : String(value ?? "") }, col.key);
297
- })
298
- ]
299
- },
300
- String(id)
301
- );
302
- }) })
303
- ] });
304
- }
305
- function defaultGetId(row) {
306
- return getRecordId(row);
307
- }
308
-
309
- // src/components/filter-bar.tsx
310
- import { useCallback as useCallback4 } from "react";
311
- import { useSearchParams, useNavigate, useLocation } from "react-router";
312
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
313
- function FilterBar({
314
- filters,
315
- searchable
316
- }) {
317
- const [searchParams] = useSearchParams();
318
- const navigate = useNavigate();
319
- const location = useLocation();
320
- const updateParam = useCallback4(
321
- (key, value) => {
322
- const params = new URLSearchParams(searchParams);
323
- if (value === null || value === "") {
324
- params.delete(key);
325
- } else {
326
- params.set(key, value);
327
- }
328
- params.delete("page");
329
- params.delete("cursor");
330
- navigate(`${location.pathname}?${params.toString()}`);
331
- },
332
- [searchParams, navigate, location.pathname]
333
- );
334
- return /* @__PURE__ */ jsxs4(
335
- "div",
336
- {
337
- style: {
338
- display: "flex",
339
- gap: "8px",
340
- flexWrap: "wrap",
341
- alignItems: "center",
342
- marginBottom: "16px"
343
- },
344
- children: [
345
- searchable && searchable.length > 0 ? /* @__PURE__ */ jsx7(
346
- "input",
347
- {
348
- type: "search",
349
- placeholder: `Search ${searchable.join(", ")}...`,
350
- value: searchParams.get("q") ?? "",
351
- onChange: (e) => updateParam("q", e.target.value || null),
352
- style: { padding: "6px 10px", border: "1px solid #ccc", borderRadius: "4px" }
353
- }
354
- ) : null,
355
- filters.map((filter) => /* @__PURE__ */ jsx7(
356
- FilterInput,
357
- {
358
- filter,
359
- value: searchParams.get(filter.column) ?? "",
360
- onChange: (value) => updateParam(filter.column, value || null)
361
- },
362
- filter.column
363
- ))
364
- ]
365
- }
366
- );
367
- }
368
- function FilterInput({
369
- filter,
370
- value,
371
- onChange
372
- }) {
373
- const label = filter.label ?? filter.column;
374
- switch (filter.type) {
375
- case "select":
376
- case "boolean":
377
- return /* @__PURE__ */ jsxs4(
378
- "select",
379
- {
380
- value,
381
- onChange: (e) => onChange(e.target.value),
382
- "aria-label": label,
383
- children: [
384
- /* @__PURE__ */ jsx7("option", { value: "", children: `All ${label}` }),
385
- (filter.options ?? []).map((opt) => /* @__PURE__ */ jsx7("option", { value: String(opt.value), children: opt.label }, String(opt.value)))
386
- ]
387
- }
388
- );
389
- case "text":
390
- default:
391
- return /* @__PURE__ */ jsx7(
392
- "input",
393
- {
394
- type: "text",
395
- placeholder: filter.placeholder ?? label,
396
- value,
397
- onChange: (e) => onChange(e.target.value),
398
- "aria-label": label,
399
- style: { padding: "6px 10px", border: "1px solid #ccc", borderRadius: "4px" }
400
- }
401
- );
402
- }
403
- }
404
-
405
- // src/components/bulk-action-bar.tsx
406
- import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
407
- function BulkActionBar({
408
- selectedCount,
409
- actions,
410
- onAction,
411
- onClearSelection
412
- }) {
413
- const Button = useComponent("button");
414
- if (selectedCount === 0) return null;
415
- return /* @__PURE__ */ jsxs5(
416
- "div",
417
- {
418
- style: {
419
- display: "flex",
420
- alignItems: "center",
421
- gap: "8px",
422
- padding: "8px 16px",
423
- backgroundColor: "#f0f4ff",
424
- borderRadius: "4px",
425
- marginBottom: "8px"
426
- },
427
- children: [
428
- /* @__PURE__ */ jsx8("span", { children: `${selectedCount} selected` }),
429
- actions.map((action) => {
430
- const Icon = action.icon;
431
- return /* @__PURE__ */ jsx8(
432
- Button,
433
- {
434
- onClick: () => onAction(action),
435
- variant: "soft",
436
- size: "sm",
437
- startDecorator: Icon ? /* @__PURE__ */ jsx8(Icon, { className: "bulk-action-icon" }) : void 0,
438
- children: action.label
439
- },
440
- action.label
441
- );
442
- }),
443
- /* @__PURE__ */ jsx8(
444
- Button,
445
- {
446
- onClick: onClearSelection,
447
- variant: "plain",
448
- size: "sm",
449
- children: "Clear"
450
- }
451
- )
452
- ]
453
- }
454
- );
455
- }
456
-
457
- // src/hooks/use-column-inference.ts
458
- import { useMemo } from "react";
459
- function useColumnInference(table, columns) {
460
- return useMemo(() => {
461
- if (!table) return [];
462
- const result = [];
463
- for (const [key, col] of Object.entries(table)) {
464
- if (columns && !columns.includes(key)) continue;
465
- if (!col || typeof col !== "object" || !("dataType" in col) || !("name" in col)) continue;
466
- const meta = col;
467
- const field = fieldForColumn(meta);
468
- const label = key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
469
- result.push({
470
- key,
471
- label,
472
- sortable: true,
473
- field
474
- });
475
- }
476
- if (columns) {
477
- result.sort((a, b) => columns.indexOf(a.key) - columns.indexOf(b.key));
478
- }
479
- return result;
480
- }, [table, columns]);
481
- }
482
-
483
- // src/components/drop-zone.tsx
484
- import { useState as useState3, useCallback as useCallback5, useRef as useRef3 } from "react";
485
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
486
- function DropZone({
487
- upload,
488
- multiple = false,
489
- children
490
- }) {
491
- const DropZoneSlot = useComponent("dropZone");
492
- const [isDragOver, setIsDragOver] = useState3(false);
493
- const inputRef = useRef3(null);
494
- const handleFiles = useCallback5(
495
- (files) => {
496
- if (!files || files.length === 0) return;
497
- if (multiple) {
498
- for (let i = 0; i < files.length; i++) {
499
- upload.start(files[i]);
500
- }
501
- } else {
502
- upload.start(files[0]);
503
- }
504
- },
505
- [upload, multiple]
506
- );
507
- const handleDrop = useCallback5(
508
- (files) => {
509
- setIsDragOver(false);
510
- handleFiles(files);
511
- },
512
- [handleFiles]
513
- );
514
- const handleClick = useCallback5(() => {
515
- inputRef.current?.click();
516
- }, []);
517
- const handleDragOver = useCallback5((_e) => {
518
- setIsDragOver(true);
519
- }, []);
520
- const handleDragLeave = useCallback5(() => {
521
- setIsDragOver(false);
522
- }, []);
523
- const defaultContent = upload.isUploading ? `Uploading... ${upload.progress}%` : upload.error ?? upload.validationError ?? "Drop files here or click to browse";
524
- return /* @__PURE__ */ jsxs6("div", { children: [
525
- /* @__PURE__ */ jsx9(
526
- DropZoneSlot,
527
- {
528
- isDragOver,
529
- isInvalid: !!(upload.error || upload.validationError),
530
- onClick: handleClick,
531
- onDrop: handleDrop,
532
- onDragOver: handleDragOver,
533
- onDragLeave: handleDragLeave,
534
- accept: upload.accept,
535
- children: children ?? defaultContent
536
- }
537
- ),
538
- /* @__PURE__ */ jsx9(
539
- "input",
540
- {
541
- ref: inputRef,
542
- type: "file",
543
- accept: upload.accept,
544
- multiple,
545
- style: { display: "none" },
546
- onChange: (e) => handleFiles(e.target.files)
547
- }
548
- )
549
- ] });
550
- }
551
-
552
- // src/components/image-preview.tsx
553
- import { jsx as jsx10 } from "react/jsx-runtime";
554
- function ImagePreview({
555
- fileKey,
556
- src,
557
- getUrl,
558
- width = 200,
559
- height = 200,
560
- fallback,
561
- alt = "Image preview"
562
- }) {
563
- const resolvedSrc = src ?? (fileKey && getUrl ? getUrl(fileKey) : null);
564
- if (!resolvedSrc) {
565
- return fallback ? /* @__PURE__ */ jsx10("div", { children: fallback }) : /* @__PURE__ */ jsx10(
566
- "div",
567
- {
568
- style: {
569
- width,
570
- height,
571
- backgroundColor: "#f5f5f5",
572
- display: "flex",
573
- alignItems: "center",
574
- justifyContent: "center",
575
- borderRadius: "4px",
576
- color: "#999"
577
- },
578
- children: "No image"
579
- }
580
- );
581
- }
582
- return /* @__PURE__ */ jsx10(
583
- "img",
584
- {
585
- src: resolvedSrc,
586
- alt,
587
- style: {
588
- width,
589
- height,
590
- objectFit: "cover",
591
- borderRadius: "4px"
592
- }
593
- }
594
- );
595
- }
596
-
597
- // src/components/file-list.tsx
598
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
599
- function formatBytes(bytes) {
600
- if (bytes < 1024) return `${bytes} B`;
601
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
602
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
603
- }
604
- function FileList({
605
- files,
606
- onDownload
607
- }) {
608
- if (files.length === 0) {
609
- return /* @__PURE__ */ jsx11("div", { style: { color: "#999" }, children: "No files" });
610
- }
611
- return /* @__PURE__ */ jsx11(
612
- "ul",
613
- {
614
- style: { listStyle: "none", padding: 0, margin: 0 },
615
- "data-testid": "file-list",
616
- children: files.map((file) => /* @__PURE__ */ jsx11(
617
- FileListItem,
618
- {
619
- file,
620
- onDownload
621
- },
622
- file.key
623
- ))
624
- }
625
- );
626
- }
627
- function FileListItem({
628
- file,
629
- onDownload
630
- }) {
631
- return /* @__PURE__ */ jsxs7(
632
- "li",
633
- {
634
- style: {
635
- display: "flex",
636
- alignItems: "center",
637
- gap: "8px",
638
- padding: "8px 0",
639
- borderBottom: "1px solid #eee"
640
- },
641
- children: [
642
- /* @__PURE__ */ jsx11("span", { style: { flex: 1 }, children: file.name }),
643
- file.size != null ? /* @__PURE__ */ jsx11("span", { style: { color: "#666", fontSize: "0.85em" }, children: formatBytes(file.size) }) : null,
644
- onDownload ? /* @__PURE__ */ jsx11(
645
- "button",
646
- {
647
- onClick: () => onDownload(file),
648
- style: {
649
- background: "none",
650
- border: "none",
651
- cursor: "pointer",
652
- color: "#1976d2",
653
- textDecoration: "underline"
654
- },
655
- children: "Download"
656
- }
657
- ) : file.url ? /* @__PURE__ */ jsx11(
658
- "a",
659
- {
660
- href: file.url,
661
- download: file.name,
662
- style: { color: "#1976d2", textDecoration: "underline" },
663
- children: "Download"
664
- }
665
- ) : null
666
- ]
667
- }
668
- );
669
- }
670
-
671
- // src/components/page-container.tsx
672
- import { Fragment, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
673
- function PageContainer({
674
- title,
675
- breadcrumb,
676
- actions,
677
- tabs: _tabs,
678
- children
679
- }) {
680
- const PageContainerSlot = useComponent("pageContainer");
681
- const Breadcrumb = useComponent("breadcrumb");
682
- return /* @__PURE__ */ jsxs8(Fragment, { children: [
683
- breadcrumb && breadcrumb.length > 0 ? /* @__PURE__ */ jsx12(Breadcrumb, { items: breadcrumb }) : null,
684
- /* @__PURE__ */ jsx12(PageContainerSlot, { title, actions, children })
685
- ] });
686
- }
687
-
688
- // src/components/empty-state.tsx
689
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
690
- function EmptyState({
691
- title,
692
- description,
693
- createAction,
694
- createLabel = "Create",
695
- icon: Icon
696
- }) {
697
- const Button = useComponent("button");
698
- if (!createAction) {
699
- return /* @__PURE__ */ jsxs9("div", { style: { textAlign: "center", padding: "48px 16px" }, children: [
700
- Icon ? /* @__PURE__ */ jsx13(Icon, { className: "empty-state-icon" }) : null,
701
- /* @__PURE__ */ jsx13("h3", { style: { margin: "16px 0 8px" }, children: title }),
702
- description ? /* @__PURE__ */ jsx13("p", { style: { color: "#666" }, children: description }) : null
703
- ] });
704
- }
705
- return /* @__PURE__ */ jsx13(
706
- EmptyStateWithAction,
707
- {
708
- title,
709
- description,
710
- createAction,
711
- createLabel,
712
- icon: Icon,
713
- Button
714
- }
715
- );
716
- }
717
- function EmptyStateWithAction({
718
- title,
719
- description,
720
- createAction,
721
- createLabel,
722
- icon: Icon,
723
- Button
724
- }) {
725
- const status = useActionStatus(createAction);
726
- if (status.invisible) {
727
- return /* @__PURE__ */ jsx13("div", { style: { textAlign: "center", padding: "48px 16px" }, children: /* @__PURE__ */ jsx13("h3", { style: { margin: "16px 0 8px" }, children: "Nothing here yet" }) });
728
- }
729
- return /* @__PURE__ */ jsxs9("div", { style: { textAlign: "center", padding: "48px 16px" }, children: [
730
- Icon ? /* @__PURE__ */ jsx13(Icon, { className: "empty-state-icon" }) : null,
731
- /* @__PURE__ */ jsx13("h3", { style: { margin: "16px 0 8px" }, children: title }),
732
- description ? /* @__PURE__ */ jsx13("p", { style: { color: "#666" }, children: description }) : null,
733
- status.permitted ? /* @__PURE__ */ jsx13("div", { style: { marginTop: "16px" }, children: /* @__PURE__ */ jsx13(Button, { onClick: () => status.submit(), loading: status.pending, children: createLabel }) }) : null
734
- ] });
735
- }
736
-
737
- // src/components/list-view.tsx
738
- import { useState as useState4, useCallback as useCallback6 } from "react";
739
- import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
740
- function ListView({
741
- title,
742
- data,
743
- table: _table,
744
- columns,
745
- actions: _actions,
746
- filters,
747
- searchable,
748
- createAction,
749
- createLabel = "Create",
750
- selectable = false,
751
- bulkActions,
752
- breadcrumb
753
- }) {
754
- const [selectedRows, setSelectedRows] = useState4([]);
755
- const handleBulkAction = useCallback6(
756
- (action) => {
757
- if (action.handler) {
758
- action.handler(selectedRows);
759
- }
760
- },
761
- [selectedRows]
762
- );
763
- const clearSelection = useCallback6(() => {
764
- setSelectedRows([]);
765
- }, []);
766
- const createButton = createAction ? /* @__PURE__ */ jsx14(CreateButton, { action: createAction, label: createLabel }) : null;
767
- return /* @__PURE__ */ jsx14(PageContainer, { title, breadcrumb, actions: createButton, children: /* @__PURE__ */ jsxs10("div", { children: [
768
- filters && filters.length > 0 ? /* @__PURE__ */ jsx14(FilterBar, { filters, searchable }) : null,
769
- selectable && bulkActions && bulkActions.length > 0 ? /* @__PURE__ */ jsx14(
770
- BulkActionBar,
771
- {
772
- selectedCount: selectedRows.length,
773
- actions: bulkActions,
774
- onAction: handleBulkAction,
775
- onClearSelection: clearSelection
776
- }
777
- ) : null,
778
- data.items.length === 0 && !data.isLoading ? /* @__PURE__ */ jsx14(
779
- EmptyState,
780
- {
781
- title: `No ${title.toLowerCase()} found`,
782
- description: filters ? "Try adjusting your filters" : void 0,
783
- createAction,
784
- createLabel
785
- }
786
- ) : /* @__PURE__ */ jsx14(
787
- DataTable,
788
- {
789
- data,
790
- columns,
791
- selectable,
792
- selectedRows: selectable ? selectedRows : void 0,
793
- onSelectionChange: selectable ? (rows) => setSelectedRows(rows) : void 0
794
- }
795
- ),
796
- data.totalPages && data.totalPages > 1 && data.goToPage ? /* @__PURE__ */ jsxs10(
797
- "div",
798
- {
799
- style: {
800
- display: "flex",
801
- justifyContent: "center",
802
- gap: "8px",
803
- marginTop: "16px"
804
- },
805
- children: [
806
- /* @__PURE__ */ jsx14(
807
- "button",
808
- {
809
- disabled: data.currentPage === 1,
810
- onClick: () => data.goToPage?.(Math.max(1, (data.currentPage ?? 1) - 1)),
811
- children: "Previous"
812
- }
813
- ),
814
- /* @__PURE__ */ jsx14("span", { children: `Page ${data.currentPage ?? 1} of ${data.totalPages}` }),
815
- /* @__PURE__ */ jsx14(
816
- "button",
817
- {
818
- disabled: data.currentPage === data.totalPages,
819
- onClick: () => data.goToPage?.(
820
- Math.min(data.totalPages ?? 1, (data.currentPage ?? 1) + 1)
821
- ),
822
- children: "Next"
823
- }
824
- )
825
- ]
826
- }
827
- ) : null,
828
- data.hasMore && data.loadMore ? /* @__PURE__ */ jsx14("div", { style: { textAlign: "center", marginTop: "16px" }, children: /* @__PURE__ */ jsx14("button", { onClick: data.loadMore, children: "Load more" }) }) : null
829
- ] }) });
830
- }
831
- function CreateButton({ action, label }) {
832
- const status = useActionStatus(action);
833
- return /* @__PURE__ */ jsx14(ActionButton, { action: status, variant: "solid", color: "primary", children: label });
834
- }
835
-
836
- // src/components/detail-view.tsx
837
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
838
- function normalizeFields(fields) {
839
- if (!fields) return [];
840
- return fields.map((col) => {
841
- if (typeof col === "string") {
842
- return {
843
- key: col,
844
- label: col.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim()
845
- };
846
- }
847
- return col;
848
- });
849
- }
850
- function DetailView({
851
- title,
852
- table,
853
- record,
854
- fields: fieldsProp,
855
- exclude,
856
- breadcrumb
857
- }) {
858
- const fields = normalizeFields(fieldsProp);
859
- const displayFields = fields.length > 0 ? fields : inferFieldsFromRecord(record, exclude);
860
- return /* @__PURE__ */ jsx15(PageContainer, { title, breadcrumb, children: /* @__PURE__ */ jsx15(
861
- "div",
862
- {
863
- style: {
864
- display: "grid",
865
- gridTemplateColumns: "1fr 1fr",
866
- gap: "16px"
867
- },
868
- children: displayFields.map((field) => {
869
- const value = getField(record, field.key);
870
- const FieldComponent = field.render ? null : resolveFieldComponent(field.key, table);
871
- return /* @__PURE__ */ jsxs11("div", { children: [
872
- /* @__PURE__ */ jsx15(
873
- "div",
874
- {
875
- style: {
876
- fontSize: "0.85em",
877
- color: "#666",
878
- marginBottom: "4px",
879
- fontWeight: 600
880
- },
881
- children: field.label ?? field.key
882
- }
883
- ),
884
- /* @__PURE__ */ jsx15("div", { children: field.render ? field.render(value, record) : FieldComponent ? /* @__PURE__ */ jsx15(FieldComponent, { value }) : String(value ?? "\u2014") })
885
- ] }, field.key);
886
- })
887
- }
888
- ) });
889
- }
890
- function inferFieldsFromRecord(record, exclude) {
891
- if (!record || typeof record !== "object") return [];
892
- return Object.keys(record).filter((key) => !exclude || !exclude.includes(key)).map((key) => ({
893
- key,
894
- label: key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim()
895
- }));
896
- }
897
- function resolveFieldComponent(_key, table) {
898
- if (!table || typeof table !== "object") return null;
899
- const col = getField(table, _key);
900
- if (!col || typeof col !== "object" || !("dataType" in col) || typeof col.dataType !== "string" || !("name" in col) || typeof col.name !== "string") {
901
- return null;
902
- }
903
- return fieldForColumn({ dataType: col.dataType, name: col.name });
904
- }
905
-
906
- export {
907
- useConfirm,
908
- useActionToast,
909
- ActionButton,
910
- ConfirmProvider,
911
- FormStatus,
912
- RoleBadge,
913
- ImpersonationBanner,
914
- DataTable,
915
- FilterBar,
916
- BulkActionBar,
917
- useColumnInference,
918
- DropZone,
919
- ImagePreview,
920
- FileList,
921
- PageContainer,
922
- EmptyState,
923
- ListView,
924
- DetailView
925
- };