elysia-rack 0.2.0 → 0.3.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.
@@ -0,0 +1,790 @@
1
+ import {
2
+ Masthead,
3
+ Sidebar
4
+ } from "./index-rg87f3v1.js";
5
+ import {
6
+ getRackTree
7
+ } from "./index-4wxvmd8a.js";
8
+ import {
9
+ resolveDonateProp
10
+ } from "./index-b8sq8rbd.js";
11
+ import"./index-37x76zdn.js";
12
+
13
+ // src/react/page/components/Breadcrumb.tsx
14
+ import { jsxDEV } from "react/jsx-dev-runtime";
15
+ function Breadcrumb({ trail }) {
16
+ return /* @__PURE__ */ jsxDEV("nav", {
17
+ "aria-label": "Breadcrumb",
18
+ className: "pb-3 text-xs",
19
+ children: /* @__PURE__ */ jsxDEV("ol", {
20
+ className: "flex flex-wrap items-center gap-1.5",
21
+ children: trail.map((item, i) => {
22
+ const last = i === trail.length - 1;
23
+ return /* @__PURE__ */ jsxDEV("li", {
24
+ className: "flex items-center gap-1.5",
25
+ children: [
26
+ i > 0 ? /* @__PURE__ */ jsxDEV("span", {
27
+ "aria-hidden": "true",
28
+ className: "text-text-muted",
29
+ children: "/"
30
+ }, undefined, false, undefined, this) : null,
31
+ item.href && !last ? /* @__PURE__ */ jsxDEV("a", {
32
+ href: item.href,
33
+ className: "text-breadcrumb-foreground hover:text-foreground",
34
+ children: item.label
35
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("span", {
36
+ "aria-current": last ? "page" : undefined,
37
+ className: last ? "text-foreground font-medium" : "text-breadcrumb-foreground",
38
+ children: item.label
39
+ }, undefined, false, undefined, this)
40
+ ]
41
+ }, `${item.label}-${i}`, true, undefined, this);
42
+ })
43
+ }, undefined, false, undefined, this)
44
+ }, undefined, false, undefined, this);
45
+ }
46
+
47
+ // src/react/page/components/href.ts
48
+ function href(params, over = {}) {
49
+ const query = new URLSearchParams;
50
+ for (const [key, value] of Object.entries({ ...params, ...over })) {
51
+ if (value === undefined || value === null || value === "")
52
+ continue;
53
+ query.set(key, Array.isArray(value) ? value.join(",") : String(value));
54
+ }
55
+ const str = query.toString();
56
+ return str ? `?${str}` : "?";
57
+ }
58
+ function pageWindow(page, total) {
59
+ const keep = new Set([1, total, page - 1, page, page + 1]);
60
+ const sorted = [...keep].filter((p) => p >= 1 && p <= total).sort((a, b) => a - b);
61
+ const out = [];
62
+ let prev = 0;
63
+ for (const p of sorted) {
64
+ if (p - prev > 1)
65
+ out.push("…");
66
+ out.push(p);
67
+ prev = p;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ // src/react/page/components/DataTable.tsx
73
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
74
+ function statusTag(value) {
75
+ if (typeof value !== "string")
76
+ return null;
77
+ const text = value.toLowerCase();
78
+ if (text === "active")
79
+ return "bg-success-muted text-success-muted-foreground";
80
+ if (text === "archived")
81
+ return "bg-warning-muted text-warning-muted-foreground";
82
+ return "bg-badge-background text-badge-foreground";
83
+ }
84
+ function cell(value) {
85
+ if (value === null || value === undefined)
86
+ return "";
87
+ if (value instanceof Date)
88
+ return value.toLocaleString("en-US");
89
+ if (typeof value === "object")
90
+ return JSON.stringify(value);
91
+ return String(value);
92
+ }
93
+ function isNumericColumn(rows, col) {
94
+ if (rows.length === 0)
95
+ return false;
96
+ return rows.every((r) => typeof r[col] === "number" || r[col] == null);
97
+ }
98
+ function DataTable({
99
+ columns,
100
+ rows,
101
+ sortable,
102
+ params,
103
+ selectable,
104
+ editable,
105
+ primaryKey
106
+ }) {
107
+ const activeSort = typeof params.sort === "string" ? params.sort : "";
108
+ const activeOrder = params.order === "desc" ? "desc" : "asc";
109
+ const pk = primaryKey ?? "id";
110
+ const extra = (selectable ? 1 : 0) + (editable ? 1 : 0);
111
+ const numeric = new Set(columns.filter((c) => isNumericColumn(rows, c)));
112
+ const cellClass = (col) => `border-table-border border-t px-3 py-2.5 ${numeric.has(col) ? "text-right tabular-nums" : ""}`;
113
+ if (rows.length === 0)
114
+ return /* @__PURE__ */ jsxDEV2("div", {
115
+ className: "border-border bg-table overflow-x-auto rounded-lg border",
116
+ children: /* @__PURE__ */ jsxDEV2("table", {
117
+ className: "w-full border-collapse text-left text-sm",
118
+ "data-selectable": selectable ? true : undefined,
119
+ "data-editable": editable ? true : undefined,
120
+ "data-pk": primaryKey ?? "id",
121
+ children: /* @__PURE__ */ jsxDEV2("tbody", {
122
+ "data-panel-rows": true,
123
+ children: /* @__PURE__ */ jsxDEV2("tr", {
124
+ children: /* @__PURE__ */ jsxDEV2("td", {
125
+ colSpan: columns.length + extra || 1,
126
+ className: "text-text-muted px-4 py-10 text-center",
127
+ children: "No records found."
128
+ }, undefined, false, undefined, this)
129
+ }, undefined, false, undefined, this)
130
+ }, undefined, false, undefined, this)
131
+ }, undefined, false, undefined, this)
132
+ }, undefined, false, undefined, this);
133
+ return /* @__PURE__ */ jsxDEV2("div", {
134
+ className: "border-border bg-table overflow-x-auto rounded-lg border",
135
+ children: /* @__PURE__ */ jsxDEV2("table", {
136
+ className: "w-full border-collapse text-left text-sm",
137
+ "data-selectable": selectable ? true : undefined,
138
+ "data-editable": editable ? true : undefined,
139
+ "data-pk": primaryKey ?? "id",
140
+ children: [
141
+ /* @__PURE__ */ jsxDEV2("thead", {
142
+ children: /* @__PURE__ */ jsxDEV2("tr", {
143
+ children: [
144
+ selectable ? /* @__PURE__ */ jsxDEV2("th", {
145
+ className: "border-border w-10 border-b px-3 py-2",
146
+ children: /* @__PURE__ */ jsxDEV2("input", {
147
+ type: "checkbox",
148
+ "data-select-all": true,
149
+ "aria-label": "Select all",
150
+ className: "accent-primary size-4"
151
+ }, undefined, false, undefined, this)
152
+ }, undefined, false, undefined, this) : null,
153
+ columns.map((col) => /* @__PURE__ */ jsxDEV2("th", {
154
+ "data-col": col,
155
+ className: `border-border text-text-muted border-b px-3 py-2 text-xs font-medium ${numeric.has(col) ? "text-right" : ""}`,
156
+ children: sortable?.includes(col) ? /* @__PURE__ */ jsxDEV2("a", {
157
+ className: "hover:text-foreground text-link",
158
+ "data-qlink": true,
159
+ "data-sort-link": col,
160
+ href: href(params, {
161
+ sort: col,
162
+ order: activeSort === col && activeOrder === "asc" ? "desc" : "asc"
163
+ }),
164
+ children: [
165
+ col,
166
+ /* @__PURE__ */ jsxDEV2("span", {
167
+ "data-sort-ind": true,
168
+ children: activeSort === col ? activeOrder === "asc" ? " ▲" : " ▼" : ""
169
+ }, undefined, false, undefined, this)
170
+ ]
171
+ }, undefined, true, undefined, this) : col
172
+ }, col, false, undefined, this)),
173
+ editable ? /* @__PURE__ */ jsxDEV2("th", {
174
+ className: "border-border text-text-muted border-b px-3 py-2 text-right text-xs font-medium",
175
+ children: "Actions"
176
+ }, undefined, false, undefined, this) : null
177
+ ]
178
+ }, undefined, true, undefined, this)
179
+ }, undefined, false, undefined, this),
180
+ /* @__PURE__ */ jsxDEV2("tbody", {
181
+ "data-panel-rows": true,
182
+ children: rows.map((row, i) => /* @__PURE__ */ jsxDEV2("tr", {
183
+ className: "hover:bg-table-row-hover",
184
+ children: [
185
+ selectable ? /* @__PURE__ */ jsxDEV2("td", {
186
+ className: "border-table-border border-t px-3 py-2.5",
187
+ children: /* @__PURE__ */ jsxDEV2("input", {
188
+ type: "checkbox",
189
+ "data-select-row": true,
190
+ value: String(row[pk] ?? ""),
191
+ "aria-label": `Select row ${String(row[pk] ?? i)}`,
192
+ className: "accent-primary size-4"
193
+ }, undefined, false, undefined, this)
194
+ }, undefined, false, undefined, this) : null,
195
+ columns.map((col) => /* @__PURE__ */ jsxDEV2("td", {
196
+ className: cellClass(col),
197
+ children: col === "status" && statusTag(row[col]) ? /* @__PURE__ */ jsxDEV2("span", {
198
+ className: `rounded-sm px-1.5 py-0.5 text-xs font-medium ${statusTag(row[col]) ?? ""}`,
199
+ children: cell(row[col])
200
+ }, undefined, false, undefined, this) : cell(row[col])
201
+ }, col, false, undefined, this)),
202
+ editable ? /* @__PURE__ */ jsxDEV2("td", {
203
+ className: "border-table-border whitespace-nowrap border-t px-3 py-2.5 text-right",
204
+ children: [
205
+ /* @__PURE__ */ jsxDEV2("button", {
206
+ type: "button",
207
+ "data-edit-id": String(row[pk] ?? ""),
208
+ className: "text-link hover:text-link-hover px-1 text-sm font-medium hover:underline",
209
+ children: "Edit"
210
+ }, undefined, false, undefined, this),
211
+ /* @__PURE__ */ jsxDEV2("button", {
212
+ type: "button",
213
+ "data-delete-id": String(row[pk] ?? ""),
214
+ className: "text-destructive ml-3 px-1 text-sm font-medium hover:underline",
215
+ children: "Delete"
216
+ }, undefined, false, undefined, this)
217
+ ]
218
+ }, undefined, true, undefined, this) : null
219
+ ]
220
+ }, i, true, undefined, this))
221
+ }, undefined, false, undefined, this)
222
+ ]
223
+ }, undefined, true, undefined, this)
224
+ }, undefined, false, undefined, this);
225
+ }
226
+
227
+ // src/react/page/components/Pagination.tsx
228
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
229
+ var pageClass = "border-border rounded-md border px-2.5 py-1 text-xs hover:bg-navigation-hover";
230
+ var currentClass = "bg-primary text-primary-foreground rounded-md px-2.5 py-1 text-xs font-medium";
231
+ var disabledClass = "border-border text-text-disabled rounded-md border px-2.5 py-1 text-xs";
232
+ function Pagination({ page, totalPages, total, limit, params }) {
233
+ const from = total === 0 ? 0 : (page - 1) * limit + 1;
234
+ const to = Math.min(page * limit, total);
235
+ return /* @__PURE__ */ jsxDEV3("nav", {
236
+ className: "flex flex-wrap items-center justify-between gap-3 py-3 text-sm",
237
+ children: [
238
+ /* @__PURE__ */ jsxDEV3("p", {
239
+ className: "text-text-muted text-xs",
240
+ "data-panel-count": true,
241
+ children: total === 0 ? "0 records" : `${from}–${to} of ${total} records`
242
+ }, undefined, false, undefined, this),
243
+ /* @__PURE__ */ jsxDEV3("div", {
244
+ className: "flex items-center gap-1",
245
+ "data-panel-pages": true,
246
+ children: [
247
+ page > 1 ? /* @__PURE__ */ jsxDEV3("a", {
248
+ className: pageClass,
249
+ "data-qlink": true,
250
+ href: href(params, { page: page - 1 }),
251
+ children: "Previous"
252
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3("span", {
253
+ className: disabledClass,
254
+ children: "Previous"
255
+ }, undefined, false, undefined, this),
256
+ pageWindow(page, totalPages).map((p, i) => p === "…" ? /* @__PURE__ */ jsxDEV3("span", {
257
+ className: "text-text-muted px-1",
258
+ children: "…"
259
+ }, `e${i}`, false, undefined, this) : p === page ? /* @__PURE__ */ jsxDEV3("span", {
260
+ className: currentClass,
261
+ children: p
262
+ }, p, false, undefined, this) : /* @__PURE__ */ jsxDEV3("a", {
263
+ className: pageClass,
264
+ "data-qlink": true,
265
+ href: href(params, { page: p }),
266
+ children: p
267
+ }, p, false, undefined, this)),
268
+ page < totalPages ? /* @__PURE__ */ jsxDEV3("a", {
269
+ className: pageClass,
270
+ "data-qlink": true,
271
+ href: href(params, { page: page + 1 }),
272
+ children: "Next"
273
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3("span", {
274
+ className: disabledClass,
275
+ children: "Next"
276
+ }, undefined, false, undefined, this)
277
+ ]
278
+ }, undefined, true, undefined, this)
279
+ ]
280
+ }, undefined, true, undefined, this);
281
+ }
282
+
283
+ // src/react/page/components/Toolbar.tsx
284
+ import { jsxDEV as jsxDEV4, Fragment } from "react/jsx-dev-runtime";
285
+ var inputClass = "border-input bg-input-background text-foreground placeholder:text-form-placeholder focus:border-ring h-8 rounded-md border px-2.5 text-sm";
286
+ var labelClass = "flex flex-col gap-1";
287
+ function Toolbar({ searchable, filterable, sortable, params }) {
288
+ if (!searchable?.length && !filterable?.length && !sortable?.length)
289
+ return null;
290
+ return /* @__PURE__ */ jsxDEV4("form", {
291
+ method: "get",
292
+ action: "",
293
+ className: "flex flex-wrap items-end gap-3 pb-4",
294
+ children: [
295
+ searchable && searchable.length > 0 ? /* @__PURE__ */ jsxDEV4("label", {
296
+ className: labelClass,
297
+ children: [
298
+ /* @__PURE__ */ jsxDEV4("span", {
299
+ className: "text-text-muted text-xs font-medium",
300
+ children: "Search"
301
+ }, undefined, false, undefined, this),
302
+ /* @__PURE__ */ jsxDEV4("input", {
303
+ name: "search",
304
+ "data-search-input": true,
305
+ defaultValue: String(params.search ?? ""),
306
+ placeholder: searchable.join(", "),
307
+ autoComplete: "off",
308
+ className: `${inputClass} w-56 max-w-full`
309
+ }, undefined, false, undefined, this)
310
+ ]
311
+ }, undefined, true, undefined, this) : null,
312
+ (filterable ?? []).map((field) => /* @__PURE__ */ jsxDEV4("label", {
313
+ className: labelClass,
314
+ children: [
315
+ /* @__PURE__ */ jsxDEV4("span", {
316
+ className: "text-text-muted text-xs font-medium capitalize",
317
+ children: field
318
+ }, undefined, false, undefined, this),
319
+ /* @__PURE__ */ jsxDEV4("input", {
320
+ name: field,
321
+ defaultValue: String(params[field] ?? ""),
322
+ className: `${inputClass} w-40 max-w-full`
323
+ }, undefined, false, undefined, this)
324
+ ]
325
+ }, field, true, undefined, this)),
326
+ sortable && sortable.length > 0 ? /* @__PURE__ */ jsxDEV4(Fragment, {
327
+ children: [
328
+ /* @__PURE__ */ jsxDEV4("label", {
329
+ className: labelClass,
330
+ children: [
331
+ /* @__PURE__ */ jsxDEV4("span", {
332
+ className: "text-text-muted text-xs font-medium",
333
+ children: "Sort"
334
+ }, undefined, false, undefined, this),
335
+ /* @__PURE__ */ jsxDEV4("select", {
336
+ name: "sort",
337
+ defaultValue: String(params.sort ?? ""),
338
+ className: inputClass,
339
+ children: [
340
+ /* @__PURE__ */ jsxDEV4("option", {
341
+ value: "",
342
+ children: "Default"
343
+ }, undefined, false, undefined, this),
344
+ sortable.map((field) => /* @__PURE__ */ jsxDEV4("option", {
345
+ value: field,
346
+ children: field
347
+ }, field, false, undefined, this))
348
+ ]
349
+ }, undefined, true, undefined, this)
350
+ ]
351
+ }, undefined, true, undefined, this),
352
+ /* @__PURE__ */ jsxDEV4("label", {
353
+ className: labelClass,
354
+ children: [
355
+ /* @__PURE__ */ jsxDEV4("span", {
356
+ className: "text-text-muted text-xs font-medium",
357
+ children: "Order"
358
+ }, undefined, false, undefined, this),
359
+ /* @__PURE__ */ jsxDEV4("select", {
360
+ name: "order",
361
+ defaultValue: String(params.order ?? ""),
362
+ className: inputClass,
363
+ children: [
364
+ /* @__PURE__ */ jsxDEV4("option", {
365
+ value: "",
366
+ children: "Default"
367
+ }, undefined, false, undefined, this),
368
+ /* @__PURE__ */ jsxDEV4("option", {
369
+ value: "asc",
370
+ children: "Ascending"
371
+ }, undefined, false, undefined, this),
372
+ /* @__PURE__ */ jsxDEV4("option", {
373
+ value: "desc",
374
+ children: "Descending"
375
+ }, undefined, false, undefined, this)
376
+ ]
377
+ }, undefined, true, undefined, this)
378
+ ]
379
+ }, undefined, true, undefined, this)
380
+ ]
381
+ }, undefined, true, undefined, this) : null,
382
+ /* @__PURE__ */ jsxDEV4("button", {
383
+ type: "submit",
384
+ className: "border-border bg-background text-foreground hover:bg-navigation-hover h-8 rounded-md border px-3.5 text-sm font-medium",
385
+ children: "Apply"
386
+ }, undefined, false, undefined, this)
387
+ ]
388
+ }, undefined, true, undefined, this);
389
+ }
390
+
391
+ // src/react/page/Panel.tsx
392
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
393
+ var inputClass2 = "border-input bg-input-background text-foreground placeholder:text-form-placeholder h-9 rounded-md border px-2.5 text-sm";
394
+ var fieldLabelClass = "flex flex-col gap-1.5 text-sm font-medium text-form-label";
395
+ var secondaryButtonClass = "border-border bg-background text-foreground hover:bg-navigation-hover rounded-md border px-3.5 py-1.5 text-sm font-medium";
396
+ var primaryButtonClass = "bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-3.5 py-1.5 text-sm font-medium";
397
+ function FieldInput({ field, required }) {
398
+ switch (field.kind) {
399
+ case "enum":
400
+ return /* @__PURE__ */ jsxDEV5("label", {
401
+ className: fieldLabelClass,
402
+ children: [
403
+ field.name,
404
+ /* @__PURE__ */ jsxDEV5("select", {
405
+ name: field.name,
406
+ required,
407
+ defaultValue: "",
408
+ className: inputClass2,
409
+ children: [
410
+ /* @__PURE__ */ jsxDEV5("option", {
411
+ value: "",
412
+ children: "Select"
413
+ }, undefined, false, undefined, this),
414
+ (field.enumValues ?? []).map((v) => /* @__PURE__ */ jsxDEV5("option", {
415
+ value: v,
416
+ children: v
417
+ }, v, false, undefined, this))
418
+ ]
419
+ }, undefined, true, undefined, this)
420
+ ]
421
+ }, undefined, true, undefined, this);
422
+ case "boolean":
423
+ return /* @__PURE__ */ jsxDEV5("label", {
424
+ className: `${fieldLabelClass} flex-row items-center gap-2`,
425
+ children: [
426
+ /* @__PURE__ */ jsxDEV5("input", {
427
+ type: "checkbox",
428
+ name: field.name,
429
+ value: "true",
430
+ className: "accent-primary size-4"
431
+ }, undefined, false, undefined, this),
432
+ field.name
433
+ ]
434
+ }, undefined, true, undefined, this);
435
+ case "integer":
436
+ case "number":
437
+ return /* @__PURE__ */ jsxDEV5("label", {
438
+ className: fieldLabelClass,
439
+ children: [
440
+ field.name,
441
+ /* @__PURE__ */ jsxDEV5("input", {
442
+ type: "number",
443
+ step: field.kind === "number" ? "any" : "1",
444
+ name: field.name,
445
+ required,
446
+ className: inputClass2
447
+ }, undefined, false, undefined, this)
448
+ ]
449
+ }, undefined, true, undefined, this);
450
+ case "date":
451
+ return /* @__PURE__ */ jsxDEV5("label", {
452
+ className: fieldLabelClass,
453
+ children: [
454
+ field.name,
455
+ /* @__PURE__ */ jsxDEV5("input", {
456
+ type: "datetime-local",
457
+ name: field.name,
458
+ required,
459
+ className: inputClass2
460
+ }, undefined, false, undefined, this)
461
+ ]
462
+ }, undefined, true, undefined, this);
463
+ case "json":
464
+ return /* @__PURE__ */ jsxDEV5("label", {
465
+ className: fieldLabelClass,
466
+ children: [
467
+ field.name,
468
+ /* @__PURE__ */ jsxDEV5("textarea", {
469
+ name: field.name,
470
+ "data-json": true,
471
+ rows: 3,
472
+ placeholder: "{}",
473
+ className: `${inputClass2} font-mono`
474
+ }, undefined, false, undefined, this)
475
+ ]
476
+ }, undefined, true, undefined, this);
477
+ default:
478
+ return /* @__PURE__ */ jsxDEV5("label", {
479
+ className: fieldLabelClass,
480
+ children: [
481
+ field.name,
482
+ /* @__PURE__ */ jsxDEV5("input", {
483
+ name: field.name,
484
+ required,
485
+ className: inputClass2
486
+ }, undefined, false, undefined, this)
487
+ ]
488
+ }, undefined, true, undefined, this);
489
+ }
490
+ }
491
+ function itemLabel(r) {
492
+ return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
493
+ }
494
+ function toSidebarItem(node, activeId) {
495
+ return {
496
+ id: node.metadata.id,
497
+ label: itemLabel(node),
498
+ icon: node.metadata.icon,
499
+ href: node.path,
500
+ active: node.metadata.id === activeId,
501
+ children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, activeId))
502
+ };
503
+ }
504
+ function buildGroupsFromTree(tree, activeId) {
505
+ const byGroup = new Map;
506
+ for (const root of tree) {
507
+ if (root.metadata.hidden)
508
+ continue;
509
+ const name = root.metadata.group ?? "General";
510
+ let group = byGroup.get(name);
511
+ if (!group) {
512
+ group = { name, items: [] };
513
+ byGroup.set(name, group);
514
+ }
515
+ group.items.push(toSidebarItem(root, activeId));
516
+ }
517
+ return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
518
+ }
519
+ function plural(count, word) {
520
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
521
+ }
522
+ async function Panel(props) {
523
+ const params = props.params ?? {};
524
+ const page = Math.max(Number(params.page ?? 1) || 1, 1);
525
+ const limit = Math.max(Number(params.limit ?? props.query?.pagination?.default ?? 20) || 1, 1);
526
+ let rows = [];
527
+ let total = 0;
528
+ if (props.load) {
529
+ try {
530
+ const result = await props.load(params);
531
+ rows = result.data;
532
+ total = result.total;
533
+ } catch {}
534
+ }
535
+ const columns = rows.length > 0 ? Object.keys(rows[0] ?? {}).filter((c) => c !== "deletedAt") : [];
536
+ const title = props.metadata?.pluralLabel ?? props.metadata?.label ?? props.resource ?? "Panel";
537
+ const totalPages = Math.max(1, Math.ceil(total / limit));
538
+ const pk = props.primaryKey ?? "id";
539
+ const deletedAt = props.deletedAtField ?? "deletedAt";
540
+ const fieldDefs = props.fields?.length ? props.fields.filter((f) => !f.autoIncrement && f.name !== deletedAt).map((f) => ({
541
+ name: f.name,
542
+ kind: f.kind,
543
+ nullable: f.nullable,
544
+ enumValues: f.enumValues
545
+ })) : columns.filter((c) => c !== deletedAt).map((name) => ({
546
+ name,
547
+ kind: "text",
548
+ nullable: name === pk
549
+ }));
550
+ const createFields = fieldDefs;
551
+ const editFields = fieldDefs.filter((f) => f.name !== pk);
552
+ const isRequired = (f) => !f.nullable && f.kind !== "boolean";
553
+ const canCreate = props.operations?.create !== false;
554
+ const canEdit = props.operations?.update !== false || props.operations?.replace !== false;
555
+ const canDelete = props.operations?.delete !== false;
556
+ const selectable = canEdit || canDelete;
557
+ const tree = getRackTree().filter((r) => !r.metadata.hidden);
558
+ const groups = buildGroupsFromTree(tree, props.resource);
559
+ const donate = resolveDonateProp(props.donate);
560
+ const trail = [
561
+ { label: "Dashboard", href: "/" },
562
+ ...props.metadata?.group ? [{ label: props.metadata.group }] : [],
563
+ { label: title }
564
+ ];
565
+ return /* @__PURE__ */ jsxDEV5("div", {
566
+ className: "text-foreground flex min-h-screen flex-col lg:flex-row",
567
+ children: [
568
+ /* @__PURE__ */ jsxDEV5(Sidebar, {
569
+ title: "Panel",
570
+ groups
571
+ }, undefined, false, undefined, this),
572
+ /* @__PURE__ */ jsxDEV5("main", {
573
+ className: "min-w-0 flex-1",
574
+ children: /* @__PURE__ */ jsxDEV5("div", {
575
+ className: "mx-auto w-full max-w-5xl px-4 py-4 sm:px-6",
576
+ children: [
577
+ /* @__PURE__ */ jsxDEV5(Breadcrumb, {
578
+ trail
579
+ }, undefined, false, undefined, this),
580
+ /* @__PURE__ */ jsxDEV5("div", {
581
+ "data-query-url": props.queryUrl,
582
+ children: [
583
+ /* @__PURE__ */ jsxDEV5(Masthead, {
584
+ title,
585
+ icon: props.metadata?.icon,
586
+ detail: /* @__PURE__ */ jsxDEV5("span", {
587
+ "data-panel-total": true,
588
+ children: plural(total, "record")
589
+ }, undefined, false, undefined, this),
590
+ actions: canCreate ? /* @__PURE__ */ jsxDEV5("button", {
591
+ type: "button",
592
+ "data-open-create": true,
593
+ className: primaryButtonClass,
594
+ children: "New record"
595
+ }, undefined, false, undefined, this) : undefined
596
+ }, undefined, false, undefined, this),
597
+ /* @__PURE__ */ jsxDEV5(Toolbar, {
598
+ searchable: props.query?.searchable,
599
+ filterable: props.query?.filterable,
600
+ sortable: props.query?.sortable,
601
+ params
602
+ }, undefined, false, undefined, this),
603
+ /* @__PURE__ */ jsxDEV5("div", {
604
+ "data-bulk-bar": true,
605
+ style: { display: "none" },
606
+ className: "flex flex-wrap items-center gap-2 pb-3",
607
+ children: [
608
+ /* @__PURE__ */ jsxDEV5("span", {
609
+ "data-bulk-count": true,
610
+ className: "text-text-muted text-xs"
611
+ }, undefined, false, undefined, this),
612
+ canDelete ? /* @__PURE__ */ jsxDEV5("button", {
613
+ type: "button",
614
+ "data-bulk-delete": true,
615
+ className: "bg-destructive text-destructive-foreground rounded-md px-3 py-1.5 text-xs font-medium",
616
+ children: "Delete selected"
617
+ }, undefined, false, undefined, this) : null,
618
+ canEdit ? /* @__PURE__ */ jsxDEV5("span", {
619
+ className: "inline-flex items-center gap-2",
620
+ children: [
621
+ /* @__PURE__ */ jsxDEV5("select", {
622
+ "data-bulk-field": true,
623
+ "aria-label": "Bulk field",
624
+ className: "border-input bg-input-background h-8 rounded-md border px-2 text-xs",
625
+ children: editFields.map((f) => /* @__PURE__ */ jsxDEV5("option", {
626
+ value: f.name,
627
+ children: f.name
628
+ }, f.name, false, undefined, this))
629
+ }, undefined, false, undefined, this),
630
+ /* @__PURE__ */ jsxDEV5("input", {
631
+ "data-bulk-value": true,
632
+ placeholder: "New value",
633
+ "aria-label": "Bulk value",
634
+ className: "border-input bg-input-background placeholder:text-form-placeholder h-8 rounded-md border px-2 text-xs"
635
+ }, undefined, false, undefined, this),
636
+ /* @__PURE__ */ jsxDEV5("button", {
637
+ type: "button",
638
+ "data-bulk-apply": true,
639
+ className: "bg-secondary text-secondary-foreground hover:bg-navigation-hover rounded-md px-3 py-1.5 text-xs font-medium",
640
+ children: "Apply"
641
+ }, undefined, false, undefined, this)
642
+ ]
643
+ }, undefined, true, undefined, this) : null,
644
+ /* @__PURE__ */ jsxDEV5("button", {
645
+ type: "button",
646
+ "data-bulk-clear": true,
647
+ className: "text-text-muted hover:text-foreground text-xs underline",
648
+ children: "Clear"
649
+ }, undefined, false, undefined, this)
650
+ ]
651
+ }, undefined, true, undefined, this),
652
+ /* @__PURE__ */ jsxDEV5(DataTable, {
653
+ columns,
654
+ rows,
655
+ sortable: props.query?.sortable,
656
+ params,
657
+ selectable,
658
+ editable: canEdit || canDelete,
659
+ primaryKey: pk
660
+ }, undefined, false, undefined, this),
661
+ /* @__PURE__ */ jsxDEV5(Pagination, {
662
+ page,
663
+ totalPages,
664
+ total,
665
+ limit,
666
+ params
667
+ }, undefined, false, undefined, this),
668
+ /* @__PURE__ */ jsxDEV5("dialog", {
669
+ id: "rack-create",
670
+ className: "bg-card text-card-foreground w-full max-w-md rounded-lg border p-0 shadow-lg",
671
+ children: /* @__PURE__ */ jsxDEV5("form", {
672
+ "data-create-form": true,
673
+ className: "flex flex-col gap-4 p-5",
674
+ children: [
675
+ /* @__PURE__ */ jsxDEV5("h2", {
676
+ className: "font-display text-xl leading-tight",
677
+ children: "New record"
678
+ }, undefined, false, undefined, this),
679
+ createFields.length > 0 ? createFields.map((f) => /* @__PURE__ */ jsxDEV5(FieldInput, {
680
+ field: f,
681
+ required: isRequired(f)
682
+ }, f.name, false, undefined, this)) : /* @__PURE__ */ jsxDEV5("label", {
683
+ className: "flex flex-col gap-1.5 text-sm font-medium text-form-label",
684
+ children: [
685
+ "JSON body",
686
+ /* @__PURE__ */ jsxDEV5("textarea", {
687
+ "data-create-json": true,
688
+ rows: 4,
689
+ placeholder: '{"name": "..."}',
690
+ className: `${inputClass2} h-auto font-mono`
691
+ }, undefined, false, undefined, this)
692
+ ]
693
+ }, undefined, true, undefined, this),
694
+ /* @__PURE__ */ jsxDEV5("p", {
695
+ "data-form-error": true,
696
+ className: "text-form-error text-sm"
697
+ }, undefined, false, undefined, this),
698
+ /* @__PURE__ */ jsxDEV5("div", {
699
+ className: "border-border flex justify-end gap-2 border-t pt-4",
700
+ children: [
701
+ /* @__PURE__ */ jsxDEV5("button", {
702
+ type: "button",
703
+ "data-close": true,
704
+ className: secondaryButtonClass,
705
+ children: "Cancel"
706
+ }, undefined, false, undefined, this),
707
+ /* @__PURE__ */ jsxDEV5("button", {
708
+ type: "submit",
709
+ className: primaryButtonClass,
710
+ children: "Create"
711
+ }, undefined, false, undefined, this)
712
+ ]
713
+ }, undefined, true, undefined, this)
714
+ ]
715
+ }, undefined, true, undefined, this)
716
+ }, undefined, false, undefined, this),
717
+ /* @__PURE__ */ jsxDEV5("dialog", {
718
+ id: "rack-edit",
719
+ className: "bg-card text-card-foreground w-full max-w-md rounded-lg border p-0 shadow-lg",
720
+ children: /* @__PURE__ */ jsxDEV5("form", {
721
+ "data-edit-form": true,
722
+ className: "flex flex-col gap-4 p-5",
723
+ children: [
724
+ /* @__PURE__ */ jsxDEV5("h2", {
725
+ className: "font-display text-xl leading-tight",
726
+ children: [
727
+ "Edit record",
728
+ " ",
729
+ /* @__PURE__ */ jsxDEV5("span", {
730
+ "data-edit-id-label": true,
731
+ className: "text-text-muted font-mono text-sm"
732
+ }, undefined, false, undefined, this)
733
+ ]
734
+ }, undefined, true, undefined, this),
735
+ editFields.map((f) => /* @__PURE__ */ jsxDEV5(FieldInput, {
736
+ field: f
737
+ }, f.name, false, undefined, this)),
738
+ /* @__PURE__ */ jsxDEV5("p", {
739
+ "data-form-error": true,
740
+ className: "text-form-error text-sm"
741
+ }, undefined, false, undefined, this),
742
+ /* @__PURE__ */ jsxDEV5("div", {
743
+ className: "border-border flex justify-end gap-2 border-t pt-4",
744
+ children: [
745
+ /* @__PURE__ */ jsxDEV5("button", {
746
+ type: "button",
747
+ "data-close": true,
748
+ className: secondaryButtonClass,
749
+ children: "Cancel"
750
+ }, undefined, false, undefined, this),
751
+ /* @__PURE__ */ jsxDEV5("button", {
752
+ type: "submit",
753
+ className: primaryButtonClass,
754
+ children: "Save"
755
+ }, undefined, false, undefined, this)
756
+ ]
757
+ }, undefined, true, undefined, this)
758
+ ]
759
+ }, undefined, true, undefined, this)
760
+ }, undefined, false, undefined, this),
761
+ /* @__PURE__ */ jsxDEV5("script", {
762
+ type: "module",
763
+ src: "/__rack/panel-app.js"
764
+ }, undefined, false, undefined, this)
765
+ ]
766
+ }, undefined, true, undefined, this),
767
+ /* @__PURE__ */ jsxDEV5("footer", {
768
+ className: "border-border text-text-muted mt-6 flex flex-wrap items-center justify-between gap-2 border-t pt-3 text-xs",
769
+ children: [
770
+ /* @__PURE__ */ jsxDEV5("span", {
771
+ children: "elysia-rack"
772
+ }, undefined, false, undefined, this),
773
+ donate.enabled ? /* @__PURE__ */ jsxDEV5("a", {
774
+ href: donate.url,
775
+ target: "_blank",
776
+ rel: "noopener noreferrer",
777
+ className: "hover:text-foreground underline-offset-4 hover:underline",
778
+ children: donate.label
779
+ }, undefined, false, undefined, this) : null
780
+ ]
781
+ }, undefined, true, undefined, this)
782
+ ]
783
+ }, undefined, true, undefined, this)
784
+ }, undefined, false, undefined, this)
785
+ ]
786
+ }, undefined, true, undefined, this);
787
+ }
788
+ export {
789
+ Panel as default
790
+ };