elysia-rack 0.2.2 → 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.
Files changed (38) hide show
  1. package/README.md +22 -15
  2. package/dist/Dashboard-4wv82gwx.js +212 -0
  3. package/dist/Dashboard-jkvfzyk2.js +210 -0
  4. package/dist/Dashboard-zhg2jtq5.js +211 -0
  5. package/dist/{Panel-fbp3wrsw.js → Panel-1qkzh8t7.js} +355 -311
  6. package/dist/Panel-7zpqz8gy.js +782 -0
  7. package/dist/Panel-9mmh3tbh.js +790 -0
  8. package/dist/Panel-ba0qzknj.js +789 -0
  9. package/dist/Panel-e6jyf4rc.js +789 -0
  10. package/dist/index-0dwhzk3j.js +145 -0
  11. package/dist/{index-6v0ns1xn.js → index-b8sq8rbd.js} +1 -1
  12. package/dist/index-rg87f3v1.js +174 -0
  13. package/dist/panel-app.js +27 -11
  14. package/dist/panel.css +2 -1
  15. package/dist/rack/types/metadata.d.ts +2 -1
  16. package/dist/react/donate.d.ts +1 -1
  17. package/dist/react/index.js +3 -3
  18. package/dist/react/page/Panel.d.ts +1 -0
  19. package/dist/react/page/components/Icon.d.ts +10 -0
  20. package/dist/react/page/components/Masthead.d.ts +5 -4
  21. package/package.json +8 -3
  22. package/dist/Dashboard-0da8dvbv.js +0 -225
  23. package/dist/Dashboard-2j561p7c.js +0 -225
  24. package/dist/Dashboard-5mt9hnbw.js +0 -252
  25. package/dist/Dashboard-7gmx8c9y.js +0 -248
  26. package/dist/Dashboard-s8nfyf7s.js +0 -39
  27. package/dist/Dashboard-xkh2g9f7.js +0 -248
  28. package/dist/Dashboard-xpstr7fr.js +0 -172
  29. package/dist/Panel-3yrk3krj.js +0 -914
  30. package/dist/Panel-b7jhgvnm.js +0 -916
  31. package/dist/Panel-n0kstkyw.js +0 -735
  32. package/dist/Panel-pkvqf5v8.js +0 -912
  33. package/dist/Panel-wjxjtbmr.js +0 -753
  34. package/dist/Panel-za4av616.js +0 -753
  35. package/dist/index-h3zbq796.js +0 -39
  36. package/dist/index-tgk4jt4d.js +0 -63
  37. package/dist/index-xsfqad54.js +0 -187
  38. package/dist/index-zc7rgvmm.js +0 -13
package/README.md CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  Model-driven CRUD rack for [Elysia](https://elysiajs.com): declare a resource once, get REST + `QUERY` routes, validation, authorization, query engine, OpenAPI metadata, and a React panel — running on [Bun](https://bun.com) or Node.js 20+.
6
6
 
7
+ Full usage guide: [docs/USAGE.md](docs/USAGE.md).
8
+
9
+ ## Documentation
10
+
11
+ End-user documentation covers installation, configuration, daily usage, and troubleshooting: [docs/index.md](docs/index.md).
12
+
7
13
  ## Install
8
14
 
9
15
  ```bash
@@ -85,7 +91,7 @@ work through plain `GET` navigation without JavaScript).
85
91
 
86
92
  The panel ships create / edit / delete UI driven by the same API:
87
93
 
88
- - `+ New` opens a create dialog (fields derived from row columns,
94
+ - `New record` opens a create dialog (fields derived from row columns,
89
95
  primary key optional). Submit sends `POST /` with an auto-generated
90
96
  `Idempotency-Key`. Tables without known columns fall back to a raw
91
97
  JSON body field.
@@ -147,7 +153,7 @@ Conventions:
147
153
  - String route ids are coerced to numbers for integer columns
148
154
  (e.g. `serial`); anything else passes through. Malformed ids resolve
149
155
  to `404`, missing rows to `404`.
150
- - `searchable` fields use SQL `LIKE` (case-sensitivity follows the DB).
156
+ - `searchable` fields use SQL `ILIKE` through the Drizzle adapter, so search requires a database that supports `ILIKE` (for example PostgreSQL). SQLite search fails with a SQL syntax error.
151
157
  - `settings.softDelete: true` writes a timestamp to `deletedAt`
152
158
  (override with `settings.deletedAtField`) instead of deleting, and
153
159
  reads skip soft-deleted rows.
@@ -155,7 +161,7 @@ Conventions:
155
161
 
156
162
  ## Dashboard
157
163
 
158
- `dashboard()` is now a simple *Coming Soon* placeholder. The real navigation lives in `panel` — each `rack()` panel shows a sidebar with the entire tree.
164
+ `dashboard()` renders a navigation overview of every registered resource, with a sidebar and a resource table. Each `rack()` panel also shows a sidebar with the entire tree.
159
165
 
160
166
  Recommended setup:
161
167
 
@@ -165,7 +171,7 @@ import { reactPlugin } from "elysia-rack/react";
165
171
 
166
172
  const app = new Elysia()
167
173
  .use(reactPlugin()) // pages are optional — defaults to dashboard + panel
168
- .use(dashboard({ title: "My Panel", path: "/" })) // → Coming Soon at /
174
+ .use(dashboard({ title: "My Panel", path: "/" })) // → Dashboard at /
169
175
  .use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
170
176
  .use(rack("/catalog/variants", { model, metadata: { id: "variants", group: "Catalog", order: 2, parent: "products" } }));
171
177
  // → Panel at /catalog/products now shows sidebar with Catalog > Products > Variants
@@ -227,14 +233,14 @@ rack("/catalog/products", {
227
233
  pluralLabel: "Products", // shown in sidebar
228
234
  group: "Catalog", // sidebar group
229
235
  order: 1, // smaller = higher
230
- icon: "📦",
236
+ icon: "package", // Lucide icon name (kebab-case or PascalCase)
231
237
  parent: "catalog-root", // nest under another id
232
238
  hidden: false
233
239
  }
234
240
  });
235
241
  ```
236
242
 
237
- **Decide what users can do.** Turn operations on/off. If you disable `create`, the “+ New” button disappears automatically.
243
+ **Decide what users can do.** Turn operations on/off. If you disable `create`, the “New record” button disappears automatically.
238
244
 
239
245
  ```ts
240
246
  rack("/catalog/products", {
@@ -293,9 +299,9 @@ rack("/catalog/products", {
293
299
 
294
300
  That’s the whole `rack()` surface. Start with `model`, then add `metadata` → `query` → `validation/authorization` as your app grows.
295
301
 
296
- ### 2) `dashboard()` — one line, now Coming Soon
302
+ ### 2) `dashboard()`: one line, navigation overview
297
303
 
298
- `dashboard()` mounts `GET /` that renders a *Coming Soon* page. The tree you registered with `rack()` is not shown here — it lives in `panel`.
304
+ `dashboard()` mounts `GET /` and renders a sidebar plus a table of every resource registered with `rack()`.
299
305
 
300
306
  ```ts
301
307
  import { dashboard, rack } from "elysia-rack";
@@ -303,7 +309,7 @@ import { reactPlugin } from "elysia-rack/react";
303
309
 
304
310
  const app = new Elysia()
305
311
  .use(reactPlugin()) // 1) UI engine — pages optional
306
- .use(dashboard({ title: "My Shop", path: "/" })) // 2) Coming Soon at /
312
+ .use(dashboard({ title: "My Shop", path: "/" })) // 2) Dashboard at /
307
313
  .use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
308
314
  .use(rack("/catalog/variants", { model, metadata: { id: "variants", parent: "products", group: "Catalog", order: 2 } }));
309
315
  // visit /catalog/products → panel shows sidebar with Catalog > Products > Variants
@@ -404,13 +410,13 @@ Fallback is `dist/panel.css`.
404
410
 
405
411
  Static assets are served under `/__rack/*`:
406
412
 
407
- - `dist/panel.css` — panel stylesheet (prebuilt)
413
+ - `dist/panel.css` — panel stylesheet (built with Tailwind CSS)
408
414
  - `dist/panel-theme.js` — dark/light toggle
409
415
  - `dist/panel-app.js` — live query + actions (imports `components/href.ts`, `components/panelForm.ts`)
410
416
 
411
417
  Pages reference `/__rack/panel.css`, `/__rack/panel-theme.js` and
412
- `/__rack/panel-app.js` via `reactPlugin`. No build step required — assets are
413
- static and included in the published package.
418
+ `/__rack/panel-app.js` via `reactPlugin`. Run `bun run build` to produce
419
+ `dist/` (`build:css` compiles the stylesheet, `build:js` bundles the clients).
414
420
 
415
421
  ## Panel theme
416
422
 
@@ -420,9 +426,10 @@ static and included in the published package.
420
426
  chart, selection, focus, overlay, navigation, table, form, sidebar),
421
427
  shadcn/Tailwind compatible, mapped via `@theme inline`.
422
428
  - `theme/dark.css` — `.dark` overrides.
423
- - `theme/koran.css` — default newspaper theme (warm paper, serif
424
- headlines). Remove its import for the neutral look.
425
- - `theme/components.css` — newspaper component classes + base styles.
429
+ - `theme/rack.css` — default theme (black, white, dark blue, gold;
430
+ Poppins for UI, Chocolate Classical Sans for display headings).
431
+ Remove its import for the neutral look.
432
+ - `theme/components.css` — base styles, dialog reset, reduced motion.
426
433
 
427
434
  Dark mode flips automatically: stored choice (`localStorage`), else
428
435
  `prefers-color-scheme`. The `◐ Theme` button in the masthead toggles it.
@@ -0,0 +1,212 @@
1
+ import {
2
+ Masthead,
3
+ RackIcon,
4
+ Sidebar
5
+ } from "./index-rg87f3v1.js";
6
+ import {
7
+ getRackTree
8
+ } from "./index-4wxvmd8a.js";
9
+ import {
10
+ resolveDonateProp
11
+ } from "./index-b8sq8rbd.js";
12
+ import"./index-37x76zdn.js";
13
+
14
+ // src/react/page/Dashboard.tsx
15
+ import { Fragment } from "react";
16
+ import { jsxDEV, Fragment as Fragment2 } from "react/jsx-dev-runtime";
17
+ function itemLabel(r) {
18
+ return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
19
+ }
20
+ function toSidebarItem(node, activeId) {
21
+ return {
22
+ id: node.metadata.id,
23
+ label: itemLabel(node),
24
+ icon: node.metadata.icon,
25
+ href: node.path,
26
+ active: node.metadata.id === activeId,
27
+ children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, activeId))
28
+ };
29
+ }
30
+ function buildGroupsFromTree(tree, activeId) {
31
+ const byGroup = new Map;
32
+ for (const root of tree) {
33
+ if (root.metadata.hidden)
34
+ continue;
35
+ const name = root.metadata.group ?? "General";
36
+ let group = byGroup.get(name);
37
+ if (!group) {
38
+ group = { name, items: [] };
39
+ byGroup.set(name, group);
40
+ }
41
+ group.items.push(toSidebarItem(root, activeId));
42
+ }
43
+ return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
44
+ }
45
+ function countNodes(nodes) {
46
+ return nodes.reduce((sum, n) => sum + 1 + countNodes(n.children), 0);
47
+ }
48
+ function operationsLabel(node) {
49
+ const op = node.operations;
50
+ const write = [op.create, op.update, op.replace, op.delete];
51
+ if (!write.some(Boolean))
52
+ return "Read only";
53
+ if (write.every(Boolean))
54
+ return "Full";
55
+ const labels = [];
56
+ if (op.create)
57
+ labels.push("Create");
58
+ if (op.update)
59
+ labels.push("Update");
60
+ if (op.replace)
61
+ labels.push("Replace");
62
+ if (op.delete)
63
+ labels.push("Delete");
64
+ return labels.join(", ");
65
+ }
66
+ function plural(count, word) {
67
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
68
+ }
69
+ function ResourceRow({ node, depth = 0 }) {
70
+ return /* @__PURE__ */ jsxDEV(Fragment2, {
71
+ children: [
72
+ /* @__PURE__ */ jsxDEV("tr", {
73
+ className: "hover:bg-table-row-hover",
74
+ children: [
75
+ /* @__PURE__ */ jsxDEV("td", {
76
+ className: "border-table-border border-t px-3 py-2.5",
77
+ children: /* @__PURE__ */ jsxDEV("a", {
78
+ href: node.path,
79
+ className: "text-link hover:text-link-hover font-medium hover:underline",
80
+ style: depth > 0 ? { paddingLeft: depth * 16 } : undefined,
81
+ children: [
82
+ node.metadata.icon ? /* @__PURE__ */ jsxDEV(RackIcon, {
83
+ name: node.metadata.icon,
84
+ size: 16,
85
+ className: "mr-1.5 inline size-4 align-text-bottom"
86
+ }, undefined, false, undefined, this) : null,
87
+ node.metadata.label ?? node.metadata.id
88
+ ]
89
+ }, undefined, true, undefined, this)
90
+ }, undefined, false, undefined, this),
91
+ /* @__PURE__ */ jsxDEV("td", {
92
+ className: "border-table-border text-text-muted border-t px-3 py-2.5 font-mono text-xs",
93
+ children: node.path
94
+ }, undefined, false, undefined, this),
95
+ /* @__PURE__ */ jsxDEV("td", {
96
+ className: "border-table-border text-text-muted border-t px-3 py-2.5 text-xs",
97
+ children: operationsLabel(node)
98
+ }, undefined, false, undefined, this)
99
+ ]
100
+ }, undefined, true, undefined, this),
101
+ node.children.filter((c) => !c.metadata.hidden).map((child) => /* @__PURE__ */ jsxDEV(ResourceRow, {
102
+ node: child,
103
+ depth: depth + 1
104
+ }, child.metadata.id, false, undefined, this))
105
+ ]
106
+ }, undefined, true, undefined, this);
107
+ }
108
+ async function Dashboard(props) {
109
+ const tree = getRackTree().filter((r) => !r.metadata.hidden);
110
+ const groups = buildGroupsFromTree(tree, props.resource);
111
+ const totalResources = countNodes(tree);
112
+ const totalGroups = groups.length;
113
+ const donate = resolveDonateProp(props.donate);
114
+ return /* @__PURE__ */ jsxDEV("div", {
115
+ className: "text-foreground flex min-h-screen flex-col lg:flex-row",
116
+ children: [
117
+ /* @__PURE__ */ jsxDEV(Sidebar, {
118
+ title: props.name ?? "Panel",
119
+ groups
120
+ }, undefined, false, undefined, this),
121
+ /* @__PURE__ */ jsxDEV("main", {
122
+ className: "min-w-0 flex-1",
123
+ children: /* @__PURE__ */ jsxDEV("div", {
124
+ className: "mx-auto w-full max-w-5xl px-4 py-4 sm:px-6",
125
+ children: [
126
+ /* @__PURE__ */ jsxDEV(Masthead, {
127
+ title: props.name ?? "Panel",
128
+ detail: `${plural(totalResources, "resource")} in ${plural(totalGroups, "group")}`
129
+ }, undefined, false, undefined, this),
130
+ /* @__PURE__ */ jsxDEV("div", {
131
+ className: "bg-table border-border overflow-x-auto rounded-lg border",
132
+ children: /* @__PURE__ */ jsxDEV("table", {
133
+ className: "w-full border-collapse text-left text-sm",
134
+ children: [
135
+ /* @__PURE__ */ jsxDEV("thead", {
136
+ children: /* @__PURE__ */ jsxDEV("tr", {
137
+ children: [
138
+ /* @__PURE__ */ jsxDEV("th", {
139
+ className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
140
+ children: "Resource"
141
+ }, undefined, false, undefined, this),
142
+ /* @__PURE__ */ jsxDEV("th", {
143
+ className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
144
+ children: "Path"
145
+ }, undefined, false, undefined, this),
146
+ /* @__PURE__ */ jsxDEV("th", {
147
+ className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
148
+ children: "Operations"
149
+ }, undefined, false, undefined, this)
150
+ ]
151
+ }, undefined, true, undefined, this)
152
+ }, undefined, false, undefined, this),
153
+ /* @__PURE__ */ jsxDEV("tbody", {
154
+ children: groups.length === 0 ? /* @__PURE__ */ jsxDEV("tr", {
155
+ children: /* @__PURE__ */ jsxDEV("td", {
156
+ colSpan: 3,
157
+ className: "text-text-muted px-4 py-10 text-center",
158
+ children: "No resources registered. Call rack() before rendering this page."
159
+ }, undefined, false, undefined, this)
160
+ }, undefined, false, undefined, this) : groups.map((group) => {
161
+ const nodes = tree.filter((r) => (r.metadata.group ?? "General") === group.name);
162
+ return /* @__PURE__ */ jsxDEV(Fragment, {
163
+ children: [
164
+ /* @__PURE__ */ jsxDEV("tr", {
165
+ className: "bg-surface-muted",
166
+ children: /* @__PURE__ */ jsxDEV("th", {
167
+ colSpan: 3,
168
+ className: "border-border text-text-muted border-b px-3 py-1.5 text-left text-xs font-medium",
169
+ children: [
170
+ group.name,
171
+ " ",
172
+ /* @__PURE__ */ jsxDEV("span", {
173
+ className: "text-text-subtle font-normal",
174
+ children: plural(countNodes(nodes), "resource")
175
+ }, undefined, false, undefined, this)
176
+ ]
177
+ }, undefined, true, undefined, this)
178
+ }, undefined, false, undefined, this),
179
+ nodes.map((node) => /* @__PURE__ */ jsxDEV(ResourceRow, {
180
+ node
181
+ }, node.metadata.id, false, undefined, this))
182
+ ]
183
+ }, group.name, true, undefined, this);
184
+ })
185
+ }, undefined, false, undefined, this)
186
+ ]
187
+ }, undefined, true, undefined, this)
188
+ }, undefined, false, undefined, this),
189
+ /* @__PURE__ */ jsxDEV("footer", {
190
+ className: "border-border text-text-muted mt-6 flex flex-wrap items-center justify-between gap-2 border-t pt-3 text-xs",
191
+ children: [
192
+ /* @__PURE__ */ jsxDEV("span", {
193
+ children: "elysia-rack"
194
+ }, undefined, false, undefined, this),
195
+ donate.enabled ? /* @__PURE__ */ jsxDEV("a", {
196
+ href: donate.url,
197
+ target: "_blank",
198
+ rel: "noopener noreferrer",
199
+ className: "hover:text-foreground underline-offset-4 hover:underline",
200
+ children: donate.label
201
+ }, undefined, false, undefined, this) : null
202
+ ]
203
+ }, undefined, true, undefined, this)
204
+ ]
205
+ }, undefined, true, undefined, this)
206
+ }, undefined, false, undefined, this)
207
+ ]
208
+ }, undefined, true, undefined, this);
209
+ }
210
+ export {
211
+ Dashboard as default
212
+ };
@@ -0,0 +1,210 @@
1
+ import {
2
+ Masthead,
3
+ Sidebar
4
+ } from "./index-0dwhzk3j.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/Dashboard.tsx
14
+ import { Fragment } from "react";
15
+ import { jsxDEV, Fragment as Fragment2 } from "react/jsx-dev-runtime";
16
+ function itemLabel(r) {
17
+ return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
18
+ }
19
+ function toSidebarItem(node, activeId) {
20
+ return {
21
+ id: node.metadata.id,
22
+ label: itemLabel(node),
23
+ icon: node.metadata.icon,
24
+ href: node.path,
25
+ active: node.metadata.id === activeId,
26
+ children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, activeId))
27
+ };
28
+ }
29
+ function buildGroupsFromTree(tree, activeId) {
30
+ const byGroup = new Map;
31
+ for (const root of tree) {
32
+ if (root.metadata.hidden)
33
+ continue;
34
+ const name = root.metadata.group ?? "General";
35
+ let group = byGroup.get(name);
36
+ if (!group) {
37
+ group = { name, items: [] };
38
+ byGroup.set(name, group);
39
+ }
40
+ group.items.push(toSidebarItem(root, activeId));
41
+ }
42
+ return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
43
+ }
44
+ function countNodes(nodes) {
45
+ return nodes.reduce((sum, n) => sum + 1 + countNodes(n.children), 0);
46
+ }
47
+ function operationsLabel(node) {
48
+ const op = node.operations;
49
+ const write = [op.create, op.update, op.replace, op.delete];
50
+ if (!write.some(Boolean))
51
+ return "Read only";
52
+ if (write.every(Boolean))
53
+ return "Full";
54
+ const labels = [];
55
+ if (op.create)
56
+ labels.push("Create");
57
+ if (op.update)
58
+ labels.push("Update");
59
+ if (op.replace)
60
+ labels.push("Replace");
61
+ if (op.delete)
62
+ labels.push("Delete");
63
+ return labels.join(", ");
64
+ }
65
+ function plural(count, word) {
66
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
67
+ }
68
+ function ResourceRow({ node, depth = 0 }) {
69
+ return /* @__PURE__ */ jsxDEV(Fragment2, {
70
+ children: [
71
+ /* @__PURE__ */ jsxDEV("tr", {
72
+ className: "hover:bg-table-row-hover",
73
+ children: [
74
+ /* @__PURE__ */ jsxDEV("td", {
75
+ className: "border-table-border border-t px-3 py-2.5",
76
+ children: /* @__PURE__ */ jsxDEV("a", {
77
+ href: node.path,
78
+ className: "text-link hover:text-link-hover font-medium hover:underline",
79
+ style: depth > 0 ? { paddingLeft: depth * 16 } : undefined,
80
+ children: [
81
+ node.metadata.icon ? /* @__PURE__ */ jsxDEV("span", {
82
+ "aria-hidden": "true",
83
+ className: "mr-1.5",
84
+ children: node.metadata.icon
85
+ }, undefined, false, undefined, this) : null,
86
+ node.metadata.label ?? node.metadata.id
87
+ ]
88
+ }, undefined, true, undefined, this)
89
+ }, undefined, false, undefined, this),
90
+ /* @__PURE__ */ jsxDEV("td", {
91
+ className: "border-table-border text-text-muted border-t px-3 py-2.5 font-mono text-xs",
92
+ children: node.path
93
+ }, undefined, false, undefined, this),
94
+ /* @__PURE__ */ jsxDEV("td", {
95
+ className: "border-table-border text-text-muted border-t px-3 py-2.5 text-xs",
96
+ children: operationsLabel(node)
97
+ }, undefined, false, undefined, this)
98
+ ]
99
+ }, undefined, true, undefined, this),
100
+ node.children.filter((c) => !c.metadata.hidden).map((child) => /* @__PURE__ */ jsxDEV(ResourceRow, {
101
+ node: child,
102
+ depth: depth + 1
103
+ }, child.metadata.id, false, undefined, this))
104
+ ]
105
+ }, undefined, true, undefined, this);
106
+ }
107
+ async function Dashboard(props) {
108
+ const tree = getRackTree().filter((r) => !r.metadata.hidden);
109
+ const groups = buildGroupsFromTree(tree, props.resource);
110
+ const totalResources = countNodes(tree);
111
+ const totalGroups = groups.length;
112
+ const donate = resolveDonateProp(props.donate);
113
+ return /* @__PURE__ */ jsxDEV("div", {
114
+ className: "text-foreground flex min-h-screen flex-col lg:flex-row",
115
+ children: [
116
+ /* @__PURE__ */ jsxDEV(Sidebar, {
117
+ title: props.name ?? "Panel",
118
+ groups
119
+ }, undefined, false, undefined, this),
120
+ /* @__PURE__ */ jsxDEV("main", {
121
+ className: "min-w-0 flex-1",
122
+ children: /* @__PURE__ */ jsxDEV("div", {
123
+ className: "mx-auto w-full max-w-5xl px-4 py-4 sm:px-6",
124
+ children: [
125
+ /* @__PURE__ */ jsxDEV(Masthead, {
126
+ title: props.name ?? "Panel",
127
+ detail: `${plural(totalResources, "resource")} in ${plural(totalGroups, "group")}`
128
+ }, undefined, false, undefined, this),
129
+ /* @__PURE__ */ jsxDEV("div", {
130
+ className: "bg-table border-border overflow-x-auto rounded-lg border",
131
+ children: /* @__PURE__ */ jsxDEV("table", {
132
+ className: "w-full border-collapse text-left text-sm",
133
+ children: [
134
+ /* @__PURE__ */ jsxDEV("thead", {
135
+ children: /* @__PURE__ */ jsxDEV("tr", {
136
+ children: [
137
+ /* @__PURE__ */ jsxDEV("th", {
138
+ className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
139
+ children: "Resource"
140
+ }, undefined, false, undefined, this),
141
+ /* @__PURE__ */ jsxDEV("th", {
142
+ className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
143
+ children: "Path"
144
+ }, undefined, false, undefined, this),
145
+ /* @__PURE__ */ jsxDEV("th", {
146
+ className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
147
+ children: "Operations"
148
+ }, undefined, false, undefined, this)
149
+ ]
150
+ }, undefined, true, undefined, this)
151
+ }, undefined, false, undefined, this),
152
+ /* @__PURE__ */ jsxDEV("tbody", {
153
+ children: groups.length === 0 ? /* @__PURE__ */ jsxDEV("tr", {
154
+ children: /* @__PURE__ */ jsxDEV("td", {
155
+ colSpan: 3,
156
+ className: "text-text-muted px-4 py-10 text-center",
157
+ children: "No resources registered. Call rack() before rendering this page."
158
+ }, undefined, false, undefined, this)
159
+ }, undefined, false, undefined, this) : groups.map((group) => {
160
+ const nodes = tree.filter((r) => (r.metadata.group ?? "General") === group.name);
161
+ return /* @__PURE__ */ jsxDEV(Fragment, {
162
+ children: [
163
+ /* @__PURE__ */ jsxDEV("tr", {
164
+ className: "bg-surface-muted",
165
+ children: /* @__PURE__ */ jsxDEV("th", {
166
+ colSpan: 3,
167
+ className: "border-border text-text-muted border-b px-3 py-1.5 text-left text-xs font-medium",
168
+ children: [
169
+ group.name,
170
+ /* @__PURE__ */ jsxDEV("span", {
171
+ className: "text-text-subtle ml-2 font-normal",
172
+ children: plural(countNodes(nodes), "resource")
173
+ }, undefined, false, undefined, this)
174
+ ]
175
+ }, undefined, true, undefined, this)
176
+ }, undefined, false, undefined, this),
177
+ nodes.map((node) => /* @__PURE__ */ jsxDEV(ResourceRow, {
178
+ node
179
+ }, node.metadata.id, false, undefined, this))
180
+ ]
181
+ }, group.name, true, undefined, this);
182
+ })
183
+ }, undefined, false, undefined, this)
184
+ ]
185
+ }, undefined, true, undefined, this)
186
+ }, undefined, false, undefined, this),
187
+ /* @__PURE__ */ jsxDEV("footer", {
188
+ className: "border-border text-text-muted mt-6 flex flex-wrap items-center justify-between gap-2 border-t pt-3 text-xs",
189
+ children: [
190
+ /* @__PURE__ */ jsxDEV("span", {
191
+ children: "elysia-rack"
192
+ }, undefined, false, undefined, this),
193
+ donate.enabled ? /* @__PURE__ */ jsxDEV("a", {
194
+ href: donate.url,
195
+ target: "_blank",
196
+ rel: "noopener noreferrer",
197
+ className: "hover:text-foreground underline-offset-4 hover:underline",
198
+ children: donate.label
199
+ }, undefined, false, undefined, this) : null
200
+ ]
201
+ }, undefined, true, undefined, this)
202
+ ]
203
+ }, undefined, true, undefined, this)
204
+ }, undefined, false, undefined, this)
205
+ ]
206
+ }, undefined, true, undefined, this);
207
+ }
208
+ export {
209
+ Dashboard as default
210
+ };