@cfast/ui 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/llms.txt +159 -0
- package/package.json +26 -18
- package/LICENSE +0 -21
- package/dist/chunk-755IRYDN.js +0 -941
- package/dist/chunk-7SNK37GF.js +0 -418
- package/dist/chunk-ASMYTWTR.js +0 -356
- package/dist/chunk-B2XXH5V4.js +0 -66
- package/dist/chunk-BQMXYYEV.js +0 -348
- package/dist/chunk-DTKBXCTU.js +0 -211
- package/dist/chunk-EYIBATYR.js +0 -33
- package/dist/chunk-FPZAQ2YQ.js +0 -474
- package/dist/chunk-G2OU4BYC.js +0 -205
- package/dist/chunk-JUNLQJ6H.js +0 -1013
- package/dist/chunk-NRGMW3JA.js +0 -906
- package/dist/chunk-Q6FPL2OJ.js +0 -1086
- package/dist/chunk-QHWAGKNW.js +0 -456
- package/dist/chunk-QZT62CGJ.js +0 -924
- package/dist/chunk-RESL4IJJ.js +0 -112
- package/dist/chunk-UDCWQUTR.js +0 -221
- package/dist/chunk-UE7PZOIJ.js +0 -11
- package/dist/chunk-UTZTHGNE.js +0 -84
- package/dist/chunk-UVRXMOX5.js +0 -439
- package/dist/chunk-XFD3N2D4.js +0 -161
- package/dist/client-CXIHCQtA.d.ts +0 -274
- package/dist/permission-gate-apt9T9Mu.d.ts +0 -1256
- package/dist/types-1bAiH2uK.d.ts +0 -392
- package/dist/types-BX6u5sAd.d.ts +0 -403
- package/dist/types-BpdY7w5l.d.ts +0 -403
- package/dist/types-BrepeVp8.d.ts +0 -403
- package/dist/types-BvAqMZhn.d.ts +0 -403
- package/dist/types-C74nSscq.d.ts +0 -403
- package/dist/types-DD1Cpx8F.d.ts +0 -403
- package/dist/types-DHUhQwJn.d.ts +0 -403
- package/dist/types-DZSJNt_M.d.ts +0 -392
- package/dist/types-DaaJiIjW.d.ts +0 -391
- package/dist/types-LUpWJwps.d.ts +0 -403
- package/dist/types-a7zVU6WE.d.ts +0 -394
- package/dist/types-biJTHMcH.d.ts +0 -403
- package/dist/types-ow_qSEYJ.d.ts +0 -392
- package/dist/types-wnLasZaB.d.ts +0 -1234
package/dist/chunk-NRGMW3JA.js
DELETED
|
@@ -1,906 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useActionStatus,
|
|
3
|
-
useToast
|
|
4
|
-
} from "./chunk-RESL4IJJ.js";
|
|
5
|
-
|
|
6
|
-
// src/plugin.ts
|
|
7
|
-
import { createContext, useContext, createElement as createElement2 } from "react";
|
|
8
|
-
|
|
9
|
-
// src/headless-defaults.ts
|
|
10
|
-
import { createElement } from "react";
|
|
11
|
-
var headlessDefaults = {
|
|
12
|
-
// Actions
|
|
13
|
-
button: ({ children, onClick, disabled, loading, type }) => createElement(
|
|
14
|
-
"button",
|
|
15
|
-
{ onClick, disabled: disabled || loading, type: type ?? "button" },
|
|
16
|
-
loading ? "Loading..." : children
|
|
17
|
-
),
|
|
18
|
-
tooltip: ({ children, title }) => createElement("span", { title }, children),
|
|
19
|
-
confirmDialog: ({ open, onClose, onConfirm, title, description, confirmLabel, cancelLabel }) => open ? createElement(
|
|
20
|
-
"dialog",
|
|
21
|
-
{ open: true },
|
|
22
|
-
createElement("p", null, createElement("strong", null, title)),
|
|
23
|
-
description ? createElement("p", null, description) : null,
|
|
24
|
-
createElement(
|
|
25
|
-
"div",
|
|
26
|
-
null,
|
|
27
|
-
createElement("button", { onClick: onClose }, cancelLabel ?? "Cancel"),
|
|
28
|
-
createElement("button", { onClick: onConfirm }, confirmLabel ?? "Confirm")
|
|
29
|
-
)
|
|
30
|
-
) : null,
|
|
31
|
-
// Data display
|
|
32
|
-
table: ({ children }) => createElement("table", null, children),
|
|
33
|
-
tableHead: ({ children }) => createElement("thead", null, children),
|
|
34
|
-
tableBody: ({ children }) => createElement("tbody", null, children),
|
|
35
|
-
tableRow: ({ children, onClick }) => createElement("tr", { onClick }, children),
|
|
36
|
-
tableCell: ({ children, header, sortable, sortDirection, onSort }) => createElement(
|
|
37
|
-
header ? "th" : "td",
|
|
38
|
-
{
|
|
39
|
-
onClick: sortable ? onSort : void 0,
|
|
40
|
-
style: sortable ? { cursor: "pointer" } : void 0
|
|
41
|
-
},
|
|
42
|
-
children,
|
|
43
|
-
sortable && sortDirection ? sortDirection === "asc" ? " \u2191" : " \u2193" : null
|
|
44
|
-
),
|
|
45
|
-
chip: ({ children, size }) => createElement(
|
|
46
|
-
"span",
|
|
47
|
-
{
|
|
48
|
-
style: {
|
|
49
|
-
display: "inline-block",
|
|
50
|
-
padding: size === "sm" ? "1px 6px" : "2px 8px",
|
|
51
|
-
borderRadius: "12px",
|
|
52
|
-
fontSize: size === "sm" ? "12px" : "14px",
|
|
53
|
-
backgroundColor: "#eee"
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
children
|
|
57
|
-
),
|
|
58
|
-
// Layout
|
|
59
|
-
appShell: ({ children, sidebar, header }) => createElement(
|
|
60
|
-
"div",
|
|
61
|
-
{ style: { display: "flex", minHeight: "100vh" } },
|
|
62
|
-
sidebar ? createElement("nav", null, sidebar) : null,
|
|
63
|
-
createElement(
|
|
64
|
-
"div",
|
|
65
|
-
{ style: { flex: 1 } },
|
|
66
|
-
header ?? null,
|
|
67
|
-
createElement("main", null, children)
|
|
68
|
-
)
|
|
69
|
-
),
|
|
70
|
-
sidebar: ({ children }) => createElement(
|
|
71
|
-
"aside",
|
|
72
|
-
{ style: { width: "240px", borderRight: "1px solid #ddd" } },
|
|
73
|
-
children
|
|
74
|
-
),
|
|
75
|
-
pageContainer: ({ children, title, actions }) => createElement(
|
|
76
|
-
"div",
|
|
77
|
-
null,
|
|
78
|
-
title || actions ? createElement(
|
|
79
|
-
"div",
|
|
80
|
-
{ style: { display: "flex", justifyContent: "space-between", alignItems: "center" } },
|
|
81
|
-
title ? createElement("h1", null, title) : null,
|
|
82
|
-
actions ?? null
|
|
83
|
-
) : null,
|
|
84
|
-
children
|
|
85
|
-
),
|
|
86
|
-
breadcrumb: ({ items }) => createElement(
|
|
87
|
-
"nav",
|
|
88
|
-
{ "aria-label": "breadcrumb" },
|
|
89
|
-
items.map(
|
|
90
|
-
(item, i) => createElement(
|
|
91
|
-
"span",
|
|
92
|
-
{ key: i },
|
|
93
|
-
i > 0 ? " / " : null,
|
|
94
|
-
item.to ? createElement("a", { href: item.to }, item.label) : item.label
|
|
95
|
-
)
|
|
96
|
-
)
|
|
97
|
-
),
|
|
98
|
-
// Feedback
|
|
99
|
-
toast: ({ children }) => createElement("div", null, children),
|
|
100
|
-
alert: ({ children, color }) => createElement(
|
|
101
|
-
"div",
|
|
102
|
-
{
|
|
103
|
-
role: "alert",
|
|
104
|
-
style: {
|
|
105
|
-
padding: "8px 12px",
|
|
106
|
-
borderRadius: "4px",
|
|
107
|
-
backgroundColor: color === "danger" ? "#fee" : color === "success" ? "#efe" : color === "warning" ? "#ffe" : "#f5f5f5"
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
children
|
|
111
|
-
),
|
|
112
|
-
// File
|
|
113
|
-
dropZone: ({ children, isDragOver, onClick, onDrop, onDragOver, onDragLeave }) => createElement(
|
|
114
|
-
"div",
|
|
115
|
-
{
|
|
116
|
-
onClick,
|
|
117
|
-
onDrop: (e) => {
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
onDrop(e.dataTransfer.files);
|
|
120
|
-
},
|
|
121
|
-
onDragOver: (e) => {
|
|
122
|
-
e.preventDefault();
|
|
123
|
-
onDragOver(e);
|
|
124
|
-
},
|
|
125
|
-
onDragLeave,
|
|
126
|
-
style: {
|
|
127
|
-
border: `2px dashed ${isDragOver ? "#4caf50" : "#ccc"}`,
|
|
128
|
-
borderRadius: "8px",
|
|
129
|
-
padding: "32px",
|
|
130
|
-
textAlign: "center",
|
|
131
|
-
cursor: "pointer"
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
children
|
|
135
|
-
)
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
// src/plugin.ts
|
|
139
|
-
var UIPluginContext = createContext(null);
|
|
140
|
-
function createUIPlugin(config) {
|
|
141
|
-
return { components: config.components };
|
|
142
|
-
}
|
|
143
|
-
function UIPluginProvider({
|
|
144
|
-
plugin,
|
|
145
|
-
children
|
|
146
|
-
}) {
|
|
147
|
-
return createElement2(UIPluginContext.Provider, { value: plugin }, children);
|
|
148
|
-
}
|
|
149
|
-
function useUIPlugin() {
|
|
150
|
-
return useContext(UIPluginContext);
|
|
151
|
-
}
|
|
152
|
-
function useComponent(slot) {
|
|
153
|
-
const plugin = useUIPlugin();
|
|
154
|
-
const component = plugin?.components[slot];
|
|
155
|
-
if (component) {
|
|
156
|
-
return component;
|
|
157
|
-
}
|
|
158
|
-
return headlessDefaults[slot];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// src/hooks/use-confirm.ts
|
|
162
|
-
import { createContext as createContext2, useContext as useContext2, useCallback } from "react";
|
|
163
|
-
var ConfirmContext = createContext2(null);
|
|
164
|
-
function useConfirm() {
|
|
165
|
-
const ctx = useContext2(ConfirmContext);
|
|
166
|
-
if (!ctx) {
|
|
167
|
-
throw new Error("useConfirm must be used within a <ConfirmProvider>");
|
|
168
|
-
}
|
|
169
|
-
return useCallback(
|
|
170
|
-
(options) => ctx.confirm(options),
|
|
171
|
-
[ctx]
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// src/hooks/use-action-toast.ts
|
|
176
|
-
import { useEffect, useRef } from "react";
|
|
177
|
-
import { useActions } from "@cfast/actions/client";
|
|
178
|
-
function useActionToast(descriptor, config) {
|
|
179
|
-
const actions = useActions(descriptor);
|
|
180
|
-
const toast = useToast();
|
|
181
|
-
const prevDataRef = useRef({});
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
for (const [name, cfg] of Object.entries(config)) {
|
|
184
|
-
const actionFn = actions[name];
|
|
185
|
-
if (!actionFn) continue;
|
|
186
|
-
const result = actionFn();
|
|
187
|
-
const prevData = prevDataRef.current[name];
|
|
188
|
-
if (result.data !== void 0 && result.data !== prevData) {
|
|
189
|
-
prevDataRef.current[name] = result.data;
|
|
190
|
-
if (cfg.success) {
|
|
191
|
-
toast.success(cfg.success);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (result.error !== void 0 && result.error !== prevData) {
|
|
195
|
-
prevDataRef.current[name] = result.error;
|
|
196
|
-
if (cfg.error) {
|
|
197
|
-
toast.error(cfg.error);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// src/components/permission-gate.tsx
|
|
205
|
-
import { createElement as createElement3, Fragment } from "react";
|
|
206
|
-
function PermissionGate({
|
|
207
|
-
action,
|
|
208
|
-
actionName,
|
|
209
|
-
input,
|
|
210
|
-
children,
|
|
211
|
-
fallback
|
|
212
|
-
}) {
|
|
213
|
-
const status = useActionStatus(action, actionName, input);
|
|
214
|
-
if (status.invisible) {
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
if (!status.permitted) {
|
|
218
|
-
return fallback ? createElement3(Fragment, null, fallback) : null;
|
|
219
|
-
}
|
|
220
|
-
return createElement3(Fragment, null, children);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// src/components/action-button.tsx
|
|
224
|
-
import { createElement as createElement4 } from "react";
|
|
225
|
-
function ActionButton({
|
|
226
|
-
action,
|
|
227
|
-
actionName,
|
|
228
|
-
input,
|
|
229
|
-
children,
|
|
230
|
-
whenForbidden = "disable",
|
|
231
|
-
confirmation: _confirmation,
|
|
232
|
-
variant,
|
|
233
|
-
color,
|
|
234
|
-
size,
|
|
235
|
-
startDecorator
|
|
236
|
-
}) {
|
|
237
|
-
const status = useActionStatus(action, actionName, input);
|
|
238
|
-
const Button = useComponent("button");
|
|
239
|
-
if (status.invisible) {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
if (!status.permitted && whenForbidden === "hide") {
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
const disabled = !status.permitted && whenForbidden === "disable";
|
|
246
|
-
return createElement4(Button, {
|
|
247
|
-
children,
|
|
248
|
-
onClick: () => status.submit(),
|
|
249
|
-
disabled,
|
|
250
|
-
loading: status.pending,
|
|
251
|
-
variant,
|
|
252
|
-
color,
|
|
253
|
-
size,
|
|
254
|
-
startDecorator
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// src/components/confirm-provider.tsx
|
|
259
|
-
import { createElement as createElement5, useState, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
260
|
-
function ConfirmProvider({ children }) {
|
|
261
|
-
const [state, setState] = useState(null);
|
|
262
|
-
const ConfirmDialog = useComponent("confirmDialog");
|
|
263
|
-
const resolveRef = useRef2(null);
|
|
264
|
-
const confirm = useCallback2((options) => {
|
|
265
|
-
return new Promise((resolve) => {
|
|
266
|
-
resolveRef.current = resolve;
|
|
267
|
-
setState({ ...options, resolve });
|
|
268
|
-
});
|
|
269
|
-
}, []);
|
|
270
|
-
const handleClose = useCallback2(() => {
|
|
271
|
-
resolveRef.current?.(false);
|
|
272
|
-
resolveRef.current = null;
|
|
273
|
-
setState(null);
|
|
274
|
-
}, []);
|
|
275
|
-
const handleConfirm = useCallback2(() => {
|
|
276
|
-
resolveRef.current?.(true);
|
|
277
|
-
resolveRef.current = null;
|
|
278
|
-
setState(null);
|
|
279
|
-
}, []);
|
|
280
|
-
return createElement5(
|
|
281
|
-
ConfirmContext.Provider,
|
|
282
|
-
{ value: { confirm } },
|
|
283
|
-
children,
|
|
284
|
-
state ? createElement5(ConfirmDialog, {
|
|
285
|
-
open: true,
|
|
286
|
-
onClose: handleClose,
|
|
287
|
-
onConfirm: handleConfirm,
|
|
288
|
-
title: state.title,
|
|
289
|
-
description: state.description,
|
|
290
|
-
confirmLabel: state.confirmLabel,
|
|
291
|
-
cancelLabel: state.cancelLabel,
|
|
292
|
-
variant: state.variant
|
|
293
|
-
}) : null
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// src/components/form-status.tsx
|
|
298
|
-
import { createElement as createElement6 } from "react";
|
|
299
|
-
function FormStatus({ data }) {
|
|
300
|
-
const Alert = useComponent("alert");
|
|
301
|
-
if (!data) return null;
|
|
302
|
-
const elements = [];
|
|
303
|
-
if (data.success) {
|
|
304
|
-
elements.push(
|
|
305
|
-
createElement6(Alert, { key: "success", color: "success", children: data.success })
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
if (data.error) {
|
|
309
|
-
elements.push(
|
|
310
|
-
createElement6(Alert, { key: "error", color: "danger", children: data.error })
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
if (data.fieldErrors) {
|
|
314
|
-
const errorMessages = Object.entries(data.fieldErrors).flatMap(
|
|
315
|
-
([field, errors]) => errors.map((err) => `${field}: ${err}`)
|
|
316
|
-
);
|
|
317
|
-
if (errorMessages.length > 0) {
|
|
318
|
-
elements.push(
|
|
319
|
-
createElement6(Alert, {
|
|
320
|
-
key: "field-errors",
|
|
321
|
-
color: "danger",
|
|
322
|
-
children: createElement6(
|
|
323
|
-
"ul",
|
|
324
|
-
{ style: { margin: 0, paddingLeft: "16px" } },
|
|
325
|
-
...errorMessages.map(
|
|
326
|
-
(msg, i) => createElement6("li", { key: i }, msg)
|
|
327
|
-
)
|
|
328
|
-
)
|
|
329
|
-
})
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
if (elements.length === 0) return null;
|
|
334
|
-
return createElement6("div", { style: { display: "flex", flexDirection: "column", gap: "8px" } }, ...elements);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// src/components/role-badge.tsx
|
|
338
|
-
import { createElement as createElement7 } from "react";
|
|
339
|
-
var defaultColors = {
|
|
340
|
-
admin: "danger",
|
|
341
|
-
editor: "primary",
|
|
342
|
-
author: "success",
|
|
343
|
-
reader: "neutral"
|
|
344
|
-
};
|
|
345
|
-
function RoleBadge({ role, colors }) {
|
|
346
|
-
const Chip = useComponent("chip");
|
|
347
|
-
const colorMap = colors ? { ...defaultColors, ...Object.fromEntries(
|
|
348
|
-
Object.entries(colors).map(([k, v]) => [k, v])
|
|
349
|
-
) } : defaultColors;
|
|
350
|
-
const chipColor = colorMap[role] ?? "neutral";
|
|
351
|
-
return createElement7(Chip, {
|
|
352
|
-
children: role,
|
|
353
|
-
color: chipColor,
|
|
354
|
-
variant: "soft",
|
|
355
|
-
size: "sm"
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// src/components/impersonation-banner.tsx
|
|
360
|
-
import { createElement as createElement8 } from "react";
|
|
361
|
-
import { useCurrentUser } from "@cfast/auth/client";
|
|
362
|
-
function ImpersonationBanner({
|
|
363
|
-
stopAction = "/admin/stop-impersonation"
|
|
364
|
-
}) {
|
|
365
|
-
const user = useCurrentUser();
|
|
366
|
-
const Alert = useComponent("alert");
|
|
367
|
-
const Button = useComponent("button");
|
|
368
|
-
if (!user?.isImpersonating) {
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
return createElement8(
|
|
372
|
-
Alert,
|
|
373
|
-
{
|
|
374
|
-
color: "warning",
|
|
375
|
-
children: createElement8(
|
|
376
|
-
"div",
|
|
377
|
-
{
|
|
378
|
-
style: {
|
|
379
|
-
display: "flex",
|
|
380
|
-
alignItems: "center",
|
|
381
|
-
justifyContent: "center",
|
|
382
|
-
gap: "12px"
|
|
383
|
-
}
|
|
384
|
-
},
|
|
385
|
-
createElement8(
|
|
386
|
-
"strong",
|
|
387
|
-
null,
|
|
388
|
-
`Viewing as ${user.name} (${user.email})`
|
|
389
|
-
),
|
|
390
|
-
createElement8(
|
|
391
|
-
"form",
|
|
392
|
-
{ method: "post", action: stopAction },
|
|
393
|
-
createElement8(Button, {
|
|
394
|
-
type: "submit",
|
|
395
|
-
variant: "outlined",
|
|
396
|
-
size: "sm",
|
|
397
|
-
children: "Stop Impersonating"
|
|
398
|
-
})
|
|
399
|
-
)
|
|
400
|
-
)
|
|
401
|
-
}
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// src/components/data-table.tsx
|
|
406
|
-
import { createElement as createElement9, useState as useState2, useCallback as useCallback3 } from "react";
|
|
407
|
-
function normalizeColumns(columns) {
|
|
408
|
-
if (!columns) return [];
|
|
409
|
-
return columns.map((col) => {
|
|
410
|
-
if (typeof col === "string") {
|
|
411
|
-
return {
|
|
412
|
-
key: col,
|
|
413
|
-
label: col.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim(),
|
|
414
|
-
sortable: true
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
return col;
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
function DataTable({
|
|
421
|
-
data,
|
|
422
|
-
columns: columnsProp,
|
|
423
|
-
selectable = false,
|
|
424
|
-
selectedRows: externalSelectedRows,
|
|
425
|
-
onSelectionChange,
|
|
426
|
-
onRowClick,
|
|
427
|
-
getRowId,
|
|
428
|
-
emptyMessage = "No data"
|
|
429
|
-
}) {
|
|
430
|
-
const Table = useComponent("table");
|
|
431
|
-
const TableHead = useComponent("tableHead");
|
|
432
|
-
const TableBody = useComponent("tableBody");
|
|
433
|
-
const TableRow = useComponent("tableRow");
|
|
434
|
-
const TableCell = useComponent("tableCell");
|
|
435
|
-
const columns = normalizeColumns(columnsProp);
|
|
436
|
-
const [sortKey, setSortKey] = useState2(null);
|
|
437
|
-
const [sortDir, setSortDir] = useState2("asc");
|
|
438
|
-
const [internalSelected, setInternalSelected] = useState2(/* @__PURE__ */ new Set());
|
|
439
|
-
const selectedSet = externalSelectedRows ? new Set(externalSelectedRows.map((r) => (getRowId ?? defaultGetId)(r))) : internalSelected;
|
|
440
|
-
const handleSort = useCallback3((key) => {
|
|
441
|
-
if (sortKey === key) {
|
|
442
|
-
setSortDir((d) => d === "asc" ? "desc" : "asc");
|
|
443
|
-
} else {
|
|
444
|
-
setSortKey(key);
|
|
445
|
-
setSortDir("asc");
|
|
446
|
-
}
|
|
447
|
-
}, [sortKey]);
|
|
448
|
-
const toggleRow = useCallback3((id) => {
|
|
449
|
-
if (onSelectionChange) {
|
|
450
|
-
const row = data.items.find((r) => (getRowId ?? defaultGetId)(r) === id);
|
|
451
|
-
if (!row) return;
|
|
452
|
-
const current = externalSelectedRows ?? [];
|
|
453
|
-
const isSelected = current.some((r) => (getRowId ?? defaultGetId)(r) === id);
|
|
454
|
-
onSelectionChange(isSelected ? current.filter((r) => (getRowId ?? defaultGetId)(r) !== id) : [...current, row]);
|
|
455
|
-
} else {
|
|
456
|
-
setInternalSelected((prev) => {
|
|
457
|
-
const next = new Set(prev);
|
|
458
|
-
if (next.has(id)) next.delete(id);
|
|
459
|
-
else next.add(id);
|
|
460
|
-
return next;
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}, [data.items, externalSelectedRows, onSelectionChange, getRowId]);
|
|
464
|
-
if (data.items.length === 0 && !data.isLoading) {
|
|
465
|
-
return createElement9("div", { style: { textAlign: "center", padding: "32px", color: "#666" } }, emptyMessage);
|
|
466
|
-
}
|
|
467
|
-
return createElement9(
|
|
468
|
-
Table,
|
|
469
|
-
{ hoverRow: true, children: createElement9("div") },
|
|
470
|
-
createElement9(
|
|
471
|
-
TableHead,
|
|
472
|
-
{ children: createElement9("div") },
|
|
473
|
-
createElement9(
|
|
474
|
-
TableRow,
|
|
475
|
-
{ children: createElement9("div") },
|
|
476
|
-
selectable ? createElement9(TableCell, { header: true, children: "" }) : null,
|
|
477
|
-
...columns.map(
|
|
478
|
-
(col) => createElement9(TableCell, {
|
|
479
|
-
key: col.key,
|
|
480
|
-
header: true,
|
|
481
|
-
sortable: col.sortable !== false,
|
|
482
|
-
sortDirection: sortKey === col.key ? sortDir : null,
|
|
483
|
-
onSort: () => handleSort(col.key),
|
|
484
|
-
children: col.label ?? col.key
|
|
485
|
-
})
|
|
486
|
-
)
|
|
487
|
-
)
|
|
488
|
-
),
|
|
489
|
-
createElement9(
|
|
490
|
-
TableBody,
|
|
491
|
-
{ children: createElement9("div") },
|
|
492
|
-
...data.items.map((row) => {
|
|
493
|
-
const id = (getRowId ?? defaultGetId)(row);
|
|
494
|
-
const isSelected = selectedSet.has(id);
|
|
495
|
-
return createElement9(
|
|
496
|
-
TableRow,
|
|
497
|
-
{
|
|
498
|
-
key: String(id),
|
|
499
|
-
selected: isSelected,
|
|
500
|
-
onClick: onRowClick ? () => onRowClick(row) : void 0,
|
|
501
|
-
children: createElement9("div")
|
|
502
|
-
},
|
|
503
|
-
selectable ? createElement9(TableCell, {
|
|
504
|
-
children: createElement9("input", {
|
|
505
|
-
type: "checkbox",
|
|
506
|
-
checked: isSelected,
|
|
507
|
-
onChange: () => toggleRow(id)
|
|
508
|
-
})
|
|
509
|
-
}) : null,
|
|
510
|
-
...columns.map((col) => {
|
|
511
|
-
const value = row[col.key];
|
|
512
|
-
return createElement9(TableCell, {
|
|
513
|
-
key: col.key,
|
|
514
|
-
children: col.render ? col.render(value, row) : String(value ?? "")
|
|
515
|
-
});
|
|
516
|
-
})
|
|
517
|
-
);
|
|
518
|
-
})
|
|
519
|
-
)
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
function defaultGetId(row) {
|
|
523
|
-
return row.id;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// src/components/filter-bar.tsx
|
|
527
|
-
import { createElement as createElement10, useCallback as useCallback4 } from "react";
|
|
528
|
-
import { useSearchParams, useNavigate, useLocation } from "react-router";
|
|
529
|
-
function FilterBar({
|
|
530
|
-
filters,
|
|
531
|
-
searchable
|
|
532
|
-
}) {
|
|
533
|
-
const [searchParams] = useSearchParams();
|
|
534
|
-
const navigate = useNavigate();
|
|
535
|
-
const location = useLocation();
|
|
536
|
-
const updateParam = useCallback4(
|
|
537
|
-
(key, value) => {
|
|
538
|
-
const params = new URLSearchParams(searchParams);
|
|
539
|
-
if (value === null || value === "") {
|
|
540
|
-
params.delete(key);
|
|
541
|
-
} else {
|
|
542
|
-
params.set(key, value);
|
|
543
|
-
}
|
|
544
|
-
params.delete("page");
|
|
545
|
-
params.delete("cursor");
|
|
546
|
-
navigate(`${location.pathname}?${params.toString()}`);
|
|
547
|
-
},
|
|
548
|
-
[searchParams, navigate, location.pathname]
|
|
549
|
-
);
|
|
550
|
-
return createElement10(
|
|
551
|
-
"div",
|
|
552
|
-
{
|
|
553
|
-
style: {
|
|
554
|
-
display: "flex",
|
|
555
|
-
gap: "8px",
|
|
556
|
-
flexWrap: "wrap",
|
|
557
|
-
alignItems: "center",
|
|
558
|
-
marginBottom: "16px"
|
|
559
|
-
}
|
|
560
|
-
},
|
|
561
|
-
// Search input
|
|
562
|
-
searchable && searchable.length > 0 ? createElement10("input", {
|
|
563
|
-
type: "search",
|
|
564
|
-
placeholder: `Search ${searchable.join(", ")}...`,
|
|
565
|
-
value: searchParams.get("q") ?? "",
|
|
566
|
-
onChange: (e) => updateParam("q", e.target.value || null),
|
|
567
|
-
style: { padding: "6px 10px", border: "1px solid #ccc", borderRadius: "4px" }
|
|
568
|
-
}) : null,
|
|
569
|
-
...filters.map(
|
|
570
|
-
(filter) => createElement10(FilterInput, {
|
|
571
|
-
key: filter.column,
|
|
572
|
-
filter,
|
|
573
|
-
value: searchParams.get(filter.column) ?? "",
|
|
574
|
-
onChange: (value) => updateParam(filter.column, value || null)
|
|
575
|
-
})
|
|
576
|
-
)
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
function FilterInput({
|
|
580
|
-
filter,
|
|
581
|
-
value,
|
|
582
|
-
onChange
|
|
583
|
-
}) {
|
|
584
|
-
const label = filter.label ?? filter.column;
|
|
585
|
-
switch (filter.type) {
|
|
586
|
-
case "select":
|
|
587
|
-
case "boolean":
|
|
588
|
-
return createElement10(
|
|
589
|
-
"select",
|
|
590
|
-
{
|
|
591
|
-
value,
|
|
592
|
-
onChange: (e) => onChange(e.target.value),
|
|
593
|
-
"aria-label": label
|
|
594
|
-
},
|
|
595
|
-
createElement10("option", { value: "" }, `All ${label}`),
|
|
596
|
-
...(filter.options ?? []).map(
|
|
597
|
-
(opt) => createElement10("option", { key: String(opt.value), value: String(opt.value) }, opt.label)
|
|
598
|
-
)
|
|
599
|
-
);
|
|
600
|
-
case "text":
|
|
601
|
-
default:
|
|
602
|
-
return createElement10("input", {
|
|
603
|
-
type: "text",
|
|
604
|
-
placeholder: filter.placeholder ?? label,
|
|
605
|
-
value,
|
|
606
|
-
onChange: (e) => onChange(e.target.value),
|
|
607
|
-
"aria-label": label,
|
|
608
|
-
style: { padding: "6px 10px", border: "1px solid #ccc", borderRadius: "4px" }
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// src/components/bulk-action-bar.tsx
|
|
614
|
-
import { createElement as createElement11 } from "react";
|
|
615
|
-
function BulkActionBar({
|
|
616
|
-
selectedCount,
|
|
617
|
-
actions,
|
|
618
|
-
onAction,
|
|
619
|
-
onClearSelection
|
|
620
|
-
}) {
|
|
621
|
-
const Button = useComponent("button");
|
|
622
|
-
if (selectedCount === 0) return null;
|
|
623
|
-
return createElement11(
|
|
624
|
-
"div",
|
|
625
|
-
{
|
|
626
|
-
style: {
|
|
627
|
-
display: "flex",
|
|
628
|
-
alignItems: "center",
|
|
629
|
-
gap: "8px",
|
|
630
|
-
padding: "8px 16px",
|
|
631
|
-
backgroundColor: "#f0f4ff",
|
|
632
|
-
borderRadius: "4px",
|
|
633
|
-
marginBottom: "8px"
|
|
634
|
-
}
|
|
635
|
-
},
|
|
636
|
-
createElement11(
|
|
637
|
-
"span",
|
|
638
|
-
null,
|
|
639
|
-
`${selectedCount} selected`
|
|
640
|
-
),
|
|
641
|
-
...actions.map(
|
|
642
|
-
(action) => createElement11(Button, {
|
|
643
|
-
key: action.label,
|
|
644
|
-
children: action.label,
|
|
645
|
-
onClick: () => onAction(action),
|
|
646
|
-
variant: "soft",
|
|
647
|
-
size: "sm",
|
|
648
|
-
startDecorator: action.icon ? createElement11(action.icon, { className: "bulk-action-icon" }) : void 0
|
|
649
|
-
})
|
|
650
|
-
),
|
|
651
|
-
createElement11(Button, {
|
|
652
|
-
children: "Clear",
|
|
653
|
-
onClick: onClearSelection,
|
|
654
|
-
variant: "plain",
|
|
655
|
-
size: "sm"
|
|
656
|
-
})
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// src/fields/date-field.tsx
|
|
661
|
-
import { createElement as createElement12 } from "react";
|
|
662
|
-
var rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
663
|
-
function getRelativeTime(date) {
|
|
664
|
-
const now = Date.now();
|
|
665
|
-
const diffMs = date.getTime() - now;
|
|
666
|
-
const diffSec = Math.round(diffMs / 1e3);
|
|
667
|
-
const diffMin = Math.round(diffSec / 60);
|
|
668
|
-
const diffHour = Math.round(diffMin / 60);
|
|
669
|
-
const diffDay = Math.round(diffHour / 24);
|
|
670
|
-
if (Math.abs(diffSec) < 60) return rtf.format(diffSec, "second");
|
|
671
|
-
if (Math.abs(diffMin) < 60) return rtf.format(diffMin, "minute");
|
|
672
|
-
if (Math.abs(diffHour) < 24) return rtf.format(diffHour, "hour");
|
|
673
|
-
if (Math.abs(diffDay) < 30) return rtf.format(diffDay, "day");
|
|
674
|
-
const diffMonth = Math.round(diffDay / 30);
|
|
675
|
-
if (Math.abs(diffMonth) < 12) return rtf.format(diffMonth, "month");
|
|
676
|
-
return rtf.format(Math.round(diffDay / 365), "year");
|
|
677
|
-
}
|
|
678
|
-
function formatDate(date, format, locale) {
|
|
679
|
-
switch (format) {
|
|
680
|
-
case "relative":
|
|
681
|
-
return getRelativeTime(date);
|
|
682
|
-
case "long":
|
|
683
|
-
return new Intl.DateTimeFormat(locale, {
|
|
684
|
-
dateStyle: "long"
|
|
685
|
-
}).format(date);
|
|
686
|
-
case "datetime":
|
|
687
|
-
return new Intl.DateTimeFormat(locale, {
|
|
688
|
-
dateStyle: "medium",
|
|
689
|
-
timeStyle: "short"
|
|
690
|
-
}).format(date);
|
|
691
|
-
case "short":
|
|
692
|
-
default:
|
|
693
|
-
return new Intl.DateTimeFormat(locale, {
|
|
694
|
-
dateStyle: "medium"
|
|
695
|
-
}).format(date);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
function DateField({
|
|
699
|
-
value,
|
|
700
|
-
format = "short",
|
|
701
|
-
locale = "en"
|
|
702
|
-
}) {
|
|
703
|
-
if (value == null) {
|
|
704
|
-
return createElement12("span", null, "\u2014");
|
|
705
|
-
}
|
|
706
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
707
|
-
if (Number.isNaN(date.getTime())) {
|
|
708
|
-
return createElement12("span", null, "Invalid date");
|
|
709
|
-
}
|
|
710
|
-
return createElement12("time", { dateTime: date.toISOString() }, formatDate(date, format, locale));
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// src/fields/boolean-field.tsx
|
|
714
|
-
import { createElement as createElement13 } from "react";
|
|
715
|
-
function BooleanField({
|
|
716
|
-
value,
|
|
717
|
-
trueLabel = "Yes",
|
|
718
|
-
falseLabel = "No",
|
|
719
|
-
trueColor = "success",
|
|
720
|
-
falseColor = "neutral"
|
|
721
|
-
}) {
|
|
722
|
-
const Chip = useComponent("chip");
|
|
723
|
-
if (value == null) {
|
|
724
|
-
return createElement13("span", null, "\u2014");
|
|
725
|
-
}
|
|
726
|
-
return createElement13(Chip, {
|
|
727
|
-
children: value ? trueLabel : falseLabel,
|
|
728
|
-
color: value ? trueColor : falseColor,
|
|
729
|
-
variant: "soft",
|
|
730
|
-
size: "sm"
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// src/fields/number-field.tsx
|
|
735
|
-
import { createElement as createElement14 } from "react";
|
|
736
|
-
function NumberField({
|
|
737
|
-
value,
|
|
738
|
-
locale = "en",
|
|
739
|
-
currency,
|
|
740
|
-
decimals
|
|
741
|
-
}) {
|
|
742
|
-
if (value == null) {
|
|
743
|
-
return createElement14("span", null, "\u2014");
|
|
744
|
-
}
|
|
745
|
-
const options = {};
|
|
746
|
-
if (currency) {
|
|
747
|
-
options.style = "currency";
|
|
748
|
-
options.currency = currency;
|
|
749
|
-
}
|
|
750
|
-
if (decimals !== void 0) {
|
|
751
|
-
options.minimumFractionDigits = decimals;
|
|
752
|
-
options.maximumFractionDigits = decimals;
|
|
753
|
-
}
|
|
754
|
-
const formatted = new Intl.NumberFormat(locale, options).format(value);
|
|
755
|
-
return createElement14("span", null, formatted);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// src/fields/text-field.tsx
|
|
759
|
-
import { createElement as createElement15 } from "react";
|
|
760
|
-
function TextField({
|
|
761
|
-
value,
|
|
762
|
-
maxLength
|
|
763
|
-
}) {
|
|
764
|
-
if (value == null) {
|
|
765
|
-
return createElement15("span", null, "\u2014");
|
|
766
|
-
}
|
|
767
|
-
const display = maxLength && value.length > maxLength ? `${value.slice(0, maxLength)}\u2026` : value;
|
|
768
|
-
if (maxLength && value.length > maxLength) {
|
|
769
|
-
return createElement15("span", { title: value }, display);
|
|
770
|
-
}
|
|
771
|
-
return createElement15("span", null, display);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// src/fields/email-field.tsx
|
|
775
|
-
import { createElement as createElement16 } from "react";
|
|
776
|
-
function EmailField({ value }) {
|
|
777
|
-
if (value == null) {
|
|
778
|
-
return createElement16("span", null, "\u2014");
|
|
779
|
-
}
|
|
780
|
-
return createElement16("a", { href: `mailto:${value}` }, value);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// src/fields/json-field.tsx
|
|
784
|
-
import { createElement as createElement17, useState as useState3 } from "react";
|
|
785
|
-
function JsonField({
|
|
786
|
-
value,
|
|
787
|
-
collapsed = false
|
|
788
|
-
}) {
|
|
789
|
-
const [isCollapsed, setIsCollapsed] = useState3(collapsed);
|
|
790
|
-
if (value == null) {
|
|
791
|
-
return createElement17("span", null, "\u2014");
|
|
792
|
-
}
|
|
793
|
-
const formatted = JSON.stringify(value, null, 2);
|
|
794
|
-
if (isCollapsed) {
|
|
795
|
-
const preview = JSON.stringify(value);
|
|
796
|
-
const short = preview.length > 60 ? `${preview.slice(0, 60)}\u2026` : preview;
|
|
797
|
-
return createElement17(
|
|
798
|
-
"span",
|
|
799
|
-
null,
|
|
800
|
-
createElement17("code", null, short),
|
|
801
|
-
" ",
|
|
802
|
-
createElement17("button", {
|
|
803
|
-
onClick: () => setIsCollapsed(false),
|
|
804
|
-
style: { border: "none", background: "none", cursor: "pointer", color: "#666", fontSize: "12px" }
|
|
805
|
-
}, "expand")
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
return createElement17(
|
|
809
|
-
"pre",
|
|
810
|
-
{
|
|
811
|
-
style: {
|
|
812
|
-
margin: 0,
|
|
813
|
-
fontSize: "13px",
|
|
814
|
-
backgroundColor: "#f5f5f5",
|
|
815
|
-
padding: "8px",
|
|
816
|
-
borderRadius: "4px",
|
|
817
|
-
overflow: "auto"
|
|
818
|
-
}
|
|
819
|
-
},
|
|
820
|
-
createElement17("code", null, formatted)
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// src/fields/field-for-column.ts
|
|
825
|
-
function fieldForColumn(column) {
|
|
826
|
-
const { dataType, name } = column;
|
|
827
|
-
const dt = dataType.toLowerCase();
|
|
828
|
-
if (dt === "boolean" || dt === "integer" && name.startsWith("is")) {
|
|
829
|
-
return BooleanField;
|
|
830
|
-
}
|
|
831
|
-
if (dt.includes("timestamp") || dt.includes("date") || dt.includes("datetime")) {
|
|
832
|
-
return DateField;
|
|
833
|
-
}
|
|
834
|
-
if (dt === "integer" || dt === "real" || dt === "numeric" || dt.includes("int") || dt.includes("float") || dt.includes("double") || dt.includes("decimal")) {
|
|
835
|
-
return NumberField;
|
|
836
|
-
}
|
|
837
|
-
if (name === "email" || name.endsWith("Email")) {
|
|
838
|
-
return EmailField;
|
|
839
|
-
}
|
|
840
|
-
if (dt === "json" || dt === "jsonb" || dt === "blob") {
|
|
841
|
-
return JsonField;
|
|
842
|
-
}
|
|
843
|
-
return TextField;
|
|
844
|
-
}
|
|
845
|
-
function fieldsForTable(table) {
|
|
846
|
-
const result = {};
|
|
847
|
-
for (const [key, col] of Object.entries(table)) {
|
|
848
|
-
if (col && typeof col === "object" && "dataType" in col && "name" in col) {
|
|
849
|
-
result[key] = fieldForColumn(col);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
return result;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// src/hooks/use-column-inference.ts
|
|
856
|
-
import { useMemo } from "react";
|
|
857
|
-
function useColumnInference(table, columns) {
|
|
858
|
-
return useMemo(() => {
|
|
859
|
-
if (!table) return [];
|
|
860
|
-
const result = [];
|
|
861
|
-
for (const [key, col] of Object.entries(table)) {
|
|
862
|
-
if (columns && !columns.includes(key)) continue;
|
|
863
|
-
if (!col || typeof col !== "object" || !("dataType" in col) || !("name" in col)) continue;
|
|
864
|
-
const meta = col;
|
|
865
|
-
const field = fieldForColumn(meta);
|
|
866
|
-
const label = key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
867
|
-
result.push({
|
|
868
|
-
key,
|
|
869
|
-
label,
|
|
870
|
-
sortable: true,
|
|
871
|
-
field
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
if (columns) {
|
|
875
|
-
result.sort((a, b) => columns.indexOf(a.key) - columns.indexOf(b.key));
|
|
876
|
-
}
|
|
877
|
-
return result;
|
|
878
|
-
}, [table, columns]);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
export {
|
|
882
|
-
createUIPlugin,
|
|
883
|
-
UIPluginProvider,
|
|
884
|
-
useUIPlugin,
|
|
885
|
-
useComponent,
|
|
886
|
-
useConfirm,
|
|
887
|
-
useActionToast,
|
|
888
|
-
PermissionGate,
|
|
889
|
-
ActionButton,
|
|
890
|
-
ConfirmProvider,
|
|
891
|
-
FormStatus,
|
|
892
|
-
RoleBadge,
|
|
893
|
-
ImpersonationBanner,
|
|
894
|
-
DataTable,
|
|
895
|
-
FilterBar,
|
|
896
|
-
BulkActionBar,
|
|
897
|
-
DateField,
|
|
898
|
-
BooleanField,
|
|
899
|
-
NumberField,
|
|
900
|
-
TextField,
|
|
901
|
-
EmailField,
|
|
902
|
-
JsonField,
|
|
903
|
-
fieldForColumn,
|
|
904
|
-
fieldsForTable,
|
|
905
|
-
useColumnInference
|
|
906
|
-
};
|