@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/README.md +18 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +1151 -0
- package/llms.txt +108 -0
- package/package.json +65 -0
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
|
+
};
|