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