elysia-rack 0.5.4 → 0.5.8
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 +3 -0
- package/dist/panel-app.js +129 -37
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +24 -2
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -231,6 +231,9 @@ new Elysia().use(
|
|
|
231
231
|
);
|
|
232
232
|
|
|
233
233
|
// override panel.css (file path, raw CSS string, or { path } / { content })
|
|
234
|
+
// Custom CSS is appended after the default panel.css, so it cascades over
|
|
235
|
+
// the default instead of replacing it — useful for changing only a few
|
|
236
|
+
// tokens or rules.
|
|
234
237
|
new Elysia().use(reactPlugin({ css: "./my-panel.css" }));
|
|
235
238
|
```
|
|
236
239
|
|
package/dist/panel-app.js
CHANGED
|
@@ -233,25 +233,64 @@ function actionButton(action, id) {
|
|
|
233
233
|
const target = esc(action.href.replaceAll(":id", encodeURIComponent(id)));
|
|
234
234
|
const cls = "text-link hover:text-link-hover ml-3 px-1 text-sm font-medium hover:underline";
|
|
235
235
|
if (action.method) {
|
|
236
|
-
const
|
|
237
|
-
|
|
236
|
+
const btn = document.createElement("button");
|
|
237
|
+
btn.type = "button";
|
|
238
|
+
btn.dataset.rackHref = target;
|
|
239
|
+
btn.dataset.rackMethod = esc(action.method);
|
|
240
|
+
btn.className = cls;
|
|
241
|
+
if (action.confirm) {
|
|
242
|
+
btn.dataset.rackConfirm = esc(action.confirm);
|
|
243
|
+
}
|
|
244
|
+
btn.textContent = esc(action.label);
|
|
245
|
+
return btn;
|
|
238
246
|
}
|
|
239
|
-
|
|
247
|
+
const a = document.createElement("a");
|
|
248
|
+
a.href = target;
|
|
249
|
+
a.className = cls;
|
|
250
|
+
a.textContent = esc(action.label);
|
|
251
|
+
return a;
|
|
240
252
|
}
|
|
241
253
|
function actionCell(id, cfg, deleted) {
|
|
242
|
-
|
|
254
|
+
const td = document.createElement("td");
|
|
255
|
+
td.className = "border-table-border border-t px-3 py-2.5 whitespace-nowrap text-right";
|
|
256
|
+
const cls = "text-link hover:text-link-hover px-1 text-sm font-medium hover:underline";
|
|
243
257
|
if (deleted) {
|
|
244
|
-
if (cfg.restorable)
|
|
245
|
-
|
|
258
|
+
if (cfg.restorable) {
|
|
259
|
+
const btn = document.createElement("button");
|
|
260
|
+
btn.type = "button";
|
|
261
|
+
btn.dataset.restoreId = esc(id);
|
|
262
|
+
btn.className = cls;
|
|
263
|
+
btn.textContent = "Restore";
|
|
264
|
+
td.appendChild(btn);
|
|
265
|
+
}
|
|
246
266
|
} else {
|
|
247
|
-
if (cfg.viewable)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
267
|
+
if (cfg.viewable) {
|
|
268
|
+
const btn = document.createElement("button");
|
|
269
|
+
btn.type = "button";
|
|
270
|
+
btn.dataset.viewId = esc(id);
|
|
271
|
+
btn.className = cls;
|
|
272
|
+
btn.textContent = "View";
|
|
273
|
+
td.appendChild(btn);
|
|
274
|
+
}
|
|
275
|
+
if (cfg.editable) {
|
|
276
|
+
const editBtn = document.createElement("button");
|
|
277
|
+
editBtn.type = "button";
|
|
278
|
+
editBtn.dataset.editId = esc(id);
|
|
279
|
+
editBtn.className = `text-link hover:text-link-hover${cfg.viewable ? " ml-3" : ""} px-1 text-sm font-medium hover:underline`;
|
|
280
|
+
editBtn.textContent = "Edit";
|
|
281
|
+
td.appendChild(editBtn);
|
|
282
|
+
const delBtn = document.createElement("button");
|
|
283
|
+
delBtn.type = "button";
|
|
284
|
+
delBtn.dataset.deleteId = esc(id);
|
|
285
|
+
delBtn.className = "text-destructive ml-3 px-1 text-sm font-medium hover:underline";
|
|
286
|
+
delBtn.textContent = "Delete";
|
|
287
|
+
td.appendChild(delBtn);
|
|
288
|
+
}
|
|
289
|
+
for (const action of cfg.actions) {
|
|
290
|
+
td.appendChild(actionButton(action, id));
|
|
291
|
+
}
|
|
253
292
|
}
|
|
254
|
-
return
|
|
293
|
+
return td;
|
|
255
294
|
}
|
|
256
295
|
function numericCols(rows, cols) {
|
|
257
296
|
const out = new Set;
|
|
@@ -263,37 +302,68 @@ function numericCols(rows, cols) {
|
|
|
263
302
|
}
|
|
264
303
|
return out;
|
|
265
304
|
}
|
|
266
|
-
function
|
|
305
|
+
function cellText(field, value) {
|
|
267
306
|
if (value === null || value === undefined)
|
|
268
307
|
return "";
|
|
269
|
-
|
|
270
|
-
if (field?.format === "badge" || field?.name === "status")
|
|
271
|
-
return `<span class="rounded-sm px-1.5 py-0.5 text-xs font-medium ${badgeClass(value)}">${text}</span>`;
|
|
272
|
-
return text;
|
|
308
|
+
return esc(displayValue(field, value));
|
|
273
309
|
}
|
|
274
310
|
function paintRows(tbody, cols, rows, cfg) {
|
|
275
311
|
const showActions = cfg.editable || cfg.viewable || cfg.softDelete || cfg.actions.length > 0;
|
|
276
312
|
const span = cols.length + (cfg.selectable ? 1 : 0) + (showActions ? 1 : 0);
|
|
277
|
-
if (!rows.length) {
|
|
278
|
-
tbody.innerHTML = `<tr><td colspan="${span || 1}" class="text-text-muted px-4 py-10 text-center">No records found.</td></tr>`;
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
313
|
const numeric = numericCols(rows, cols);
|
|
282
|
-
|
|
314
|
+
if (!rows.length) {
|
|
315
|
+
tbody.replaceChildren();
|
|
316
|
+
const tr = document.createElement("tr");
|
|
317
|
+
const td = document.createElement("td");
|
|
318
|
+
td.colSpan = span || 1;
|
|
319
|
+
td.className = "text-text-muted px-4 py-10 text-center";
|
|
320
|
+
td.textContent = "No records found.";
|
|
321
|
+
tr.appendChild(td);
|
|
322
|
+
tbody.appendChild(tr);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
tbody.replaceChildren();
|
|
326
|
+
for (const row of rows) {
|
|
283
327
|
const id = row[cfg.pk] == null ? "" : String(row[cfg.pk]);
|
|
284
328
|
const deleted = cfg.softDelete && row[cfg.deletedAt] != null;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
329
|
+
const tr = document.createElement("tr");
|
|
330
|
+
tr.className = "hover:bg-table-row-hover";
|
|
331
|
+
if (cfg.selectable) {
|
|
332
|
+
const td = document.createElement("td");
|
|
333
|
+
td.className = "border-table-border border-t px-3 py-2.5";
|
|
334
|
+
const input = document.createElement("input");
|
|
335
|
+
input.type = "checkbox";
|
|
336
|
+
input.dataset.selectRow = "";
|
|
337
|
+
input.value = id;
|
|
338
|
+
input.setAttribute("aria-label", `Select row ${esc(id)}`);
|
|
339
|
+
input.className = "accent-primary size-4";
|
|
340
|
+
td.appendChild(input);
|
|
341
|
+
tr.appendChild(td);
|
|
342
|
+
}
|
|
343
|
+
for (const col of cols) {
|
|
289
344
|
const field = cfg.byName.get(col);
|
|
290
345
|
const align = numeric.has(col) || field && isNumericField(field) ? " text-right tabular-nums" : "";
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
346
|
+
const td = document.createElement("td");
|
|
347
|
+
td.className = `border-table-border border-t px-3 py-2.5${align}`;
|
|
348
|
+
const value = row[col];
|
|
349
|
+
const badge = field?.format === "badge" || field?.name === "status";
|
|
350
|
+
if (value === null || value === undefined) {
|
|
351
|
+
td.textContent = "";
|
|
352
|
+
} else if (badge) {
|
|
353
|
+
const span2 = document.createElement("span");
|
|
354
|
+
span2.className = `rounded-sm px-1.5 py-0.5 text-xs font-medium ${badgeClass(value)}`;
|
|
355
|
+
span2.textContent = displayValue(field, value);
|
|
356
|
+
td.appendChild(span2);
|
|
357
|
+
} else {
|
|
358
|
+
td.textContent = cellText(field, value);
|
|
359
|
+
}
|
|
360
|
+
tr.appendChild(td);
|
|
361
|
+
}
|
|
362
|
+
if (showActions) {
|
|
363
|
+
tr.appendChild(actionCell(id, cfg, Boolean(deleted)));
|
|
364
|
+
}
|
|
365
|
+
tbody.appendChild(tr);
|
|
366
|
+
}
|
|
297
367
|
}
|
|
298
368
|
function paintPages(box, params, page, totalPages) {
|
|
299
369
|
const linkCls = "border-border rounded-md border px-2.5 py-1 text-xs hover:bg-navigation-hover";
|
|
@@ -501,14 +571,36 @@ async function openDetail(root, id) {
|
|
|
501
571
|
if (label)
|
|
502
572
|
label.textContent = `#${id}`;
|
|
503
573
|
const body = dlg.querySelector("[data-detail-body]");
|
|
504
|
-
if (body)
|
|
505
|
-
body.
|
|
574
|
+
if (body) {
|
|
575
|
+
body.replaceChildren(renderDetail(root, row));
|
|
576
|
+
}
|
|
506
577
|
dlg.showModal?.();
|
|
507
578
|
}
|
|
508
579
|
function renderDetail(root, row) {
|
|
509
580
|
const cfg = tableConfig(root);
|
|
510
|
-
const
|
|
511
|
-
|
|
581
|
+
const frag = document.createDocumentFragment();
|
|
582
|
+
const visibleFields = cfg.fields.filter((field) => row[field.name] !== undefined && !field.hideInTable);
|
|
583
|
+
if (!visibleFields.length) {
|
|
584
|
+
const p = document.createElement("p");
|
|
585
|
+
p.className = "text-text-muted text-sm";
|
|
586
|
+
p.textContent = "No fields";
|
|
587
|
+
frag.appendChild(p);
|
|
588
|
+
return frag;
|
|
589
|
+
}
|
|
590
|
+
for (const field of visibleFields) {
|
|
591
|
+
const wrapper = document.createElement("div");
|
|
592
|
+
wrapper.className = "flex flex-col gap-0.5";
|
|
593
|
+
const dt = document.createElement("dt");
|
|
594
|
+
dt.className = "text-text-muted text-xs font-medium";
|
|
595
|
+
dt.textContent = String(field.label ?? "");
|
|
596
|
+
const dd = document.createElement("dd");
|
|
597
|
+
dd.className = "text-foreground text-sm";
|
|
598
|
+
dd.textContent = displayValue(field, row[field.name]) || "—";
|
|
599
|
+
wrapper.appendChild(dt);
|
|
600
|
+
wrapper.appendChild(dd);
|
|
601
|
+
frag.appendChild(wrapper);
|
|
602
|
+
}
|
|
603
|
+
return frag;
|
|
512
604
|
}
|
|
513
605
|
async function deleteOne(root, id) {
|
|
514
606
|
if (!window.confirm(`Delete record ${id}?`))
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Elysia from "elysia";
|
|
2
2
|
import { ReactRack } from "./types";
|
|
3
|
+
export declare const lazyPage: typeof ReactRack.lazyPage;
|
|
3
4
|
export declare function page<Props = Record<string, never>>(path: string, props?: Props): ReactRack.PageDescriptor<Props>;
|
|
4
5
|
export declare const pages: ReactRack.PageRegistry;
|
|
5
6
|
export declare function reactPlugin(options?: ReactRack.ReactPluginOptions): Elysia<"", {
|
package/dist/react/index.js
CHANGED
|
@@ -149,6 +149,7 @@ function createRenderer(pages) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// src/react/index.ts
|
|
152
|
+
var lazyPage = ReactRack.lazyPage;
|
|
152
153
|
var PANEL_ASSETS = {
|
|
153
154
|
"panel.css": "text/css; charset=utf-8",
|
|
154
155
|
"panel-theme.js": "text/javascript; charset=utf-8",
|
|
@@ -260,14 +261,34 @@ function reactPlugin(options = {}) {
|
|
|
260
261
|
return status(404, "Panel asset not found");
|
|
261
262
|
if (params.name === "panel.css") {
|
|
262
263
|
const custom = await loadCustomPanelCss();
|
|
264
|
+
const dir2 = resolveDistDir();
|
|
265
|
+
let defaultCss = null;
|
|
266
|
+
if (dir2) {
|
|
267
|
+
try {
|
|
268
|
+
defaultCss = await readFile2(join2(dir2, "panel.css"), "utf-8");
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
263
271
|
if (custom !== null) {
|
|
264
|
-
|
|
272
|
+
const combined = defaultCss ? `${defaultCss}
|
|
273
|
+
|
|
274
|
+
/* Custom panel.css overrides */
|
|
275
|
+
${custom}` : custom;
|
|
276
|
+
return new Response(combined, {
|
|
265
277
|
headers: {
|
|
266
278
|
"Content-Type": type,
|
|
267
279
|
"Cache-Control": "public, max-age=3600"
|
|
268
280
|
}
|
|
269
281
|
});
|
|
270
282
|
}
|
|
283
|
+
if (defaultCss) {
|
|
284
|
+
return new Response(defaultCss, {
|
|
285
|
+
headers: {
|
|
286
|
+
"Content-Type": type,
|
|
287
|
+
"Cache-Control": "public, max-age=3600"
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return status(404, "Panel asset not found");
|
|
271
292
|
}
|
|
272
293
|
const dir = resolveDistDir();
|
|
273
294
|
if (!dir)
|
|
@@ -298,5 +319,6 @@ function reactPlugin(options = {}) {
|
|
|
298
319
|
export {
|
|
299
320
|
reactPlugin,
|
|
300
321
|
pages,
|
|
301
|
-
page
|
|
322
|
+
page,
|
|
323
|
+
lazyPage
|
|
302
324
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elysia-rack",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "Model-driven CRUD rack for Elysia: REST + QUERY routes, validation, authorization, OpenAPI metadata, and React panel rendering.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"build:js": "bun build ./src/rack/index.ts ./src/react/index.ts --outdir ./dist --target node --format esm --splitting --external elysia --external react --external react-dom --external lucide-react",
|
|
60
60
|
"build:client": "bun build src/react/page/client/panel.ts --outfile dist/panel-app.js --target browser --format esm && bun build src/react/page/client/theme.ts --outfile dist/panel-theme.js --target browser --format esm && mkdir -p dist && cp src/react/app.html dist/app.html 2>/dev/null || true",
|
|
61
61
|
"build:dts": "tsc -p tsconfig.build.json",
|
|
62
|
-
"prepublishOnly": "bun run typecheck && bun run build"
|
|
62
|
+
"prepublishOnly": "bun run typecheck && bun run build",
|
|
63
|
+
"docs:build": "vitepress build docs"
|
|
63
64
|
},
|
|
64
65
|
"peerDependencies": {
|
|
65
66
|
"elysia": "^1.4.0",
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"@tailwindcss/cli": "4.3.3",
|
|
78
79
|
"@types/bun": "latest",
|
|
79
80
|
"@types/react": "^19.2.18",
|
|
81
|
+
"vitepress": "1",
|
|
80
82
|
"@types/react-dom": "^19.2.5",
|
|
81
83
|
"drizzle-orm": "^0.45.2",
|
|
82
84
|
"elysia": "^1.4.30",
|