elysia-rack 0.1.1 → 0.1.2

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 CHANGED
@@ -151,14 +151,36 @@ Conventions:
151
151
  reads skip soft-deleted rows.
152
152
  - `settings.returning: false` omits `data` from mutation responses.
153
153
 
154
- ## Dashboard (sidebar, breadcrumb, iframe)
154
+ ## Dashboard
155
155
 
156
- Every `rack()` call registers its path, metadata and operations in a
157
- registry (`listRacks()` / `clearRacks()` from `elysia-rack`). The default
158
- `/dashboard` page builds a sidebar (grouped by `metadata.group`, ordered
159
- by `metadata.order`, skipping `hidden`), a breadcrumb
160
- (Dashboard / Group / Label), and shows the selected resource's `GET`
161
- panel inside an `<iframe>`:
156
+ Recommended setup uses `react()`, `dashboard()`, and `rack()`. Each `rack()`
157
+ stores its metadata tree in memory (registry) and `dashboard()` loads it at
158
+ render time via `getRackTree()`.
159
+
160
+ ```ts
161
+ import { dashboard, rack } from "elysia-rack";
162
+ import { pages, reactPlugin } from "elysia-rack/react";
163
+
164
+ const app = new Elysia()
165
+ .use(reactPlugin({ pages }))
166
+ .use(dashboard({ title: "My Panel", path: "/" }))
167
+ .use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
168
+ .use(rack("/catalog/variants", { model, metadata: { id: "variants", group: "Catalog", order: 2, parent: "products" } }));
169
+ ```
170
+
171
+ ### Tree metadata
172
+
173
+ `metadata.parent` builds a hierarchy. Children are rendered nested under their
174
+ parent in the sidebar and kept ordered by `order` then label.
175
+
176
+ ```ts
177
+ rack("/catalog/products", { metadata: { id: "products", group: "Catalog", order: 1 } });
178
+ rack("/catalog/variants", { metadata: { id: "variants", parent: "products", group: "Catalog", order: 2 } });
179
+ ```
180
+
181
+ Helpers: `listRacks()`, `getRack(id)`, `getRackTree()`, `flatRackTree()`, `buildRackTree(racks)`, `clearRacks()` from `elysia-rack`.
182
+
183
+ The dashboard page itself can also be used manually:
162
184
 
163
185
  ```ts
164
186
  import { page, pages, reactPlugin } from "elysia-rack/react";
@@ -174,32 +196,60 @@ const app = new Elysia()
174
196
  );
175
197
  ```
176
198
 
177
- Sidebar links navigate with `?resource=<id>` (full reload, no JavaScript
178
- needed). Without a selection the first registered resource is shown.
199
+ The sidebar groups by `metadata.group`, sorts by `order`, skips `hidden`,
200
+ and shows the selected resource's panel inside an `<iframe>`. Links use
201
+ `?resource=<id>` (no JavaScript required).
202
+
203
+ ## HTML template
204
+
205
+ SSR uses `src/react/app.html` as the shell. It is read at runtime, cached,
206
+ and streamed with the React output.
207
+
208
+ ```html
209
+ <!doctype html>
210
+ <html><head>
211
+ <title>{{title}}</title>
212
+ <link rel="stylesheet" href="/__rack/panel.css" />
213
+ <!--app-head-->
214
+ </head><body>
215
+ <!--app-->
216
+ </body></html>
217
+ ```
218
+
219
+ - `{{title}}` is replaced (escaped) per page.
220
+ - `<!--app-->` (also `{{body}}`, `<!--app-html-->`) marks where the React stream is injected.
221
+ - Fallback shell is used if the file is missing. `dist/app.html` is a static copy of `src/react/app.html` included in the package.
179
222
 
180
- React panel rendering via the `./react` subpath:
223
+ Override the template by editing `src/react/app.html` (and `dist/app.html` for the published package).
224
+
225
+ ## Panel CSS override
226
+
227
+ Override `panel.css` at `react()` config without forking:
181
228
 
182
229
  ```ts
183
- import { page, pages, reactPlugin } from "elysia-rack/react";
230
+ import { pages, reactPlugin } from "elysia-rack/react";
184
231
 
185
- const app = new Elysia()
186
- .use(reactPlugin({ pages }))
187
- .get("/", () => page("/dashboard", { name: "Ada" }));
232
+ // file path (resolved from cwd/dist), raw string, or object
233
+ reactPlugin({ pages, css: "./my-panel.css" });
234
+ reactPlugin({ pages, css: { path: "./my-panel.css" } });
235
+ reactPlugin({ pages, css: { content: ":root{--color-primary:red}" } });
236
+ reactPlugin({ pages, panelCss: "body{...}" }); // alias
188
237
  ```
189
238
 
190
- ## Panel assets (Vite)
239
+ If a file path exists it is served; otherwise a raw CSS string is served.
240
+ Fallback is `dist/panel.css`.
241
+
242
+ ## Panel assets
191
243
 
192
- CSS, Tailwind and browser JavaScript go through Vite (`vite.config.ts`,
193
- `bun run build:assets`):
244
+ Static assets are served under `/__rack/*`:
194
245
 
195
- - `src/react/page/panel.css` — stylesheet entry (Tailwind v4) → `dist/panel.css`
196
- - `src/react/page/client/theme.ts` — dark/light toggle → `dist/panel-theme.js`
197
- - `src/react/page/client/panel.ts` — live query + actions → `dist/panel-app.js`
198
- (imports shared `components/href.ts` and `components/panelForm.ts`)
246
+ - `dist/panel.css` — panel stylesheet (prebuilt)
247
+ - `dist/panel-theme.js` — dark/light toggle
248
+ - `dist/panel-app.js` — live query + actions (imports `components/href.ts`, `components/panelForm.ts`)
199
249
 
200
250
  Pages reference `/__rack/panel.css`, `/__rack/panel-theme.js` and
201
- `/__rack/panel-app.js`, served by `reactPlugin` from `dist/` (run the
202
- build before `dev`; CI and `prepublishOnly` already chain it).
251
+ `/__rack/panel-app.js` via `reactPlugin`. No build step required — assets are
252
+ static and included in the published package.
203
253
 
204
254
  ## Panel theme
205
255
 
@@ -224,7 +274,7 @@ touching React code.
224
274
  1. One-time setup: npmjs.com → package → Settings → Trusted Publisher → GitHub Actions (`notoofly/elysia-rack`, workflow `publish.yml`).
225
275
  2. Bump `version` in `package.json`.
226
276
  3. Create a GitHub Release with tag `v<version>` (e.g. `v0.1.0`).
227
- 4. The `publish` workflow runs automatically: install, typecheck, asset build, test, build, tag-vs-version check, `npm publish --provenance`.
277
+ 4. The `publish` workflow runs automatically: install, typecheck, test, build, tag-vs-version check, `npm publish --provenance`.
228
278
 
229
279
  ## Scripts
230
280
 
@@ -0,0 +1,225 @@
1
+ import {
2
+ buildRackTree,
3
+ flatRackTree,
4
+ getRackTree
5
+ } from "./index-4wxvmd8a.js";
6
+ import"./index-37x76zdn.js";
7
+
8
+ // src/react/page/components/Breadcrumb.tsx
9
+ import { jsxDEV } from "react/jsx-dev-runtime";
10
+ function Breadcrumb({ trail }) {
11
+ return /* @__PURE__ */ jsxDEV("nav", {
12
+ "aria-label": "Breadcrumb",
13
+ className: "pb-3 text-sm",
14
+ children: /* @__PURE__ */ jsxDEV("ol", {
15
+ className: "flex flex-wrap items-center gap-1.5",
16
+ children: trail.map((item, i) => {
17
+ const last = i === trail.length - 1;
18
+ return /* @__PURE__ */ jsxDEV("li", {
19
+ className: "flex items-center gap-1.5",
20
+ children: [
21
+ i > 0 ? /* @__PURE__ */ jsxDEV("span", {
22
+ "aria-hidden": "true",
23
+ className: "text-text-muted",
24
+ children: "/"
25
+ }, undefined, false, undefined, this) : null,
26
+ item.href && !last ? /* @__PURE__ */ jsxDEV("a", {
27
+ href: item.href,
28
+ className: "text-text-muted hover:text-foreground",
29
+ children: item.label
30
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("span", {
31
+ "aria-current": last ? "page" : undefined,
32
+ className: last ? "text-foreground font-semibold" : "text-text-muted",
33
+ children: item.label
34
+ }, undefined, false, undefined, this)
35
+ ]
36
+ }, `${item.label}-${i}`, true, undefined, this);
37
+ })
38
+ }, undefined, false, undefined, this)
39
+ }, undefined, false, undefined, this);
40
+ }
41
+
42
+ // src/react/page/components/Sidebar.tsx
43
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
44
+ function SidebarItemView({ item, depth = 0 }) {
45
+ const hasChildren = item.children && item.children.length > 0;
46
+ return /* @__PURE__ */ jsxDEV2("li", {
47
+ children: [
48
+ /* @__PURE__ */ jsxDEV2("a", {
49
+ href: item.href,
50
+ "aria-current": item.active ? "page" : undefined,
51
+ className: item.active ? "bg-sidebar-primary text-sidebar-primary-foreground flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-semibold" : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
52
+ style: depth > 0 ? { paddingLeft: `${8 + depth * 12}px` } : undefined,
53
+ children: [
54
+ item.icon ? /* @__PURE__ */ jsxDEV2("span", {
55
+ "aria-hidden": "true",
56
+ children: item.icon
57
+ }, undefined, false, undefined, this) : null,
58
+ /* @__PURE__ */ jsxDEV2("span", {
59
+ children: item.label
60
+ }, undefined, false, undefined, this)
61
+ ]
62
+ }, undefined, true, undefined, this),
63
+ hasChildren ? /* @__PURE__ */ jsxDEV2("ul", {
64
+ className: "border-sidebar-border mt-0.5 ml-2 flex flex-col gap-0.5 border-l py-1 pl-2",
65
+ children: item.children.map((child) => /* @__PURE__ */ jsxDEV2(SidebarItemView, {
66
+ item: child,
67
+ depth: depth + 1
68
+ }, child.id, false, undefined, this))
69
+ }, undefined, false, undefined, this) : null
70
+ ]
71
+ }, undefined, true, undefined, this);
72
+ }
73
+ function hasActive(items) {
74
+ for (const it of items) {
75
+ if (it.active)
76
+ return true;
77
+ if (it.children && hasActive(it.children))
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+ function Sidebar({ title, groups }) {
83
+ return /* @__PURE__ */ jsxDEV2("aside", {
84
+ className: "bg-sidebar text-sidebar-foreground border-sidebar-border min-h-0 w-64 shrink-0 overflow-y-auto rounded-lg border",
85
+ children: [
86
+ /* @__PURE__ */ jsxDEV2("div", {
87
+ className: "border-sidebar-border border-b px-4 py-3",
88
+ children: /* @__PURE__ */ jsxDEV2("p", {
89
+ className: "font-koran text-xl font-black tracking-tight",
90
+ children: title
91
+ }, undefined, false, undefined, this)
92
+ }, undefined, false, undefined, this),
93
+ /* @__PURE__ */ jsxDEV2("nav", {
94
+ className: "flex flex-col gap-4 p-3",
95
+ children: groups.map((group) => /* @__PURE__ */ jsxDEV2("section", {
96
+ children: /* @__PURE__ */ jsxDEV2("details", {
97
+ className: "group",
98
+ open: hasActive(group.items) || undefined,
99
+ children: [
100
+ /* @__PURE__ */ jsxDEV2("summary", {
101
+ className: "font-koran border-sidebar-border flex cursor-pointer list-none items-center justify-between border-b px-2 pb-1 text-base font-bold tracking-tight [&::-webkit-details-marker]:hidden",
102
+ children: [
103
+ /* @__PURE__ */ jsxDEV2("span", {
104
+ children: group.name
105
+ }, undefined, false, undefined, this),
106
+ /* @__PURE__ */ jsxDEV2("span", {
107
+ "aria-hidden": "true",
108
+ className: "text-text-muted text-xs transition group-open:rotate-180",
109
+ children: "▾"
110
+ }, undefined, false, undefined, this)
111
+ ]
112
+ }, undefined, true, undefined, this),
113
+ /* @__PURE__ */ jsxDEV2("ul", {
114
+ className: "border-sidebar-border mt-1 ml-2 flex flex-col gap-0.5 border-l py-1 pl-2",
115
+ children: group.items.map((item) => /* @__PURE__ */ jsxDEV2(SidebarItemView, {
116
+ item
117
+ }, item.id, false, undefined, this))
118
+ }, undefined, false, undefined, this)
119
+ ]
120
+ }, undefined, true, undefined, this)
121
+ }, group.name, false, undefined, this))
122
+ }, undefined, false, undefined, this)
123
+ ]
124
+ }, undefined, true, undefined, this);
125
+ }
126
+
127
+ // src/react/page/Dashboard.tsx
128
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
129
+ function itemLabel(r) {
130
+ return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
131
+ }
132
+ function buildGroups(racks, selectedId) {
133
+ const byGroup = new Map;
134
+ for (const r of racks) {
135
+ if (r.metadata.hidden)
136
+ continue;
137
+ const name = r.metadata.group ?? "General";
138
+ let group = byGroup.get(name);
139
+ if (!group) {
140
+ group = { name, items: [] };
141
+ byGroup.set(name, group);
142
+ }
143
+ group.items.push({
144
+ id: r.metadata.id,
145
+ label: itemLabel(r),
146
+ icon: r.metadata.icon,
147
+ href: `?resource=${encodeURIComponent(r.metadata.id)}`,
148
+ active: r.metadata.id === selectedId
149
+ });
150
+ }
151
+ const groups = [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
152
+ for (const group of groups) {
153
+ const orderOf = (id) => racks.find((r) => r.metadata.id === id)?.metadata.order ?? 0;
154
+ group.items.sort((a, b) => orderOf(a.id) - orderOf(b.id) || a.label.localeCompare(b.label));
155
+ }
156
+ return groups;
157
+ }
158
+ function toSidebarItem(node, selectedId) {
159
+ return {
160
+ id: node.metadata.id,
161
+ label: itemLabel(node),
162
+ icon: node.metadata.icon,
163
+ href: `?resource=${encodeURIComponent(node.metadata.id)}`,
164
+ active: node.metadata.id === selectedId,
165
+ children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, selectedId))
166
+ };
167
+ }
168
+ function buildGroupsFromTree(tree, selectedId) {
169
+ const byGroup = new Map;
170
+ for (const root of tree) {
171
+ if (root.metadata.hidden)
172
+ continue;
173
+ const name = root.metadata.group ?? "General";
174
+ let group = byGroup.get(name);
175
+ if (!group) {
176
+ group = { name, items: [] };
177
+ byGroup.set(name, group);
178
+ }
179
+ group.items.push(toSidebarItem(root, selectedId));
180
+ }
181
+ return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
182
+ }
183
+ async function Dashboard(props) {
184
+ const tree = props.racks !== undefined ? buildRackTree(props.racks.filter((r) => !r.metadata.hidden)) : getRackTree().filter((r) => !r.metadata.hidden);
185
+ const flat = props.racks !== undefined ? props.racks.filter((r) => !r.metadata.hidden) : flatRackTree(tree).filter((r) => !r.metadata.hidden);
186
+ const selected = flat.find((r) => r.metadata.id === props.resource) ?? flat[0];
187
+ const groups = tree.length ? buildGroupsFromTree(tree, selected?.metadata.id) : buildGroups(flat, selected?.metadata.id);
188
+ const trail = [
189
+ { label: "Dashboard", href: "?" },
190
+ ...selected?.metadata.group ? [{ label: selected.metadata.group }] : [],
191
+ ...selected ? [{ label: itemLabel(selected) }] : []
192
+ ];
193
+ return /* @__PURE__ */ jsxDEV3("div", {
194
+ className: "koran-paper font-koran-body text-foreground flex flex-col min-h-screen",
195
+ children: /* @__PURE__ */ jsxDEV3("div", {
196
+ className: "flex min-h-0 flex-1 gap-6 p-4 sm:p-6 h-full",
197
+ children: [
198
+ /* @__PURE__ */ jsxDEV3(Sidebar, {
199
+ title: props.name ?? "Panel",
200
+ groups
201
+ }, undefined, false, undefined, this),
202
+ /* @__PURE__ */ jsxDEV3("main", {
203
+ className: "flex min-w-0 flex-1 flex-col",
204
+ children: [
205
+ /* @__PURE__ */ jsxDEV3(Breadcrumb, {
206
+ trail
207
+ }, undefined, false, undefined, this),
208
+ selected ? /* @__PURE__ */ jsxDEV3("iframe", {
209
+ src: selected.path,
210
+ title: itemLabel(selected),
211
+ sandbox: "allow-scripts allow-same-origin allow-forms",
212
+ className: "bg-card min-h-0 w-full flex-1 rounded-lg"
213
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3("p", {
214
+ className: "text-text-muted border-border rounded-lg border px-4 py-10 text-center",
215
+ children: "No resources registered. Mount a resource with rack() first."
216
+ }, undefined, false, undefined, this)
217
+ ]
218
+ }, undefined, true, undefined, this)
219
+ ]
220
+ }, undefined, true, undefined, this)
221
+ }, undefined, false, undefined, this);
222
+ }
223
+ export {
224
+ Dashboard as default
225
+ };
@@ -0,0 +1,252 @@
1
+ import {
2
+ flatRackTree,
3
+ getRackTree
4
+ } from "./index-tgk4jt4d.js";
5
+ import"./index-37x76zdn.js";
6
+
7
+ // src/react/page/components/Breadcrumb.tsx
8
+ import { jsxDEV } from "react/jsx-dev-runtime";
9
+ function Breadcrumb({ trail }) {
10
+ return /* @__PURE__ */ jsxDEV("nav", {
11
+ "aria-label": "Breadcrumb",
12
+ className: "pb-3 text-sm",
13
+ children: /* @__PURE__ */ jsxDEV("ol", {
14
+ className: "flex flex-wrap items-center gap-1.5",
15
+ children: trail.map((item, i) => {
16
+ const last = i === trail.length - 1;
17
+ return /* @__PURE__ */ jsxDEV("li", {
18
+ className: "flex items-center gap-1.5",
19
+ children: [
20
+ i > 0 ? /* @__PURE__ */ jsxDEV("span", {
21
+ "aria-hidden": "true",
22
+ className: "text-text-muted",
23
+ children: "/"
24
+ }, undefined, false, undefined, this) : null,
25
+ item.href && !last ? /* @__PURE__ */ jsxDEV("a", {
26
+ href: item.href,
27
+ className: "text-text-muted hover:text-foreground",
28
+ children: item.label
29
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("span", {
30
+ "aria-current": last ? "page" : undefined,
31
+ className: last ? "text-foreground font-semibold" : "text-text-muted",
32
+ children: item.label
33
+ }, undefined, false, undefined, this)
34
+ ]
35
+ }, `${item.label}-${i}`, true, undefined, this);
36
+ })
37
+ }, undefined, false, undefined, this)
38
+ }, undefined, false, undefined, this);
39
+ }
40
+
41
+ // src/react/page/components/Sidebar.tsx
42
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
43
+ function SidebarItemView({ item, depth = 0 }) {
44
+ const hasChildren = item.children && item.children.length > 0;
45
+ return /* @__PURE__ */ jsxDEV2("li", {
46
+ children: [
47
+ /* @__PURE__ */ jsxDEV2("a", {
48
+ href: item.href,
49
+ "aria-current": item.active ? "page" : undefined,
50
+ className: item.active ? "bg-sidebar-primary text-sidebar-primary-foreground flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-semibold" : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
51
+ style: depth > 0 ? { paddingLeft: `${8 + depth * 12}px` } : undefined,
52
+ children: [
53
+ item.icon ? /* @__PURE__ */ jsxDEV2("span", {
54
+ "aria-hidden": "true",
55
+ children: item.icon
56
+ }, undefined, false, undefined, this) : null,
57
+ /* @__PURE__ */ jsxDEV2("span", {
58
+ children: item.label
59
+ }, undefined, false, undefined, this)
60
+ ]
61
+ }, undefined, true, undefined, this),
62
+ hasChildren ? /* @__PURE__ */ jsxDEV2("ul", {
63
+ className: "border-sidebar-border mt-0.5 ml-2 flex flex-col gap-0.5 border-l py-1 pl-2",
64
+ children: item.children.map((child) => /* @__PURE__ */ jsxDEV2(SidebarItemView, {
65
+ item: child,
66
+ depth: depth + 1
67
+ }, child.id, false, undefined, this))
68
+ }, undefined, false, undefined, this) : null
69
+ ]
70
+ }, undefined, true, undefined, this);
71
+ }
72
+ function hasActive(items) {
73
+ for (const it of items) {
74
+ if (it.active)
75
+ return true;
76
+ if (it.children && hasActive(it.children))
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+ function Sidebar({ title, groups }) {
82
+ return /* @__PURE__ */ jsxDEV2("aside", {
83
+ className: "bg-sidebar text-sidebar-foreground border-sidebar-border min-h-0 w-64 shrink-0 overflow-y-auto rounded-lg border",
84
+ children: [
85
+ /* @__PURE__ */ jsxDEV2("div", {
86
+ className: "border-sidebar-border border-b px-4 py-3",
87
+ children: /* @__PURE__ */ jsxDEV2("p", {
88
+ className: "font-koran text-xl font-black tracking-tight",
89
+ children: title
90
+ }, undefined, false, undefined, this)
91
+ }, undefined, false, undefined, this),
92
+ /* @__PURE__ */ jsxDEV2("nav", {
93
+ className: "flex flex-col gap-4 p-3",
94
+ children: groups.map((group) => /* @__PURE__ */ jsxDEV2("section", {
95
+ children: /* @__PURE__ */ jsxDEV2("details", {
96
+ className: "group",
97
+ open: hasActive(group.items) || undefined,
98
+ children: [
99
+ /* @__PURE__ */ jsxDEV2("summary", {
100
+ className: "font-koran border-sidebar-border flex cursor-pointer list-none items-center justify-between border-b px-2 pb-1 text-base font-bold tracking-tight [&::-webkit-details-marker]:hidden",
101
+ children: [
102
+ /* @__PURE__ */ jsxDEV2("span", {
103
+ children: group.name
104
+ }, undefined, false, undefined, this),
105
+ /* @__PURE__ */ jsxDEV2("span", {
106
+ "aria-hidden": "true",
107
+ className: "text-text-muted text-xs transition group-open:rotate-180",
108
+ children: "▾"
109
+ }, undefined, false, undefined, this)
110
+ ]
111
+ }, undefined, true, undefined, this),
112
+ /* @__PURE__ */ jsxDEV2("ul", {
113
+ className: "border-sidebar-border mt-1 ml-2 flex flex-col gap-0.5 border-l py-1 pl-2",
114
+ children: group.items.map((item) => /* @__PURE__ */ jsxDEV2(SidebarItemView, {
115
+ item
116
+ }, item.id, false, undefined, this))
117
+ }, undefined, false, undefined, this)
118
+ ]
119
+ }, undefined, true, undefined, this)
120
+ }, group.name, false, undefined, this))
121
+ }, undefined, false, undefined, this)
122
+ ]
123
+ }, undefined, true, undefined, this);
124
+ }
125
+
126
+ // src/react/page/Dashboard.tsx
127
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
128
+ function itemLabel(r) {
129
+ return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
130
+ }
131
+ function buildGroups(racks, selectedId) {
132
+ const byGroup = new Map;
133
+ for (const r of racks) {
134
+ if (r.metadata.hidden)
135
+ continue;
136
+ const name = r.metadata.group ?? "General";
137
+ let group = byGroup.get(name);
138
+ if (!group) {
139
+ group = { name, items: [] };
140
+ byGroup.set(name, group);
141
+ }
142
+ group.items.push({
143
+ id: r.metadata.id,
144
+ label: itemLabel(r),
145
+ icon: r.metadata.icon,
146
+ href: `?resource=${encodeURIComponent(r.metadata.id)}`,
147
+ active: r.metadata.id === selectedId
148
+ });
149
+ }
150
+ const groups = [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
151
+ for (const group of groups) {
152
+ const orderOf = (id) => racks.find((r) => r.metadata.id === id)?.metadata.order ?? 0;
153
+ group.items.sort((a, b) => orderOf(a.id) - orderOf(b.id) || a.label.localeCompare(b.label));
154
+ }
155
+ return groups;
156
+ }
157
+ function toSidebarItem(node, selectedId) {
158
+ return {
159
+ id: node.metadata.id,
160
+ label: itemLabel(node),
161
+ icon: node.metadata.icon,
162
+ href: `?resource=${encodeURIComponent(node.metadata.id)}`,
163
+ active: node.metadata.id === selectedId,
164
+ children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, selectedId))
165
+ };
166
+ }
167
+ function buildGroupsFromTree(tree, selectedId) {
168
+ const byGroup = new Map;
169
+ for (const root of tree) {
170
+ if (root.metadata.hidden)
171
+ continue;
172
+ const name = root.metadata.group ?? "General";
173
+ let group = byGroup.get(name);
174
+ if (!group) {
175
+ group = { name, items: [] };
176
+ byGroup.set(name, group);
177
+ }
178
+ group.items.push(toSidebarItem(root, selectedId));
179
+ }
180
+ return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
181
+ }
182
+ function treeFromRacks(racks) {
183
+ const nodes = new Map;
184
+ for (const r of racks)
185
+ nodes.set(r.metadata.id, { ...r, children: [] });
186
+ const roots = [];
187
+ for (const node of nodes.values()) {
188
+ const parentId = node.metadata.parent;
189
+ if (parentId && nodes.has(parentId) && parentId !== node.metadata.id) {
190
+ nodes.get(parentId).children.push(node);
191
+ } else {
192
+ roots.push(node);
193
+ }
194
+ }
195
+ const sort = (list) => {
196
+ list.sort((a, b) => {
197
+ const oa = a.metadata.order ?? 0;
198
+ const ob = b.metadata.order ?? 0;
199
+ if (oa !== ob)
200
+ return oa - ob;
201
+ return itemLabel(a).localeCompare(itemLabel(b));
202
+ });
203
+ for (const n of list)
204
+ if (n.children.length)
205
+ sort(n.children);
206
+ };
207
+ sort(roots);
208
+ return roots;
209
+ }
210
+ async function Dashboard(props) {
211
+ const tree = props.racks !== undefined ? treeFromRacks(props.racks.filter((r) => !r.metadata.hidden)) : getRackTree().filter((r) => !r.metadata.hidden);
212
+ const flat = props.racks !== undefined ? props.racks.filter((r) => !r.metadata.hidden) : flatRackTree(tree).filter((r) => !r.metadata.hidden);
213
+ const selected = flat.find((r) => r.metadata.id === props.resource) ?? flat[0];
214
+ const groups = tree.length ? buildGroupsFromTree(tree, selected?.metadata.id) : buildGroups(flat, selected?.metadata.id);
215
+ const trail = [
216
+ { label: "Dashboard", href: "?" },
217
+ ...selected?.metadata.group ? [{ label: selected.metadata.group }] : [],
218
+ ...selected ? [{ label: itemLabel(selected) }] : []
219
+ ];
220
+ return /* @__PURE__ */ jsxDEV3("div", {
221
+ className: "koran-paper font-koran-body text-foreground flex flex-col min-h-screen",
222
+ children: /* @__PURE__ */ jsxDEV3("div", {
223
+ className: "flex min-h-0 flex-1 gap-6 p-4 sm:p-6 h-full",
224
+ children: [
225
+ /* @__PURE__ */ jsxDEV3(Sidebar, {
226
+ title: props.name ?? "Panel",
227
+ groups
228
+ }, undefined, false, undefined, this),
229
+ /* @__PURE__ */ jsxDEV3("main", {
230
+ className: "flex min-w-0 flex-1 flex-col",
231
+ children: [
232
+ /* @__PURE__ */ jsxDEV3(Breadcrumb, {
233
+ trail
234
+ }, undefined, false, undefined, this),
235
+ selected ? /* @__PURE__ */ jsxDEV3("iframe", {
236
+ src: selected.path,
237
+ title: itemLabel(selected),
238
+ sandbox: "allow-scripts allow-same-origin allow-forms",
239
+ className: "bg-card min-h-0 w-full flex-1 rounded-lg"
240
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3("p", {
241
+ className: "text-text-muted border-border rounded-lg border px-4 py-10 text-center",
242
+ children: "No resources registered. Mount a resource with rack() first."
243
+ }, undefined, false, undefined, this)
244
+ ]
245
+ }, undefined, true, undefined, this)
246
+ ]
247
+ }, undefined, true, undefined, this)
248
+ }, undefined, false, undefined, this);
249
+ }
250
+ export {
251
+ Dashboard as default
252
+ };
package/dist/app.html ADDED
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{title}}</title>
7
+ <link rel="stylesheet" href="/__rack/panel.css" />
8
+ <script type="module" src="/__rack/panel-theme.js"></script>
9
+ <!--app-head-->
10
+ </head>
11
+ <body>
12
+ <!--app-->
13
+ </body>
14
+ </html>