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