@cfast/ui 0.0.1

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +727 -0
  3. package/dist/chunk-755IRYDN.js +941 -0
  4. package/dist/chunk-7SNK37GF.js +418 -0
  5. package/dist/chunk-ASMYTWTR.js +356 -0
  6. package/dist/chunk-B2XXH5V4.js +66 -0
  7. package/dist/chunk-BQMXYYEV.js +348 -0
  8. package/dist/chunk-DTKBXCTU.js +211 -0
  9. package/dist/chunk-EYIBATYR.js +33 -0
  10. package/dist/chunk-FPZAQ2YQ.js +474 -0
  11. package/dist/chunk-G2OU4BYC.js +205 -0
  12. package/dist/chunk-JEGEIQ3R.js +925 -0
  13. package/dist/chunk-JUNLQJ6H.js +1013 -0
  14. package/dist/chunk-NRGMW3JA.js +906 -0
  15. package/dist/chunk-Q6FPL2OJ.js +1086 -0
  16. package/dist/chunk-QHWAGKNW.js +456 -0
  17. package/dist/chunk-QZT62CGJ.js +924 -0
  18. package/dist/chunk-RDTUEOLK.js +486 -0
  19. package/dist/chunk-RESL4IJJ.js +112 -0
  20. package/dist/chunk-UDCWQUTR.js +221 -0
  21. package/dist/chunk-UE7PZOIJ.js +11 -0
  22. package/dist/chunk-UTZTHGNE.js +84 -0
  23. package/dist/chunk-UVRXMOX5.js +439 -0
  24. package/dist/chunk-XFD3N2D4.js +161 -0
  25. package/dist/client-CXIHCQtA.d.ts +274 -0
  26. package/dist/client.d.ts +617 -0
  27. package/dist/client.js +54 -0
  28. package/dist/index.d.ts +415 -0
  29. package/dist/index.js +296 -0
  30. package/dist/joy.d.ts +199 -0
  31. package/dist/joy.js +1150 -0
  32. package/dist/permission-gate-DVmY42oz.d.ts +1269 -0
  33. package/dist/permission-gate-apt9T9Mu.d.ts +1256 -0
  34. package/dist/types-1bAiH2uK.d.ts +392 -0
  35. package/dist/types-BX6u5sAd.d.ts +403 -0
  36. package/dist/types-BpdY7w5l.d.ts +403 -0
  37. package/dist/types-BrepeVp8.d.ts +403 -0
  38. package/dist/types-BvAqMZhn.d.ts +403 -0
  39. package/dist/types-C74nSscq.d.ts +403 -0
  40. package/dist/types-DD1Cpx8F.d.ts +403 -0
  41. package/dist/types-DHUhQwJn.d.ts +403 -0
  42. package/dist/types-DZSJNt_M.d.ts +392 -0
  43. package/dist/types-DaaJiIjW.d.ts +391 -0
  44. package/dist/types-LUpWJwps.d.ts +403 -0
  45. package/dist/types-a7zVU6WE.d.ts +394 -0
  46. package/dist/types-biJTHMcH.d.ts +403 -0
  47. package/dist/types-ow_qSEYJ.d.ts +392 -0
  48. package/dist/types-wnLasZaB.d.ts +1234 -0
  49. package/package.json +88 -0
package/README.md ADDED
@@ -0,0 +1,727 @@
1
+ # @cfast/ui
2
+
3
+ **Advanced component library for cfast. Data tables, page shells, permission-aware actions, and file uploads — all wired to the framework.**
4
+
5
+ `@cfast/ui` sits between your primitive component library (MUI Joy UI) and your application code. It provides the "smart" components that integrate with `@cfast/db`, `@cfast/actions`, `@cfast/permissions`, `@cfast/pagination`, `@cfast/auth`, and `@cfast/storage`. The headless core provides hooks and logic. UI library plugins provide the styled implementations.
6
+
7
+ ## Why This Exists
8
+
9
+ Primitive component libraries give you buttons, inputs, and cards. But every data-driven app ends up building the same things on top of them: sortable tables with pagination, filter bars that sync with the URL, page shells with breadcrumbs and action toolbars, file upload drop zones, toast notifications on action completion.
10
+
11
+ These patterns are not app-specific — they're framework-level. And in cfast, they're even more useful because they can integrate with the permission system, the pagination hooks, the storage schema, and the action pipeline automatically.
12
+
13
+ `@cfast/ui` provides these components so that `@cfast/admin` doesn't have to reinvent them (admin is a thin auto-generation layer on top of UI), and so that application code outside the admin panel can reuse the same patterns.
14
+
15
+ ## Design Decisions and Their Rationale
16
+
17
+ ### Why not keep this narrow (just ActionButton/PermissionGate)?
18
+
19
+ The original design scoped `@cfast/ui` to permission-aware wrappers only. In practice, the example app revealed that every route hand-codes the same data tables, empty states, date formatting, filter bars, and user menus. These patterns all benefit from cfast integration:
20
+
21
+ - A data table that accepts a `usePagination()` result and column definitions from a Drizzle schema
22
+ - A filter bar that serializes to URL search params and feeds into `@cfast/pagination` loaders
23
+ - An empty state that shows a "Create" CTA only when the user has permission
24
+ - A user menu that reads from `@cfast/auth` and displays role badges from `@cfast/permissions`
25
+
26
+ Keeping these in separate packages (or leaving them to apps) means the integrations get reimplemented in every project.
27
+
28
+ ### Why does @cfast/admin use @cfast/ui instead of having its own components?
29
+
30
+ Admin's job is **schema introspection → configuration**: it reads your Drizzle schema and generates the config for list views, detail views, and forms. The actual rendering is UI's job. This means:
31
+
32
+ - Apps that don't use the admin panel still get data tables, list views, and detail views
33
+ - Custom admin overrides use the same components as the rest of the app
34
+ - Admin stays thin and focused on auto-generation
35
+
36
+ ### Headless core + plugins
37
+
38
+ Same architecture as before, expanded to cover all component categories. The headless core (`@cfast/ui`) provides hooks, logic, and unstyled components. The Joy UI plugin (`@cfast/ui/joy`) provides styled implementations. Third-party plugins can add shadcn, Mantine, or any other library.
39
+
40
+ The plugin contract maps component slots to implementations:
41
+
42
+ ```typescript
43
+ createUIPlugin({
44
+ components: {
45
+ // Actions
46
+ button: MyButton,
47
+ tooltip: MyTooltip,
48
+ confirmDialog: MyConfirmDialog,
49
+ // Data display
50
+ table: MyTable,
51
+ tableHead: MyTableHead,
52
+ tableBody: MyTableBody,
53
+ tableRow: MyTableRow,
54
+ tableCell: MyTableCell,
55
+ chip: MyChip,
56
+ // Layout
57
+ appShell: MyAppShell,
58
+ sidebar: MySidebar,
59
+ pageContainer: MyPageContainer,
60
+ breadcrumb: MyBreadcrumb,
61
+ // Feedback
62
+ toast: MyToast,
63
+ alert: MyAlert,
64
+ // File
65
+ dropZone: MyDropZone,
66
+ },
67
+ });
68
+ ```
69
+
70
+ Plugins only need to implement the slots they care about. Missing slots fall back to the headless defaults (unstyled HTML elements).
71
+
72
+ ---
73
+
74
+ ## Data Display
75
+
76
+ ### `<DataTable>`
77
+
78
+ A table component that integrates with `@cfast/pagination`, `@cfast/db`, `@cfast/permissions`, and `@cfast/actions`.
79
+
80
+ ```typescript
81
+ import { DataTable } from "@cfast/ui/joy";
82
+ import { usePagination } from "@cfast/pagination/client";
83
+ import { posts } from "~/db/schema";
84
+
85
+ function PostsTable() {
86
+ const pagination = usePagination<Post>();
87
+
88
+ return (
89
+ <DataTable
90
+ data={pagination}
91
+ table={posts}
92
+ columns={["title", "author", "status", "createdAt"]}
93
+ actions={composed.client}
94
+ selectable
95
+ />
96
+ );
97
+ }
98
+ ```
99
+
100
+ **Features:**
101
+
102
+ | Feature | How it works |
103
+ |---|---|
104
+ | **Column inference** | Pass a Drizzle `table` — columns, types, and labels are derived from the schema. Override with the `columns` prop. |
105
+ | **Sorting** | Click column headers. Sort state syncs to URL search params via React Router. |
106
+ | **Row actions** | Pass `actions` from `@cfast/actions`. Each row gets an action menu. Actions are hidden/disabled based on permissions. |
107
+ | **Selection** | `selectable` enables row checkboxes. Selected rows feed into `<BulkActionBar>`. |
108
+ | **Typed cell rendering** | Column types determine the renderer: dates use `<DateField>`, booleans use `<BooleanField>`, etc. Override per-column with `render`. |
109
+ | **Loading state** | Shows skeleton rows while data is loading. |
110
+ | **Responsive** | Horizontal scroll on small screens. Priority columns stay visible. |
111
+
112
+ **Column configuration:**
113
+
114
+ ```typescript
115
+ <DataTable
116
+ data={pagination}
117
+ table={posts}
118
+ columns={[
119
+ "title", // string shorthand
120
+ { key: "author", label: "Written by" }, // custom label
121
+ { key: "published", render: (v) => v ? "Live" : "Draft" }, // custom render
122
+ { key: "createdAt", sortable: false }, // disable sorting
123
+ ]}
124
+ />
125
+ ```
126
+
127
+ ### `<FilterBar>`
128
+
129
+ URL-synced filter controls. Column types from Drizzle schema determine the filter input type.
130
+
131
+ ```typescript
132
+ import { FilterBar } from "@cfast/ui/joy";
133
+ import { posts } from "~/db/schema";
134
+
135
+ function PostFilters() {
136
+ return (
137
+ <FilterBar
138
+ table={posts}
139
+ filters={[
140
+ { column: "published", type: "select", options: [
141
+ { label: "Published", value: true },
142
+ { label: "Draft", value: false },
143
+ ]},
144
+ { column: "authorId", type: "relation", table: users, display: "name" },
145
+ { column: "createdAt", type: "dateRange" },
146
+ ]}
147
+ searchable={["title", "content"]}
148
+ />
149
+ );
150
+ }
151
+ ```
152
+
153
+ **How it works:**
154
+
155
+ 1. Each filter serializes its state to URL search params (e.g., `?published=true&author=abc`)
156
+ 2. In the loader, `@cfast/pagination`'s `parseParams()` reads these params
157
+ 3. The loader applies them as Drizzle `where` clauses
158
+ 4. On filter change, React Router navigates with the new params — no client state management
159
+
160
+ **Filter types:**
161
+
162
+ | Type | Input | Serialization |
163
+ |---|---|---|
164
+ | `text` | Text input | `?column=value` |
165
+ | `select` | Dropdown/chip group | `?column=value` |
166
+ | `multiSelect` | Multi-select dropdown | `?column=a,b,c` |
167
+ | `relation` | Async select (fetches related records) | `?column=id` |
168
+ | `dateRange` | Date range picker | `?column_from=...&column_to=...` |
169
+ | `boolean` | Toggle/chip | `?column=true` |
170
+ | `number` | Number range inputs | `?column_min=...&column_max=...` |
171
+
172
+ ### TypedField Components
173
+
174
+ Read-only display components that format values based on their type. Used by `<DataTable>` cell renderers and `<DetailView>` field layouts.
175
+
176
+ ```typescript
177
+ import { DateField, BooleanField, EmailField, ImageField, RelationField } from "@cfast/ui/joy";
178
+
179
+ <DateField value={post.createdAt} format="relative" />
180
+ // → "3 days ago"
181
+
182
+ <DateField value={post.createdAt} format="short" />
183
+ // → "Mar 11, 2026"
184
+
185
+ <BooleanField value={post.published} trueLabel="Published" falseLabel="Draft" />
186
+ // → Colored chip: "Published" (green) or "Draft" (neutral)
187
+
188
+ <EmailField value={user.email} />
189
+ // → Clickable mailto link
190
+
191
+ <ImageField value={post.coverImageKey} storage={storageConfig} />
192
+ // → Renders image from storage, handles signed URLs
193
+
194
+ <RelationField value={post.author} display="name" linkTo="/users/:id" />
195
+ // → Linked text showing the related record's display field
196
+ ```
197
+
198
+ **Available fields:**
199
+
200
+ | Field | Renders | Options |
201
+ |---|---|---|
202
+ | `TextField` | Plain text, truncated with tooltip | `maxLength`, `copyable` |
203
+ | `NumberField` | Formatted number | `locale`, `currency`, `decimals` |
204
+ | `BooleanField` | Chip/badge | `trueLabel`, `falseLabel`, `trueColor`, `falseColor` |
205
+ | `DateField` | Formatted date/time | `format: "short" \| "long" \| "relative" \| "datetime"` |
206
+ | `EmailField` | Mailto link | — |
207
+ | `UrlField` | External link with icon | `truncate` |
208
+ | `ImageField` | Image thumbnail | `width`, `height`, `storage` (for signed URLs) |
209
+ | `FileField` | File icon + name + size | `storage` |
210
+ | `RelationField` | Display field from related record | `display`, `linkTo` |
211
+ | `JsonField` | Formatted JSON | `collapsed` |
212
+
213
+ ### `<EmptyState>`
214
+
215
+ Permission-aware empty state. Shows different content based on whether the user can create records.
216
+
217
+ ```typescript
218
+ import { EmptyState } from "@cfast/ui/joy";
219
+
220
+ <EmptyState
221
+ title="No posts yet"
222
+ description="Create your first blog post to get started."
223
+ createAction={createPost.client}
224
+ createLabel="New Post"
225
+ icon={DocumentIcon}
226
+ />
227
+ ```
228
+
229
+ - If `createAction` is permitted → shows the CTA button
230
+ - If `createAction` is not permitted → shows only the title and description
231
+ - If `createAction` is invisible (no relation at all) → shows a generic "Nothing here" message
232
+
233
+ ---
234
+
235
+ ## Page Shells
236
+
237
+ ### `<ListView>`
238
+
239
+ A full page layout combining title, filters, data table, pagination, and empty state. This is the component `@cfast/admin` uses for every table view.
240
+
241
+ ```typescript
242
+ import { ListView } from "@cfast/ui/joy";
243
+
244
+ function PostsPage() {
245
+ const pagination = useOffsetPagination<Post>();
246
+
247
+ return (
248
+ <ListView
249
+ title="Blog Posts"
250
+ data={pagination}
251
+ table={posts}
252
+ columns={["title", "author", "published", "createdAt"]}
253
+ actions={composed.client}
254
+ filters={[
255
+ { column: "published", type: "select", options: publishedOptions },
256
+ ]}
257
+ searchable={["title", "content"]}
258
+ createAction={createPost.client}
259
+ selectable
260
+ bulkActions={[
261
+ { label: "Delete", action: bulkDelete.client, confirmation: "Delete selected posts?" },
262
+ { label: "Publish", action: bulkPublish.client },
263
+ ]}
264
+ />
265
+ );
266
+ }
267
+ ```
268
+
269
+ `<ListView>` composes `<PageContainer>`, `<FilterBar>`, `<DataTable>`, pagination controls, `<EmptyState>`, and `<BulkActionBar>`. It handles the loading/empty/data state transitions automatically.
270
+
271
+ ### `<DetailView>`
272
+
273
+ A read-only detail page for a single record. Auto-lays out fields based on Drizzle column types.
274
+
275
+ ```typescript
276
+ import { DetailView } from "@cfast/ui/joy";
277
+
278
+ function PostDetail({ post }: { post: Post }) {
279
+ return (
280
+ <DetailView
281
+ title={post.title}
282
+ table={posts}
283
+ record={post}
284
+ fields={["title", "content", "author", "published", "createdAt", "updatedAt"]}
285
+ actions={composed.client}
286
+ />
287
+ );
288
+ }
289
+ ```
290
+
291
+ **Features:**
292
+
293
+ - Fields render using the appropriate `TypedField` based on column type
294
+ - Action toolbar at the top (edit, delete, custom actions — permission-aware)
295
+ - Override individual fields: `fields={[..., { key: "content", render: (v) => <Markdown>{v}</Markdown> }]}`
296
+ - Exclude fields: `exclude={["id", "authorId"]}`
297
+
298
+ ### `<AppShell>`
299
+
300
+ Base layout with sidebar navigation, header, and content area.
301
+
302
+ ```typescript
303
+ import { AppShell } from "@cfast/ui/joy";
304
+
305
+ function Layout({ children }) {
306
+ return (
307
+ <AppShell
308
+ sidebar={<AppShell.Sidebar items={navigationItems} />}
309
+ header={<AppShell.Header userMenu={<UserMenu />} />}
310
+ >
311
+ {children}
312
+ </AppShell>
313
+ );
314
+ }
315
+ ```
316
+
317
+ **Sidebar navigation** can filter items based on permissions:
318
+
319
+ ```typescript
320
+ const navigationItems = [
321
+ { label: "Posts", to: "/posts", icon: DocumentIcon },
322
+ { label: "Users", to: "/users", icon: UsersIcon, action: manageUsers.client },
323
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
324
+ // Only shown if the user has permission for this action
325
+ ];
326
+ ```
327
+
328
+ ### `<PageContainer>`
329
+
330
+ Page wrapper with title, breadcrumb, tabs, and action toolbar. Used by `<ListView>` and `<DetailView>` internally, but also useful standalone.
331
+
332
+ ```typescript
333
+ import { PageContainer } from "@cfast/ui/joy";
334
+
335
+ <PageContainer
336
+ title="Edit Post"
337
+ breadcrumb={[
338
+ { label: "Posts", to: "/posts" },
339
+ { label: post.title },
340
+ ]}
341
+ actions={<ActionButton action={deletePost} input={{ postId }} />}
342
+ >
343
+ {/* page content */}
344
+ </PageContainer>
345
+ ```
346
+
347
+ ### `<UserMenu>`
348
+
349
+ Header dropdown showing current user info, role badge, and auth actions.
350
+
351
+ ```typescript
352
+ import { UserMenu } from "@cfast/ui/joy";
353
+
354
+ <UserMenu
355
+ // Reads user from @cfast/auth's useCurrentUser()
356
+ // Automatically shows:
357
+ // - Avatar with initials fallback
358
+ // - Name and email
359
+ // - Role badge
360
+ // - Impersonation indicator (if impersonating)
361
+ // - Links: Profile, Settings (configurable)
362
+ // - Sign out
363
+ links={[
364
+ { label: "Profile", to: "/profile" },
365
+ { label: "Admin", to: "/admin", action: adminAccess.client },
366
+ ]}
367
+ />
368
+ ```
369
+
370
+ ### `<NavigationProgress>`
371
+
372
+ Thin progress bar at the top of the page during React Router navigation.
373
+
374
+ ```typescript
375
+ import { NavigationProgress } from "@cfast/ui/joy";
376
+
377
+ // In your root layout:
378
+ <NavigationProgress />
379
+ ```
380
+
381
+ Uses `useNavigation().state` from React Router. Shows on `loading`, hides on `idle`. No configuration needed.
382
+
383
+ ---
384
+
385
+ ## Actions & Feedback
386
+
387
+ ### `<ActionButton>`
388
+
389
+ Permission-aware button wrapping a `@cfast/actions` action. *Already implemented in PR #6.*
390
+
391
+ ```typescript
392
+ import { ActionButton } from "@cfast/ui/joy";
393
+
394
+ <ActionButton
395
+ action={publishPost}
396
+ input={{ postId }}
397
+ whenForbidden="disable" // "hide" | "disable" | "show"
398
+ confirmation="Publish this post?"
399
+ >
400
+ Publish
401
+ </ActionButton>
402
+ ```
403
+
404
+ ### `<PermissionGate>`
405
+
406
+ Conditionally renders children based on action permissions. *Already implemented in PR #6.*
407
+
408
+ ```typescript
409
+ <PermissionGate action={editPost} input={{ postId }} fallback={<ReadOnlyBanner />}>
410
+ <EditToolbar />
411
+ </PermissionGate>
412
+ ```
413
+
414
+ ### `<ConfirmDialog>`
415
+
416
+ Standalone confirmation dialog. Used internally by `<ActionButton>` when `confirmation` is set, but also available directly.
417
+
418
+ ```typescript
419
+ import { ConfirmDialog, useConfirm } from "@cfast/ui/joy";
420
+
421
+ function DangerZone() {
422
+ const confirm = useConfirm();
423
+
424
+ async function handleDelete() {
425
+ const confirmed = await confirm({
426
+ title: "Delete account",
427
+ description: "This action cannot be undone.",
428
+ confirmLabel: "Delete",
429
+ variant: "danger",
430
+ });
431
+ if (confirmed) { /* proceed */ }
432
+ }
433
+
434
+ return <Button onClick={handleDelete}>Delete Account</Button>;
435
+ }
436
+ ```
437
+
438
+ ### `useToast()` and `<ToastProvider>`
439
+
440
+ Action result notifications. Wraps a toast library (Sonner for Joy UI plugin) with cfast-aware defaults.
441
+
442
+ ```typescript
443
+ import { ToastProvider, useToast } from "@cfast/ui/joy";
444
+
445
+ // In root layout:
446
+ <ToastProvider />
447
+
448
+ // In components — manual usage:
449
+ const toast = useToast();
450
+ toast.success("Post published");
451
+ toast.error("Failed to delete post");
452
+
453
+ // Automatic: useActionToast wires to @cfast/actions results
454
+ import { useActionToast } from "@cfast/ui";
455
+
456
+ useActionToast(composed.client, {
457
+ deletePost: { success: "Post deleted", error: "Failed to delete" },
458
+ publishPost: { success: "Post published" },
459
+ });
460
+ ```
461
+
462
+ ### `<FormStatus>`
463
+
464
+ Displays action result feedback (success/error messages) in a consistent format.
465
+
466
+ ```typescript
467
+ import { FormStatus } from "@cfast/ui/joy";
468
+
469
+ function EditForm() {
470
+ const actionData = useActionData();
471
+
472
+ return (
473
+ <Form method="post">
474
+ <FormStatus data={actionData} />
475
+ {/* form fields */}
476
+ </Form>
477
+ );
478
+ }
479
+ ```
480
+
481
+ Renders success messages as green alerts, error messages as red alerts, and validation errors as a field-keyed list. Replaces the hand-coded `<Box sx={{ bgcolor: "danger.softBg" }}>` pattern repeated throughout the example app.
482
+
483
+ ### `<BulkActionBar>`
484
+
485
+ Toolbar that appears when rows are selected in a `<DataTable>`. Shows selected count and permitted bulk actions.
486
+
487
+ ```typescript
488
+ <BulkActionBar
489
+ selected={selectedRows}
490
+ actions={[
491
+ { label: "Delete", action: bulkDelete.client, confirmation: "Delete {count} posts?" },
492
+ { label: "Publish", action: bulkPublish.client },
493
+ { label: "Export CSV", handler: (rows) => exportCsv(rows) },
494
+ ]}
495
+ onClear={() => clearSelection()}
496
+ />
497
+ ```
498
+
499
+ Actions are permission-aware — hidden if the user can't perform them.
500
+
501
+ ---
502
+
503
+ ## File Display
504
+
505
+ ### `<DropZone>`
506
+
507
+ Drag-and-drop file upload area. Integrates with `@cfast/storage`'s schema for validation and `useUpload` for upload progress.
508
+
509
+ ```typescript
510
+ import { DropZone } from "@cfast/ui/joy";
511
+ import { useUpload } from "@cfast/storage/client";
512
+
513
+ function CoverImageUpload() {
514
+ const upload = useUpload("postCoverImage");
515
+
516
+ return (
517
+ <DropZone
518
+ upload={upload}
519
+ // Inherits accept and maxSize from storage schema
520
+ // Shows: drag state, file preview, validation errors, upload progress
521
+ />
522
+ );
523
+ }
524
+ ```
525
+
526
+ **States:**
527
+
528
+ | State | Display |
529
+ |---|---|
530
+ | Idle | Dashed border, "Drag files here or click to browse" |
531
+ | Drag over (valid) | Highlighted border, "Drop to upload" |
532
+ | Drag over (invalid) | Red border, "File type not accepted" |
533
+ | Uploading | Progress bar, file name, cancel button |
534
+ | Complete | File preview (image thumbnail or file icon + name) |
535
+ | Error | Red alert with error message, retry button |
536
+
537
+ ### `<ImagePreview>`
538
+
539
+ Displays an image from `@cfast/storage` with signed URL handling.
540
+
541
+ ```typescript
542
+ import { ImagePreview } from "@cfast/ui/joy";
543
+
544
+ <ImagePreview
545
+ fileKey={post.coverImageKey}
546
+ storage={storageConfig}
547
+ width={200}
548
+ height={150}
549
+ fallback={<PlaceholderImage />}
550
+ />
551
+ ```
552
+
553
+ ### `<FileList>`
554
+
555
+ A list of uploaded files with metadata, download links, and delete actions.
556
+
557
+ ```typescript
558
+ import { FileList } from "@cfast/ui/joy";
559
+
560
+ <FileList
561
+ files={post.attachments}
562
+ storage={storageConfig}
563
+ deleteAction={deleteAttachment.client}
564
+ // Shows: file icon, name, size, download link, delete button (permission-aware)
565
+ />
566
+ ```
567
+
568
+ ---
569
+
570
+ ## Utilities
571
+
572
+ ### `<AvatarWithInitials>`
573
+
574
+ Avatar component with automatic initials fallback when no image is available.
575
+
576
+ ```typescript
577
+ import { AvatarWithInitials } from "@cfast/ui/joy";
578
+
579
+ <AvatarWithInitials
580
+ src={user.avatarUrl}
581
+ name={user.name}
582
+ size="sm"
583
+ />
584
+ // Shows image if src is available, otherwise shows "DS" for "Daniel Schmidt"
585
+ ```
586
+
587
+ ### `<RoleBadge>`
588
+
589
+ Colored badge displaying a user's role. Colors configurable per role.
590
+
591
+ ```typescript
592
+ import { RoleBadge } from "@cfast/ui/joy";
593
+
594
+ <RoleBadge role={user.role} />
595
+ // → Colored chip: "Admin" (red), "Editor" (blue), "Reader" (neutral)
596
+
597
+ // Custom colors:
598
+ <RoleBadge
599
+ role={user.role}
600
+ colors={{ admin: "danger", editor: "primary", reader: "neutral" }}
601
+ />
602
+ ```
603
+
604
+ ### `<ImpersonationBanner>`
605
+
606
+ Persistent banner shown when an admin is impersonating another user.
607
+
608
+ ```typescript
609
+ import { ImpersonationBanner } from "@cfast/ui/joy";
610
+
611
+ // In root layout:
612
+ <ImpersonationBanner />
613
+ // Shows: "Viewing as user@example.com" with a "Stop Impersonating" button
614
+ // Hidden when not impersonating
615
+ // Reads impersonation state from @cfast/auth
616
+ ```
617
+
618
+ ---
619
+
620
+ ## Architecture
621
+
622
+ ```
623
+ @cfast/ui (headless core)
624
+ ├── Hooks
625
+ │ ├── useActionStatus — permission status for a single action
626
+ │ ├── useToast — toast notification imperative API
627
+ │ ├── useActionToast — auto-toast on action results
628
+ │ ├── useConfirm — imperative confirmation dialog
629
+ │ └── useColumnInference — derive columns from Drizzle schema
630
+
631
+ ├── Headless components
632
+ │ ├── PermissionGate — conditional render on permissions
633
+ │ ├── EmptyState — permission-aware empty state logic
634
+ │ └── BulkActionBar — selection + action permission logic
635
+
636
+ ├── Typed fields (headless)
637
+ │ ├── DateField, BooleanField, NumberField, TextField
638
+ │ ├── EmailField, UrlField, ImageField, FileField
639
+ │ ├── RelationField, JsonField
640
+ │ └── fieldForColumn() — maps Drizzle column type → field component
641
+
642
+ └── Plugin API
643
+ └── createUIPlugin() — maps component slots to implementations
644
+
645
+ @cfast/ui/joy (MUI Joy UI plugin)
646
+ ├── All of the above, styled with Joy UI
647
+ ├── DataTable — Joy Table + sorting + selection
648
+ ├── FilterBar — Joy Input/Select/DatePicker filters
649
+ ├── ListView — composed page shell
650
+ ├── DetailView — composed detail page
651
+ ├── AppShell — Joy sidebar + header layout
652
+ ├── PageContainer — title + breadcrumb + actions wrapper
653
+ ├── UserMenu — Joy Dropdown + Avatar + Menu
654
+ ├── NavigationProgress — thin progress bar
655
+ ├── ActionButton — Joy Button + Tooltip + loading
656
+ ├── ConfirmDialog — Joy Modal
657
+ ├── ToastProvider — Sonner integration
658
+ ├── FormStatus — Joy Alert for action results
659
+ ├── DropZone — drag-and-drop upload area
660
+ ├── ImagePreview — image display with signed URLs
661
+ ├── FileList — file list with actions
662
+ ├── AvatarWithInitials — Joy Avatar + initials
663
+ ├── RoleBadge — Joy Chip with role colors
664
+ └── ImpersonationBanner — Joy Alert banner
665
+ ```
666
+
667
+ ## Exports
668
+
669
+ Server/shared (`@cfast/ui`):
670
+
671
+ ```typescript
672
+ // Hooks
673
+ export { useActionStatus } from "./hooks/use-action-status.js";
674
+ export { useToast, useActionToast } from "./hooks/use-toast.js";
675
+ export { useConfirm } from "./hooks/use-confirm.js";
676
+ export { useColumnInference } from "./hooks/use-column-inference.js";
677
+
678
+ // Headless components
679
+ export { PermissionGate } from "./components/permission-gate.js";
680
+ export { EmptyState } from "./components/empty-state.js";
681
+ export { BulkActionBar } from "./components/bulk-action-bar.js";
682
+
683
+ // Typed fields
684
+ export {
685
+ DateField, BooleanField, NumberField, TextField,
686
+ EmailField, UrlField, ImageField, FileField,
687
+ RelationField, JsonField, fieldForColumn,
688
+ } from "./fields/index.js";
689
+
690
+ // Plugin API
691
+ export { createUIPlugin } from "./plugin.js";
692
+
693
+ // Types
694
+ export type {
695
+ UIPlugin, DataTableProps, FilterBarProps, ListViewProps,
696
+ DetailViewProps, FieldProps, ColumnDef, FilterDef,
697
+ } from "./types.js";
698
+ ```
699
+
700
+ Joy UI plugin (`@cfast/ui/joy`):
701
+
702
+ ```typescript
703
+ export {
704
+ // Data display
705
+ DataTable, FilterBar, ListView, DetailView,
706
+ // Layout
707
+ AppShell, PageContainer, UserMenu, NavigationProgress,
708
+ // Actions & feedback
709
+ ActionButton, PermissionGate, ConfirmDialog,
710
+ ToastProvider, FormStatus, BulkActionBar,
711
+ // File
712
+ DropZone, ImagePreview, FileList,
713
+ // Utilities
714
+ AvatarWithInitials, RoleBadge, ImpersonationBanner,
715
+ } from "./joy/index.js";
716
+ ```
717
+
718
+ ## Integration with Other @cfast Packages
719
+
720
+ - **`@cfast/actions`** — `ActionButton`, `PermissionGate`, `BulkActionBar`, `useActionToast` all consume action descriptors and permission status from `useActions()`.
721
+ - **`@cfast/permissions`** — Permission status drives hide/disable/show behavior. Sidebar navigation filters items by permission. `EmptyState` adapts its CTA based on create permissions.
722
+ - **`@cfast/pagination`** — `DataTable` and `ListView` accept pagination hook results (`usePagination`, `useOffsetPagination`, `useInfiniteScroll`). `FilterBar` serializes to URL params that `parseParams()` reads.
723
+ - **`@cfast/db`** — Drizzle schema metadata drives column inference in `DataTable`, filter type inference in `FilterBar`, and field type inference in `DetailView`. `useColumnInference()` maps Drizzle column types to field components.
724
+ - **`@cfast/auth`** — `UserMenu` reads `useCurrentUser()`. `ImpersonationBanner` reads impersonation state. `RoleBadge` displays role names.
725
+ - **`@cfast/storage`** — `DropZone` integrates with `useUpload()`. `ImagePreview` and `FileList` use storage URLs. `ImageField` resolves signed URLs from storage config.
726
+ - **`@cfast/forms`** — `FormStatus` displays validation errors from form submissions. `ListView`'s create flow can open an `AutoForm` modal.
727
+ - **`@cfast/admin`** — Admin uses `ListView`, `DetailView`, `DataTable`, `FilterBar`, `AppShell`, and all other UI components. Admin's job is schema → config, UI's job is config → pixels.