@cfast/joy 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.
package/dist/index.js ADDED
@@ -0,0 +1,1151 @@
1
+ // src/action-button.tsx
2
+ import "react";
3
+ import JoyButton from "@mui/joy/Button";
4
+ import JoyTooltip from "@mui/joy/Tooltip";
5
+ import { jsx } from "react/jsx-runtime";
6
+ function ActionButton({
7
+ action,
8
+ children,
9
+ whenForbidden = "disable",
10
+ confirmation: _confirmation,
11
+ variant = "solid",
12
+ color = "primary",
13
+ size = "md",
14
+ ...buttonProps
15
+ }) {
16
+ if (action.invisible) {
17
+ return null;
18
+ }
19
+ if (!action.permitted && whenForbidden === "hide") {
20
+ return null;
21
+ }
22
+ const disabled = !action.permitted && whenForbidden === "disable";
23
+ const button = /* @__PURE__ */ jsx(
24
+ JoyButton,
25
+ {
26
+ ...buttonProps,
27
+ onClick: () => action.submit(),
28
+ disabled,
29
+ loading: action.pending,
30
+ variant,
31
+ color,
32
+ size,
33
+ children
34
+ }
35
+ );
36
+ if (disabled && action.reason) {
37
+ const wrapper = /* @__PURE__ */ jsx("span", { children: button });
38
+ return /* @__PURE__ */ jsx(JoyTooltip, { title: action.reason, children: wrapper });
39
+ }
40
+ return button;
41
+ }
42
+
43
+ // src/confirm-dialog.tsx
44
+ import "react";
45
+ import Modal from "@mui/joy/Modal";
46
+ import ModalDialog from "@mui/joy/ModalDialog";
47
+ import Typography from "@mui/joy/Typography";
48
+ import Button from "@mui/joy/Button";
49
+ import Stack from "@mui/joy/Stack";
50
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
51
+ function ConfirmDialog({
52
+ open,
53
+ onClose,
54
+ onConfirm,
55
+ title,
56
+ description,
57
+ confirmLabel = "Confirm",
58
+ cancelLabel = "Cancel",
59
+ variant = "default"
60
+ }) {
61
+ const dialog = /* @__PURE__ */ jsxs(ModalDialog, { variant: "outlined", role: "alertdialog", sx: { maxWidth: 400 }, children: [
62
+ /* @__PURE__ */ jsx2(Typography, { level: "h4", children: title }),
63
+ description ? /* @__PURE__ */ jsx2(Typography, { level: "body-md", sx: { mt: 1 }, children: description }) : null,
64
+ /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, justifyContent: "flex-end", sx: { mt: 2 }, children: [
65
+ /* @__PURE__ */ jsx2(Button, { variant: "plain", color: "neutral", onClick: onClose, children: cancelLabel }),
66
+ /* @__PURE__ */ jsx2(
67
+ Button,
68
+ {
69
+ variant: "solid",
70
+ color: variant === "danger" ? "danger" : "primary",
71
+ onClick: onConfirm,
72
+ children: confirmLabel
73
+ }
74
+ )
75
+ ] })
76
+ ] });
77
+ return /* @__PURE__ */ jsx2(Modal, { open, onClose, children: dialog });
78
+ }
79
+
80
+ // src/toast-provider.tsx
81
+ import { useCallback } from "react";
82
+ import { toast, Toaster } from "sonner";
83
+ import { ToastContext } from "@cfast/ui";
84
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
85
+ function ToastProvider({ children }) {
86
+ const show = useCallback((options) => {
87
+ const method = options.type ?? "info";
88
+ switch (method) {
89
+ case "success":
90
+ toast.success(options.message, { description: options.description, duration: options.duration });
91
+ break;
92
+ case "error":
93
+ toast.error(options.message, { description: options.description, duration: options.duration });
94
+ break;
95
+ case "warning":
96
+ toast.warning(options.message, { description: options.description, duration: options.duration });
97
+ break;
98
+ default:
99
+ toast.info(options.message, { description: options.description, duration: options.duration });
100
+ break;
101
+ }
102
+ }, []);
103
+ return /* @__PURE__ */ jsxs2(ToastContext.Provider, { value: { show }, children: [
104
+ children,
105
+ /* @__PURE__ */ jsx3(Toaster, { richColors: true, position: "bottom-right" })
106
+ ] });
107
+ }
108
+
109
+ // src/form-status.tsx
110
+ import "react";
111
+ import Alert from "@mui/joy/Alert";
112
+ import Stack2 from "@mui/joy/Stack";
113
+ import { jsx as jsx4 } from "react/jsx-runtime";
114
+ function FormStatus({ data }) {
115
+ if (!data) return null;
116
+ const elements = [];
117
+ if (data.success) {
118
+ elements.push(
119
+ /* @__PURE__ */ jsx4(Alert, { color: "success", variant: "soft", children: data.success }, "success")
120
+ );
121
+ }
122
+ if (data.error) {
123
+ elements.push(
124
+ /* @__PURE__ */ jsx4(Alert, { color: "danger", variant: "soft", children: data.error }, "error")
125
+ );
126
+ }
127
+ if (data.fieldErrors) {
128
+ const errorMessages = Object.entries(data.fieldErrors).flatMap(
129
+ ([field, errors]) => errors.map((err) => `${field}: ${err}`)
130
+ );
131
+ if (errorMessages.length > 0) {
132
+ elements.push(
133
+ /* @__PURE__ */ jsx4(Alert, { color: "danger", variant: "soft", children: /* @__PURE__ */ jsx4("ul", { style: { margin: 0, paddingLeft: "16px" }, children: errorMessages.map((msg, i) => /* @__PURE__ */ jsx4("li", { children: msg }, i)) }) }, "field-errors")
134
+ );
135
+ }
136
+ }
137
+ if (elements.length === 0) return null;
138
+ return /* @__PURE__ */ jsx4(Stack2, { spacing: 1, children: elements });
139
+ }
140
+
141
+ // src/empty-state.tsx
142
+ import "react";
143
+ import Typography2 from "@mui/joy/Typography";
144
+ import Button2 from "@mui/joy/Button";
145
+ import Stack3 from "@mui/joy/Stack";
146
+ import { useActionStatus } from "@cfast/ui";
147
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
148
+ function EmptyState({
149
+ title,
150
+ description,
151
+ createAction,
152
+ createLabel = "Create",
153
+ icon: Icon
154
+ }) {
155
+ if (!createAction) {
156
+ return /* @__PURE__ */ jsxs3(Stack3, { alignItems: "center", spacing: 2, sx: { py: 6, px: 2 }, children: [
157
+ Icon ? /* @__PURE__ */ jsx5(Icon, { className: "empty-state-icon" }) : null,
158
+ /* @__PURE__ */ jsx5(Typography2, { level: "h3", children: title }),
159
+ description ? /* @__PURE__ */ jsx5(Typography2, { level: "body-md", color: "neutral", children: description }) : null
160
+ ] });
161
+ }
162
+ return /* @__PURE__ */ jsx5(
163
+ EmptyStateWithAction,
164
+ {
165
+ title,
166
+ description,
167
+ createAction,
168
+ createLabel,
169
+ icon: Icon
170
+ }
171
+ );
172
+ }
173
+ function EmptyStateWithAction({
174
+ title,
175
+ description,
176
+ createAction,
177
+ createLabel,
178
+ icon: Icon
179
+ }) {
180
+ const status = useActionStatus(createAction);
181
+ if (status.invisible) {
182
+ return /* @__PURE__ */ jsx5(Stack3, { alignItems: "center", spacing: 2, sx: { py: 6, px: 2 }, children: /* @__PURE__ */ jsx5(Typography2, { level: "h3", children: "Nothing here yet" }) });
183
+ }
184
+ return /* @__PURE__ */ jsxs3(Stack3, { alignItems: "center", spacing: 2, sx: { py: 6, px: 2 }, children: [
185
+ Icon ? /* @__PURE__ */ jsx5(Icon, { className: "empty-state-icon" }) : null,
186
+ /* @__PURE__ */ jsx5(Typography2, { level: "h3", children: title }),
187
+ description ? /* @__PURE__ */ jsx5(Typography2, { level: "body-md", color: "neutral", children: description }) : null,
188
+ status.permitted ? /* @__PURE__ */ jsx5(Button2, { onClick: () => status.submit(), loading: status.pending, children: createLabel }) : null
189
+ ] });
190
+ }
191
+
192
+ // src/navigation-progress.tsx
193
+ import "react";
194
+ import LinearProgress from "@mui/joy/LinearProgress";
195
+ import { useNavigation } from "react-router";
196
+ import { jsx as jsx6 } from "react/jsx-runtime";
197
+ function NavigationProgress() {
198
+ const navigation = useNavigation();
199
+ const isNavigating = navigation.state === "loading";
200
+ if (!isNavigating) {
201
+ return null;
202
+ }
203
+ return /* @__PURE__ */ jsx6(
204
+ LinearProgress,
205
+ {
206
+ sx: {
207
+ position: "fixed",
208
+ top: 0,
209
+ left: 0,
210
+ right: 0,
211
+ zIndex: 9999,
212
+ "--LinearProgress-radius": "0px",
213
+ "--LinearProgress-thickness": "3px"
214
+ }
215
+ }
216
+ );
217
+ }
218
+
219
+ // src/page-container.tsx
220
+ import "react";
221
+ import Typography3 from "@mui/joy/Typography";
222
+ import Breadcrumbs from "@mui/joy/Breadcrumbs";
223
+ import { Link } from "react-router";
224
+ import Stack4 from "@mui/joy/Stack";
225
+ import { Fragment, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
226
+ function PageContainer({
227
+ title,
228
+ breadcrumb,
229
+ actions,
230
+ tabs: _tabs,
231
+ children
232
+ }) {
233
+ return /* @__PURE__ */ jsxs4("div", { style: { padding: "16px 24px" }, children: [
234
+ breadcrumb && breadcrumb.length > 0 ? /* @__PURE__ */ jsx7(Breadcrumbs, { size: "sm", sx: { mb: 1, p: 0 }, children: breadcrumb.map(
235
+ (item, i) => item.to ? /* @__PURE__ */ jsx7(Link, { to: item.to, style: { textDecoration: "none", color: "inherit" }, children: item.label }, i) : /* @__PURE__ */ jsx7(Typography3, { fontSize: "sm", children: item.label }, i)
236
+ ) }) : null,
237
+ title || actions ? /* @__PURE__ */ jsxs4(Stack4, { direction: "row", justifyContent: "space-between", alignItems: "center", sx: { mb: 2 }, children: [
238
+ title ? /* @__PURE__ */ jsx7(Typography3, { level: "h2", children: title }) : null,
239
+ actions ? /* @__PURE__ */ jsx7(Fragment, { children: actions }) : null
240
+ ] }) : null,
241
+ children
242
+ ] });
243
+ }
244
+
245
+ // src/app-shell.tsx
246
+ import "react";
247
+ import Sheet from "@mui/joy/Sheet";
248
+ import List from "@mui/joy/List";
249
+ import ListItem from "@mui/joy/ListItem";
250
+ import ListItemButton from "@mui/joy/ListItemButton";
251
+ import { Link as Link2 } from "react-router";
252
+ import { useActionStatus as useActionStatus2 } from "@cfast/ui";
253
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
254
+ var AnyListItemButton = ListItemButton;
255
+ var AnySheet = Sheet;
256
+ function AppShell({
257
+ children,
258
+ sidebar,
259
+ header
260
+ }) {
261
+ return /* @__PURE__ */ jsxs5("div", { style: { display: "flex", minHeight: "100vh" }, children: [
262
+ sidebar ?? null,
263
+ /* @__PURE__ */ jsxs5("div", { style: { flex: 1, display: "flex", flexDirection: "column" }, children: [
264
+ header ?? null,
265
+ /* @__PURE__ */ jsx8("main", { style: { flex: 1 }, children })
266
+ ] })
267
+ ] });
268
+ }
269
+ function AppShellSidebar({ items }) {
270
+ return /* @__PURE__ */ jsx8(
271
+ Sheet,
272
+ {
273
+ sx: {
274
+ width: 240,
275
+ borderRight: "1px solid",
276
+ borderColor: "divider",
277
+ p: 2
278
+ },
279
+ children: /* @__PURE__ */ jsx8(List, { size: "sm", children: items.map((item) => /* @__PURE__ */ jsx8(SidebarItem, { item }, item.to)) })
280
+ }
281
+ );
282
+ }
283
+ function SidebarItem({ item }) {
284
+ if (item.action) {
285
+ return /* @__PURE__ */ jsx8(PermissionFilteredItem, { item });
286
+ }
287
+ return /* @__PURE__ */ jsx8(ListItem, { children: /* @__PURE__ */ jsx8(AnyListItemButton, { component: Link2, to: item.to, children: item.label }) });
288
+ }
289
+ function PermissionFilteredItem({ item }) {
290
+ const status = useActionStatus2(item.action);
291
+ if (status.invisible || !status.permitted) {
292
+ return null;
293
+ }
294
+ return /* @__PURE__ */ jsx8(ListItem, { children: /* @__PURE__ */ jsx8(AnyListItemButton, { component: Link2, to: item.to, children: item.label }) });
295
+ }
296
+ function AppShellHeader({
297
+ children,
298
+ userMenu
299
+ }) {
300
+ return /* @__PURE__ */ jsxs5(
301
+ AnySheet,
302
+ {
303
+ component: "header",
304
+ sx: {
305
+ display: "flex",
306
+ alignItems: "center",
307
+ justifyContent: "space-between",
308
+ px: 3,
309
+ py: 1.5,
310
+ borderBottom: "1px solid",
311
+ borderColor: "divider"
312
+ },
313
+ children: [
314
+ children ?? /* @__PURE__ */ jsx8(Fragment2, {}),
315
+ userMenu ?? null
316
+ ]
317
+ }
318
+ );
319
+ }
320
+ AppShell.Sidebar = AppShellSidebar;
321
+ AppShell.Header = AppShellHeader;
322
+
323
+ // src/user-menu.tsx
324
+ import "react";
325
+ import Avatar from "@mui/joy/Avatar";
326
+ import Dropdown from "@mui/joy/Dropdown";
327
+ import Menu from "@mui/joy/Menu";
328
+ import MenuItem from "@mui/joy/MenuItem";
329
+ import MenuButton from "@mui/joy/MenuButton";
330
+ import IconButton from "@mui/joy/IconButton";
331
+ import { Link as Link3 } from "react-router";
332
+ import { useCurrentUser } from "@cfast/auth/client";
333
+ import { getInitials, useActionStatus as useActionStatus3 } from "@cfast/ui";
334
+
335
+ // src/role-badge.tsx
336
+ import "react";
337
+ import Chip from "@mui/joy/Chip";
338
+ import { jsx as jsx9 } from "react/jsx-runtime";
339
+ var VALID_COLORS = /* @__PURE__ */ new Set(["primary", "neutral", "danger", "success", "warning"]);
340
+ var defaultColors = {
341
+ admin: "danger",
342
+ editor: "primary",
343
+ author: "success",
344
+ reader: "neutral"
345
+ };
346
+ function isColorPaletteProp(value) {
347
+ return VALID_COLORS.has(value);
348
+ }
349
+ function RoleBadge({ role, colors }) {
350
+ const rawColor = colors?.[role] ?? defaultColors[role] ?? "neutral";
351
+ const chipColor = isColorPaletteProp(rawColor) ? rawColor : "neutral";
352
+ return /* @__PURE__ */ jsx9(Chip, { size: "sm", variant: "soft", color: chipColor, children: role });
353
+ }
354
+
355
+ // src/user-menu.tsx
356
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
357
+ var AnyMenuItem = MenuItem;
358
+ function UserMenu({
359
+ links = [],
360
+ onSignOut
361
+ }) {
362
+ const user = useCurrentUser();
363
+ if (!user) {
364
+ return null;
365
+ }
366
+ return /* @__PURE__ */ jsxs6(Dropdown, { children: [
367
+ /* @__PURE__ */ jsx10(MenuButton, { slots: { root: IconButton }, slotProps: { root: { variant: "plain", size: "sm" } }, children: /* @__PURE__ */ jsx10(Avatar, { src: user.avatarUrl ?? void 0, size: "sm", children: getInitials(user.name) }) }),
368
+ /* @__PURE__ */ jsxs6(Menu, { placement: "bottom-end", size: "sm", children: [
369
+ /* @__PURE__ */ jsx10(MenuItem, { disabled: true, children: /* @__PURE__ */ jsxs6("div", { children: [
370
+ /* @__PURE__ */ jsx10("strong", { children: user.name }),
371
+ /* @__PURE__ */ jsx10("br", {}),
372
+ /* @__PURE__ */ jsx10("small", { children: user.email })
373
+ ] }) }),
374
+ user.roles.length > 0 ? /* @__PURE__ */ jsx10(MenuItem, { disabled: true, children: /* @__PURE__ */ jsx10("div", { style: { display: "flex", gap: "4px" }, children: user.roles.map((role) => /* @__PURE__ */ jsx10(RoleBadge, { role }, role)) }) }) : null,
375
+ links.map(
376
+ (link) => link.action ? /* @__PURE__ */ jsx10(PermissionFilteredLink, { link }, link.to) : /* @__PURE__ */ jsx10(AnyMenuItem, { component: Link3, to: link.to, children: link.label }, link.to)
377
+ ),
378
+ onSignOut ? /* @__PURE__ */ jsx10(MenuItem, { onClick: onSignOut, children: "Sign out" }) : null
379
+ ] })
380
+ ] });
381
+ }
382
+ function PermissionFilteredLink({ link }) {
383
+ const status = useActionStatus3(link.action);
384
+ if (status.invisible || !status.permitted) {
385
+ return null;
386
+ }
387
+ return /* @__PURE__ */ jsx10(AnyMenuItem, { component: Link3, to: link.to, children: link.label });
388
+ }
389
+
390
+ // src/data-table.tsx
391
+ import { useState, useCallback as useCallback2 } from "react";
392
+ import JoyTable from "@mui/joy/Table";
393
+ import JoySheet from "@mui/joy/Sheet";
394
+ import JoyCheckbox from "@mui/joy/Checkbox";
395
+ import { getField, getRecordId } from "@cfast/ui";
396
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
397
+ function normalizeColumns(columns) {
398
+ if (!columns) return [];
399
+ return columns.map((col) => {
400
+ if (typeof col === "string") {
401
+ return {
402
+ key: col,
403
+ label: col.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim(),
404
+ sortable: true
405
+ };
406
+ }
407
+ return col;
408
+ });
409
+ }
410
+ function DataTable({
411
+ data,
412
+ columns: columnsProp,
413
+ selectable = false,
414
+ selectedRows: externalSelectedRows,
415
+ onSelectionChange,
416
+ onRowClick,
417
+ getRowId,
418
+ emptyMessage = "No data"
419
+ }) {
420
+ const columns = normalizeColumns(columnsProp);
421
+ const [sortKey, setSortKey] = useState(null);
422
+ const [sortDir, setSortDir] = useState("asc");
423
+ const [internalSelected, setInternalSelected] = useState(/* @__PURE__ */ new Set());
424
+ const getId = getRowId ?? defaultGetId;
425
+ const selectedSet = externalSelectedRows ? new Set(externalSelectedRows.map((r) => getId(r))) : internalSelected;
426
+ const handleSort = useCallback2((key) => {
427
+ if (sortKey === key) {
428
+ setSortDir((d) => d === "asc" ? "desc" : "asc");
429
+ } else {
430
+ setSortKey(key);
431
+ setSortDir("asc");
432
+ }
433
+ }, [sortKey]);
434
+ const toggleRow = useCallback2((id) => {
435
+ if (onSelectionChange) {
436
+ const row = data.items.find((r) => getId(r) === id);
437
+ if (!row) return;
438
+ const current = externalSelectedRows ?? [];
439
+ const isSelected = current.some((r) => getId(r) === id);
440
+ onSelectionChange(isSelected ? current.filter((r) => getId(r) !== id) : [...current, row]);
441
+ } else {
442
+ setInternalSelected((prev) => {
443
+ const next = new Set(prev);
444
+ if (next.has(id)) next.delete(id);
445
+ else next.add(id);
446
+ return next;
447
+ });
448
+ }
449
+ }, [data.items, externalSelectedRows, onSelectionChange, getId]);
450
+ if (data.items.length === 0 && !data.isLoading) {
451
+ return /* @__PURE__ */ jsx11("div", { style: { textAlign: "center", padding: "32px", color: "#666" }, children: emptyMessage });
452
+ }
453
+ return /* @__PURE__ */ jsx11(JoySheet, { variant: "outlined", sx: { borderRadius: "sm", overflow: "auto" }, children: /* @__PURE__ */ jsxs7(JoyTable, { hoverRow: true, sx: { "& th": { fontWeight: "lg" } }, children: [
454
+ /* @__PURE__ */ jsx11("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
455
+ selectable ? /* @__PURE__ */ jsx11("th", { style: { width: 40 } }) : null,
456
+ columns.map((col) => /* @__PURE__ */ jsx11("th", { children: col.sortable !== false ? /* @__PURE__ */ jsxs7(
457
+ "button",
458
+ {
459
+ onClick: () => handleSort(col.key),
460
+ style: {
461
+ background: "none",
462
+ border: "none",
463
+ cursor: "pointer",
464
+ fontWeight: "bold",
465
+ font: "inherit",
466
+ padding: 0,
467
+ color: "inherit"
468
+ },
469
+ children: [
470
+ col.label ?? col.key,
471
+ sortKey === col.key ? sortDir === "asc" ? " \u2191" : " \u2193" : null
472
+ ]
473
+ }
474
+ ) : col.label ?? col.key }, col.key))
475
+ ] }) }),
476
+ /* @__PURE__ */ jsx11("tbody", { children: data.items.map((row) => {
477
+ const id = getId(row);
478
+ const isSelected = selectedSet.has(id);
479
+ return /* @__PURE__ */ jsxs7(
480
+ "tr",
481
+ {
482
+ onClick: onRowClick ? () => onRowClick(row) : void 0,
483
+ style: onRowClick ? { cursor: "pointer" } : void 0,
484
+ children: [
485
+ selectable ? /* @__PURE__ */ jsx11("td", { children: /* @__PURE__ */ jsx11(
486
+ JoyCheckbox,
487
+ {
488
+ checked: isSelected,
489
+ onChange: () => toggleRow(id),
490
+ size: "sm"
491
+ }
492
+ ) }) : null,
493
+ columns.map((col) => {
494
+ const value = getField(row, col.key);
495
+ return /* @__PURE__ */ jsx11("td", { children: col.render ? col.render(value, row) : String(value ?? "") }, col.key);
496
+ })
497
+ ]
498
+ },
499
+ String(id)
500
+ );
501
+ }) })
502
+ ] }) });
503
+ }
504
+ function defaultGetId(row) {
505
+ return getRecordId(row);
506
+ }
507
+
508
+ // src/filter-bar.tsx
509
+ import { useCallback as useCallback3 } from "react";
510
+ import { useSearchParams, useNavigate, useLocation } from "react-router";
511
+ import JoyInput from "@mui/joy/Input";
512
+ import JoySelect from "@mui/joy/Select";
513
+ import JoyOption from "@mui/joy/Option";
514
+ import JoyStack from "@mui/joy/Stack";
515
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
516
+ function FilterBar({
517
+ filters,
518
+ searchable
519
+ }) {
520
+ const [searchParams] = useSearchParams();
521
+ const navigate = useNavigate();
522
+ const location = useLocation();
523
+ const updateParam = useCallback3(
524
+ (key, value) => {
525
+ const params = new URLSearchParams(searchParams);
526
+ if (value === null || value === "") {
527
+ params.delete(key);
528
+ } else {
529
+ params.set(key, value);
530
+ }
531
+ params.delete("page");
532
+ params.delete("cursor");
533
+ navigate(`${location.pathname}?${params.toString()}`);
534
+ },
535
+ [searchParams, navigate, location.pathname]
536
+ );
537
+ return /* @__PURE__ */ jsxs8(JoyStack, { direction: "row", spacing: 1, flexWrap: "wrap", alignItems: "center", sx: { mb: 2 }, children: [
538
+ searchable && searchable.length > 0 ? /* @__PURE__ */ jsx12(
539
+ JoyInput,
540
+ {
541
+ type: "search",
542
+ placeholder: `Search ${searchable.join(", ")}...`,
543
+ value: searchParams.get("q") ?? "",
544
+ onChange: (e) => updateParam("q", e.target.value || null),
545
+ size: "sm",
546
+ sx: { minWidth: 200 }
547
+ }
548
+ ) : null,
549
+ filters.map((filter) => /* @__PURE__ */ jsx12(
550
+ JoyFilterInput,
551
+ {
552
+ filter,
553
+ value: searchParams.get(filter.column) ?? "",
554
+ onChange: (value) => updateParam(filter.column, value || null)
555
+ },
556
+ filter.column
557
+ ))
558
+ ] });
559
+ }
560
+ function JoyFilterInput({
561
+ filter,
562
+ value,
563
+ onChange
564
+ }) {
565
+ const label = filter.label ?? filter.column;
566
+ switch (filter.type) {
567
+ case "select":
568
+ case "boolean":
569
+ return /* @__PURE__ */ jsx12(
570
+ JoySelect,
571
+ {
572
+ value: value || null,
573
+ onChange: (_e, newValue) => onChange(newValue ? String(newValue) : ""),
574
+ placeholder: `All ${label}`,
575
+ size: "sm",
576
+ sx: { minWidth: 140 },
577
+ slotProps: { button: { "aria-label": label } },
578
+ children: (filter.options ?? []).map((opt) => /* @__PURE__ */ jsx12(JoyOption, { value: String(opt.value), children: opt.label }, String(opt.value)))
579
+ }
580
+ );
581
+ case "text":
582
+ default:
583
+ return /* @__PURE__ */ jsx12(
584
+ JoyInput,
585
+ {
586
+ placeholder: filter.placeholder ?? label,
587
+ value,
588
+ onChange: (e) => onChange(e.target.value),
589
+ size: "sm",
590
+ slotProps: { input: { "aria-label": label } }
591
+ }
592
+ );
593
+ }
594
+ }
595
+
596
+ // src/bulk-action-bar.tsx
597
+ import "react";
598
+ import JoySheet2 from "@mui/joy/Sheet";
599
+ import JoyStack2 from "@mui/joy/Stack";
600
+ import JoyButton2 from "@mui/joy/Button";
601
+ import JoyTypography from "@mui/joy/Typography";
602
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
603
+ function BulkActionBar({
604
+ selectedCount,
605
+ actions,
606
+ onAction,
607
+ onClearSelection
608
+ }) {
609
+ if (selectedCount === 0) return null;
610
+ return /* @__PURE__ */ jsxs9(
611
+ JoySheet2,
612
+ {
613
+ variant: "soft",
614
+ color: "primary",
615
+ sx: {
616
+ p: 1,
617
+ px: 2,
618
+ borderRadius: "sm",
619
+ mb: 1,
620
+ display: "flex",
621
+ alignItems: "center",
622
+ gap: 1
623
+ },
624
+ children: [
625
+ /* @__PURE__ */ jsx13(JoyTypography, { level: "body-sm", fontWeight: "lg", children: `${selectedCount} selected` }),
626
+ /* @__PURE__ */ jsxs9(JoyStack2, { direction: "row", spacing: 1, sx: { ml: "auto" }, children: [
627
+ actions.map((action) => {
628
+ const Icon = action.icon;
629
+ return /* @__PURE__ */ jsx13(
630
+ JoyButton2,
631
+ {
632
+ size: "sm",
633
+ variant: "soft",
634
+ onClick: () => onAction(action),
635
+ startDecorator: Icon ? /* @__PURE__ */ jsx13(Icon, { className: "bulk-action-icon" }) : void 0,
636
+ children: action.label
637
+ },
638
+ action.label
639
+ );
640
+ }),
641
+ /* @__PURE__ */ jsx13(
642
+ JoyButton2,
643
+ {
644
+ size: "sm",
645
+ variant: "plain",
646
+ color: "neutral",
647
+ onClick: onClearSelection,
648
+ children: "Clear selection"
649
+ }
650
+ )
651
+ ] })
652
+ ]
653
+ }
654
+ );
655
+ }
656
+
657
+ // src/drop-zone.tsx
658
+ import { useState as useState2, useCallback as useCallback4, useRef } from "react";
659
+ import JoySheet3 from "@mui/joy/Sheet";
660
+ import JoyTypography2 from "@mui/joy/Typography";
661
+ import JoyLinearProgress from "@mui/joy/LinearProgress";
662
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
663
+ function DropZone({
664
+ upload,
665
+ multiple = false,
666
+ children
667
+ }) {
668
+ const [isDragOver, setIsDragOver] = useState2(false);
669
+ const inputRef = useRef(null);
670
+ const handleFiles = useCallback4(
671
+ (files) => {
672
+ if (!files || files.length === 0) return;
673
+ if (multiple) {
674
+ for (let i = 0; i < files.length; i++) {
675
+ upload.start(files[i]);
676
+ }
677
+ } else {
678
+ upload.start(files[0]);
679
+ }
680
+ },
681
+ [upload, multiple]
682
+ );
683
+ const handleDrop = useCallback4(
684
+ (e) => {
685
+ e.preventDefault();
686
+ setIsDragOver(false);
687
+ handleFiles(e.dataTransfer.files);
688
+ },
689
+ [handleFiles]
690
+ );
691
+ const handleDragOver = useCallback4((e) => {
692
+ e.preventDefault();
693
+ setIsDragOver(true);
694
+ }, []);
695
+ const handleDragLeave = useCallback4(() => {
696
+ setIsDragOver(false);
697
+ }, []);
698
+ const handleClick = useCallback4(() => {
699
+ inputRef.current?.click();
700
+ }, []);
701
+ const hasError = !!(upload.error || upload.validationError);
702
+ return /* @__PURE__ */ jsxs10("div", { children: [
703
+ /* @__PURE__ */ jsx14(
704
+ JoySheet3,
705
+ {
706
+ variant: "outlined",
707
+ sx: {
708
+ borderStyle: "dashed",
709
+ borderWidth: 2,
710
+ borderColor: hasError ? "danger.400" : isDragOver ? "primary.400" : "neutral.outlinedBorder",
711
+ borderRadius: "md",
712
+ p: 4,
713
+ textAlign: "center",
714
+ cursor: "pointer",
715
+ transition: "border-color 0.2s",
716
+ "&:hover": { borderColor: "primary.300" }
717
+ },
718
+ onClick: handleClick,
719
+ onDrop: handleDrop,
720
+ onDragOver: handleDragOver,
721
+ onDragLeave: handleDragLeave,
722
+ children: children ?? /* @__PURE__ */ jsx14("div", { children: upload.isUploading ? /* @__PURE__ */ jsxs10("div", { children: [
723
+ /* @__PURE__ */ jsx14(JoyTypography2, { level: "body-sm", children: `Uploading... ${upload.progress}%` }),
724
+ /* @__PURE__ */ jsx14(
725
+ JoyLinearProgress,
726
+ {
727
+ determinate: true,
728
+ value: upload.progress,
729
+ sx: { mt: 1 }
730
+ }
731
+ )
732
+ ] }) : hasError ? /* @__PURE__ */ jsx14(JoyTypography2, { color: "danger", level: "body-sm", children: upload.error ?? upload.validationError }) : /* @__PURE__ */ jsx14(JoyTypography2, { level: "body-sm", color: "neutral", children: "Drop files here or click to browse" }) })
733
+ }
734
+ ),
735
+ /* @__PURE__ */ jsx14(
736
+ "input",
737
+ {
738
+ ref: inputRef,
739
+ type: "file",
740
+ accept: upload.accept,
741
+ multiple,
742
+ style: { display: "none" },
743
+ onChange: (e) => handleFiles(e.target.files)
744
+ }
745
+ )
746
+ ] });
747
+ }
748
+
749
+ // src/image-preview.tsx
750
+ import "react";
751
+ import JoyAspectRatio from "@mui/joy/AspectRatio";
752
+ import JoyTypography3 from "@mui/joy/Typography";
753
+ import { jsx as jsx15 } from "react/jsx-runtime";
754
+ function ImagePreview({
755
+ fileKey,
756
+ src,
757
+ getUrl,
758
+ width = 200,
759
+ height = 200,
760
+ fallback,
761
+ alt = "Image preview"
762
+ }) {
763
+ const resolvedSrc = src ?? (fileKey && getUrl ? getUrl(fileKey) : null);
764
+ if (!resolvedSrc) {
765
+ return fallback ? /* @__PURE__ */ jsx15("div", { children: fallback }) : /* @__PURE__ */ jsx15(
766
+ JoyAspectRatio,
767
+ {
768
+ ratio: width / height,
769
+ sx: { width, borderRadius: "sm", bgcolor: "neutral.softBg" },
770
+ children: /* @__PURE__ */ jsx15(JoyTypography3, { level: "body-sm", color: "neutral", children: "No image" })
771
+ }
772
+ );
773
+ }
774
+ return /* @__PURE__ */ jsx15(
775
+ JoyAspectRatio,
776
+ {
777
+ ratio: width / height,
778
+ sx: { width, borderRadius: "sm", overflow: "hidden" },
779
+ children: /* @__PURE__ */ jsx15(
780
+ "img",
781
+ {
782
+ src: resolvedSrc,
783
+ alt,
784
+ style: { objectFit: "cover", width: "100%", height: "100%" }
785
+ }
786
+ )
787
+ }
788
+ );
789
+ }
790
+
791
+ // src/file-list.tsx
792
+ import "react";
793
+ import JoyList from "@mui/joy/List";
794
+ import JoyListItem from "@mui/joy/ListItem";
795
+ import JoyListItemContent from "@mui/joy/ListItemContent";
796
+ import JoyTypography4 from "@mui/joy/Typography";
797
+ import JoyButton3 from "@mui/joy/Button";
798
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
799
+ function formatBytes(bytes) {
800
+ if (bytes < 1024) return `${bytes} B`;
801
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
802
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
803
+ }
804
+ function FileList({
805
+ files,
806
+ onDownload
807
+ }) {
808
+ if (files.length === 0) {
809
+ return /* @__PURE__ */ jsx16(JoyTypography4, { level: "body-sm", color: "neutral", children: "No files" });
810
+ }
811
+ return /* @__PURE__ */ jsx16(JoyList, { size: "sm", children: files.map((file) => /* @__PURE__ */ jsxs11(JoyListItem, { children: [
812
+ /* @__PURE__ */ jsxs11(JoyListItemContent, { children: [
813
+ /* @__PURE__ */ jsx16(JoyTypography4, { level: "body-sm", children: file.name }),
814
+ file.size != null ? /* @__PURE__ */ jsx16(JoyTypography4, { level: "body-xs", color: "neutral", children: formatBytes(file.size) }) : null
815
+ ] }),
816
+ onDownload || file.url ? /* @__PURE__ */ jsx16(
817
+ JoyButton3,
818
+ {
819
+ size: "sm",
820
+ variant: "plain",
821
+ onClick: onDownload ? () => onDownload(file) : void 0,
822
+ children: "\u2193"
823
+ }
824
+ ) : null
825
+ ] }, file.key)) });
826
+ }
827
+
828
+ // src/list-view.tsx
829
+ import { useState as useState3, useCallback as useCallback5 } from "react";
830
+ import JoyButton4 from "@mui/joy/Button";
831
+ import JoyStack3 from "@mui/joy/Stack";
832
+ import JoyTypography5 from "@mui/joy/Typography";
833
+ import { useActionStatus as useActionStatus4 } from "@cfast/ui";
834
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
835
+ function ListView({
836
+ title,
837
+ data,
838
+ table: _table,
839
+ columns,
840
+ actions: _actions,
841
+ filters,
842
+ searchable,
843
+ createAction,
844
+ createLabel = "Create",
845
+ selectable = false,
846
+ bulkActions,
847
+ breadcrumb
848
+ }) {
849
+ const [selectedRows, setSelectedRows] = useState3([]);
850
+ const handleBulkAction = useCallback5(
851
+ (action) => {
852
+ if (action.handler) {
853
+ action.handler(selectedRows);
854
+ }
855
+ },
856
+ [selectedRows]
857
+ );
858
+ const clearSelection = useCallback5(() => {
859
+ setSelectedRows([]);
860
+ }, []);
861
+ const createButton = createAction ? /* @__PURE__ */ jsx17(CreateButton, { action: createAction, label: createLabel }) : null;
862
+ return /* @__PURE__ */ jsx17(PageContainer, { title, breadcrumb, actions: createButton, children: /* @__PURE__ */ jsxs12(JoyStack3, { spacing: 2, children: [
863
+ filters && filters.length > 0 ? /* @__PURE__ */ jsx17(FilterBar, { filters, searchable }) : null,
864
+ selectable && bulkActions && bulkActions.length > 0 ? /* @__PURE__ */ jsx17(
865
+ BulkActionBar,
866
+ {
867
+ selectedCount: selectedRows.length,
868
+ actions: bulkActions,
869
+ onAction: handleBulkAction,
870
+ onClearSelection: clearSelection
871
+ }
872
+ ) : null,
873
+ data.items.length === 0 && !data.isLoading ? /* @__PURE__ */ jsx17(
874
+ EmptyState,
875
+ {
876
+ title: `No ${title.toLowerCase()} found`,
877
+ description: filters ? "Try adjusting your filters" : void 0,
878
+ createAction,
879
+ createLabel
880
+ }
881
+ ) : /* @__PURE__ */ jsx17(
882
+ DataTable,
883
+ {
884
+ data,
885
+ columns,
886
+ selectable,
887
+ selectedRows: selectable ? selectedRows : void 0,
888
+ onSelectionChange: selectable ? (rows) => setSelectedRows(rows) : void 0
889
+ }
890
+ ),
891
+ data.totalPages && data.totalPages > 1 && data.goToPage ? /* @__PURE__ */ jsxs12(JoyStack3, { direction: "row", justifyContent: "center", alignItems: "center", spacing: 2, children: [
892
+ /* @__PURE__ */ jsx17(
893
+ JoyButton4,
894
+ {
895
+ size: "sm",
896
+ variant: "outlined",
897
+ disabled: data.currentPage === 1,
898
+ onClick: () => data.goToPage?.(Math.max(1, (data.currentPage ?? 1) - 1)),
899
+ children: "Previous"
900
+ }
901
+ ),
902
+ /* @__PURE__ */ jsx17(JoyTypography5, { level: "body-sm", children: `Page ${data.currentPage ?? 1} of ${data.totalPages}` }),
903
+ /* @__PURE__ */ jsx17(
904
+ JoyButton4,
905
+ {
906
+ size: "sm",
907
+ variant: "outlined",
908
+ disabled: data.currentPage === data.totalPages,
909
+ onClick: () => data.goToPage?.(Math.min(data.totalPages ?? 1, (data.currentPage ?? 1) + 1)),
910
+ children: "Next"
911
+ }
912
+ )
913
+ ] }) : null,
914
+ data.hasMore && data.loadMore ? /* @__PURE__ */ jsx17(JoyStack3, { alignItems: "center", children: /* @__PURE__ */ jsx17(JoyButton4, { variant: "soft", onClick: data.loadMore, children: "Load more" }) }) : null
915
+ ] }) });
916
+ }
917
+ function CreateButton({ action, label }) {
918
+ const status = useActionStatus4(action);
919
+ return /* @__PURE__ */ jsx17(ActionButton, { action: status, variant: "solid", color: "primary", children: label });
920
+ }
921
+
922
+ // src/detail-view.tsx
923
+ import JoyGrid from "@mui/joy/Grid";
924
+ import JoyTypography6 from "@mui/joy/Typography";
925
+ import { fieldForColumn, getField as getField2 } from "@cfast/ui";
926
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
927
+ function normalizeFields(fields) {
928
+ if (!fields) return [];
929
+ return fields.map((col) => {
930
+ if (typeof col === "string") {
931
+ return {
932
+ key: col,
933
+ label: col.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim()
934
+ };
935
+ }
936
+ return col;
937
+ });
938
+ }
939
+ function DetailView({
940
+ title,
941
+ table,
942
+ record,
943
+ fields: fieldsProp,
944
+ exclude,
945
+ breadcrumb
946
+ }) {
947
+ const fields = normalizeFields(fieldsProp);
948
+ const displayFields = fields.length > 0 ? fields : inferFieldsFromRecord(record, exclude);
949
+ return /* @__PURE__ */ jsx18(PageContainer, { title, breadcrumb, children: /* @__PURE__ */ jsx18(JoyGrid, { container: true, spacing: 2, children: displayFields.map((field) => {
950
+ const value = getField2(record, field.key);
951
+ const FieldComponent = field.render ? null : resolveFieldComponent(field.key, table);
952
+ return /* @__PURE__ */ jsxs13(JoyGrid, { xs: 12, md: 6, children: [
953
+ /* @__PURE__ */ jsx18(
954
+ JoyTypography6,
955
+ {
956
+ level: "body-xs",
957
+ textTransform: "uppercase",
958
+ fontWeight: "lg",
959
+ mb: 0.5,
960
+ children: field.label ?? field.key
961
+ }
962
+ ),
963
+ /* @__PURE__ */ jsx18("div", { children: field.render ? field.render(value, record) : FieldComponent ? /* @__PURE__ */ jsx18(FieldComponent, { value }) : String(value ?? "\u2014") })
964
+ ] }, field.key);
965
+ }) }) });
966
+ }
967
+ function inferFieldsFromRecord(record, exclude) {
968
+ if (!record || typeof record !== "object") return [];
969
+ return Object.keys(record).filter((key) => !exclude || !exclude.includes(key)).map((key) => ({
970
+ key,
971
+ label: key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim()
972
+ }));
973
+ }
974
+ function resolveFieldComponent(_key, table) {
975
+ if (!table || typeof table !== "object") return null;
976
+ const col = getField2(table, _key);
977
+ if (!col || typeof col !== "object" || !("dataType" in col) || typeof col.dataType !== "string" || !("name" in col) || typeof col.name !== "string") {
978
+ return null;
979
+ }
980
+ return fieldForColumn({ dataType: col.dataType, name: col.name });
981
+ }
982
+
983
+ // src/avatar-with-initials.tsx
984
+ import "react";
985
+ import Avatar2 from "@mui/joy/Avatar";
986
+ import { getInitials as getInitials2 } from "@cfast/ui";
987
+ import { jsx as jsx19 } from "react/jsx-runtime";
988
+ function AvatarWithInitials({
989
+ src,
990
+ name,
991
+ size = "md"
992
+ }) {
993
+ return /* @__PURE__ */ jsx19(Avatar2, { src: src ?? void 0, alt: name, size, children: getInitials2(name) });
994
+ }
995
+
996
+ // src/impersonation-banner.tsx
997
+ import "react";
998
+ import Sheet2 from "@mui/joy/Sheet";
999
+ import Typography4 from "@mui/joy/Typography";
1000
+ import Button3 from "@mui/joy/Button";
1001
+ import Stack5 from "@mui/joy/Stack";
1002
+ import { useCurrentUser as useCurrentUser2 } from "@cfast/auth/client";
1003
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1004
+ function ImpersonationBanner({
1005
+ stopAction = "/admin/stop-impersonation"
1006
+ }) {
1007
+ const user = useCurrentUser2();
1008
+ if (!user?.isImpersonating) {
1009
+ return null;
1010
+ }
1011
+ return /* @__PURE__ */ jsx20(
1012
+ Sheet2,
1013
+ {
1014
+ color: "warning",
1015
+ variant: "solid",
1016
+ sx: {
1017
+ position: "sticky",
1018
+ top: 0,
1019
+ zIndex: 1100,
1020
+ py: 1,
1021
+ px: 3
1022
+ },
1023
+ children: /* @__PURE__ */ jsxs14(Stack5, { direction: "row", spacing: 2, alignItems: "center", justifyContent: "center", children: [
1024
+ /* @__PURE__ */ jsx20(Typography4, { level: "body-sm", sx: { fontWeight: "bold" }, children: `Viewing as ${user.name} (${user.email})` }),
1025
+ /* @__PURE__ */ jsx20("form", { method: "post", action: stopAction, children: /* @__PURE__ */ jsx20(Button3, { size: "sm", variant: "outlined", color: "warning", type: "submit", children: "Stop Impersonating" }) })
1026
+ ] })
1027
+ }
1028
+ );
1029
+ }
1030
+
1031
+ // src/index.ts
1032
+ import { PermissionGate } from "@cfast/ui";
1033
+
1034
+ // src/login-components.tsx
1035
+ import Box from "@mui/joy/Box";
1036
+ import Card from "@mui/joy/Card";
1037
+ import Input from "@mui/joy/Input";
1038
+ import Button4 from "@mui/joy/Button";
1039
+ import Alert2 from "@mui/joy/Alert";
1040
+ import Divider from "@mui/joy/Divider";
1041
+ import { Fragment as Fragment3, jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1042
+ function Layout({ children }) {
1043
+ return /* @__PURE__ */ jsx21(
1044
+ Box,
1045
+ {
1046
+ sx: {
1047
+ display: "flex",
1048
+ justifyContent: "center",
1049
+ alignItems: "center",
1050
+ minHeight: "100vh",
1051
+ bgcolor: "background.surface"
1052
+ },
1053
+ children: /* @__PURE__ */ jsx21(Card, { variant: "outlined", sx: { maxWidth: 400, width: "100%", p: 4 }, children })
1054
+ }
1055
+ );
1056
+ }
1057
+ function EmailInput({
1058
+ value,
1059
+ onChange
1060
+ }) {
1061
+ return /* @__PURE__ */ jsx21(
1062
+ Input,
1063
+ {
1064
+ type: "email",
1065
+ placeholder: "you@example.com",
1066
+ value,
1067
+ onChange: (e) => onChange(e.target.value),
1068
+ required: true,
1069
+ size: "lg"
1070
+ }
1071
+ );
1072
+ }
1073
+ function MagicLinkButton({
1074
+ onClick,
1075
+ loading
1076
+ }) {
1077
+ return /* @__PURE__ */ jsx21(
1078
+ Button4,
1079
+ {
1080
+ onClick,
1081
+ loading,
1082
+ size: "lg",
1083
+ fullWidth: true,
1084
+ children: "Send Magic Link"
1085
+ }
1086
+ );
1087
+ }
1088
+ function PasskeyButton({
1089
+ onClick,
1090
+ loading
1091
+ }) {
1092
+ return /* @__PURE__ */ jsxs15(Fragment3, { children: [
1093
+ /* @__PURE__ */ jsx21(Divider, { children: "or" }),
1094
+ /* @__PURE__ */ jsx21(
1095
+ Button4,
1096
+ {
1097
+ variant: "outlined",
1098
+ color: "neutral",
1099
+ onClick,
1100
+ loading,
1101
+ size: "lg",
1102
+ fullWidth: true,
1103
+ children: "Sign in with Passkey"
1104
+ }
1105
+ )
1106
+ ] });
1107
+ }
1108
+ function SuccessMessage({ email }) {
1109
+ return /* @__PURE__ */ jsxs15(Alert2, { color: "success", children: [
1110
+ "Check your email (",
1111
+ email,
1112
+ ") for a magic link. Click the link to sign in."
1113
+ ] });
1114
+ }
1115
+ function ErrorMessage({ error }) {
1116
+ return /* @__PURE__ */ jsx21(Alert2, { color: "danger", sx: { mb: 2 }, children: error });
1117
+ }
1118
+ var joyLoginComponents = {
1119
+ Layout,
1120
+ EmailInput,
1121
+ MagicLinkButton,
1122
+ PasskeyButton,
1123
+ SuccessMessage,
1124
+ ErrorMessage
1125
+ };
1126
+ export {
1127
+ ActionButton,
1128
+ AppShell,
1129
+ AppShellHeader,
1130
+ AppShellSidebar,
1131
+ AvatarWithInitials,
1132
+ BulkActionBar,
1133
+ ConfirmDialog,
1134
+ DataTable,
1135
+ DetailView,
1136
+ DropZone,
1137
+ EmptyState,
1138
+ FileList,
1139
+ FilterBar,
1140
+ FormStatus,
1141
+ ImagePreview,
1142
+ ImpersonationBanner,
1143
+ ListView,
1144
+ NavigationProgress,
1145
+ PageContainer,
1146
+ PermissionGate,
1147
+ RoleBadge,
1148
+ ToastProvider,
1149
+ UserMenu,
1150
+ joyLoginComponents
1151
+ };