elysia-rack 0.2.2 → 0.4.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 (41) hide show
  1. package/README.md +37 -20
  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/index.js +5 -5
  16. package/dist/rack/types/metadata.d.ts +2 -1
  17. package/dist/react/donate.d.ts +1 -1
  18. package/dist/react/index.js +40 -15
  19. package/dist/react/page/Panel.d.ts +1 -0
  20. package/dist/react/page/components/Icon.d.ts +10 -0
  21. package/dist/react/page/components/Masthead.d.ts +5 -4
  22. package/dist/react/render.d.ts +1 -1
  23. package/dist/react/types.d.ts +18 -2
  24. package/package.json +8 -3
  25. package/dist/Dashboard-0da8dvbv.js +0 -225
  26. package/dist/Dashboard-2j561p7c.js +0 -225
  27. package/dist/Dashboard-5mt9hnbw.js +0 -252
  28. package/dist/Dashboard-7gmx8c9y.js +0 -248
  29. package/dist/Dashboard-s8nfyf7s.js +0 -39
  30. package/dist/Dashboard-xkh2g9f7.js +0 -248
  31. package/dist/Dashboard-xpstr7fr.js +0 -172
  32. package/dist/Panel-3yrk3krj.js +0 -914
  33. package/dist/Panel-b7jhgvnm.js +0 -916
  34. package/dist/Panel-n0kstkyw.js +0 -735
  35. package/dist/Panel-pkvqf5v8.js +0 -912
  36. package/dist/Panel-wjxjtbmr.js +0 -753
  37. package/dist/Panel-za4av616.js +0 -753
  38. package/dist/index-h3zbq796.js +0 -39
  39. package/dist/index-tgk4jt4d.js +0 -63
  40. package/dist/index-xsfqad54.js +0 -187
  41. package/dist/index-zc7rgvmm.js +0 -13
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  setGlobalDonate
3
- } from "../index-6v0ns1xn.js";
3
+ } from "../index-b8sq8rbd.js";
4
4
  import {
5
5
  __require
6
6
  } from "../index-37x76zdn.js";
@@ -18,6 +18,24 @@ import { readFile } from "node:fs/promises";
18
18
  import { join } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { renderToReadableStream } from "react-dom/server";
21
+
22
+ // src/react/types.ts
23
+ var ReactRack;
24
+ ((ReactRack) => {
25
+ ReactRack.PAGE = Symbol.for("@elysia-panel/react");
26
+ const lazyPages = new WeakSet;
27
+ function lazyPage(loader) {
28
+ lazyPages.add(loader);
29
+ return loader;
30
+ }
31
+ ReactRack.lazyPage = lazyPage;
32
+ function isLazyPage(entry) {
33
+ return typeof entry === "function" && lazyPages.has(entry);
34
+ }
35
+ ReactRack.isLazyPage = isLazyPage;
36
+ })(ReactRack ||= {});
37
+
38
+ // src/react/render.tsx
21
39
  import { jsxDEV } from "react/jsx-dev-runtime";
22
40
  function pageTitle(path, props) {
23
41
  const metadata = props?.metadata;
@@ -67,16 +85,18 @@ function buildShell(template, title) {
67
85
  return { head: html.slice(0, bodyClose), tail: html.slice(bodyClose) };
68
86
  return { head: html, tail: "" };
69
87
  }
88
+ function isLazyPage(entry) {
89
+ return ReactRack.isLazyPage(entry);
90
+ }
70
91
  function createRenderer(pages) {
71
92
  return async function render(path, props) {
72
- const loader = pages[path];
73
- if (!loader) {
93
+ const entry = pages[path];
94
+ if (!entry) {
74
95
  return new Response("Page Not Found", {
75
96
  status: 404
76
97
  });
77
98
  }
78
- const module = await loader();
79
- const Component = module.default;
99
+ const Component = isLazyPage(entry) ? (await entry()).default : entry;
80
100
  const stream = await renderToReadableStream(/* @__PURE__ */ jsxDEV(Component, {
81
101
  ...props
82
102
  }, undefined, false, undefined, this));
@@ -109,12 +129,6 @@ function createRenderer(pages) {
109
129
  };
110
130
  }
111
131
 
112
- // src/react/types.ts
113
- var ReactRack;
114
- ((ReactRack) => {
115
- ReactRack.PAGE = Symbol.for("@elysia-panel/react");
116
- })(ReactRack ||= {});
117
-
118
132
  // src/react/index.ts
119
133
  var PANEL_ASSETS = {
120
134
  "panel.css": "text/css; charset=utf-8",
@@ -155,8 +169,8 @@ function page(path, props) {
155
169
  };
156
170
  }
157
171
  var pages = {
158
- "/dashboard": () => import("../Dashboard-7gmx8c9y.js"),
159
- "/panel": () => import("../Panel-wjxjtbmr.js")
172
+ "/dashboard": ReactRack.lazyPage(() => import("../Dashboard-4wv82gwx.js")),
173
+ "/panel": ReactRack.lazyPage(() => import("../Panel-9mmh3tbh.js"))
160
174
  };
161
175
  var defaultPages = pages;
162
176
  function mergeRegistries(base, extra) {
@@ -167,7 +181,14 @@ function mergeRegistries(base, extra) {
167
181
  }
168
182
  function reactPlugin(options = {}) {
169
183
  setGlobalDonate(options.donate);
170
- const registry = mergeRegistries(defaultPages, options.pages);
184
+ const views = options.views ?? [];
185
+ const viewPages = {};
186
+ for (const view of views)
187
+ viewPages[view.path] = view.component;
188
+ const registry = mergeRegistries(defaultPages, [
189
+ ...options.pages ? Array.isArray(options.pages) ? options.pages : [options.pages] : [],
190
+ viewPages
191
+ ]);
171
192
  const render = createRenderer(registry);
172
193
  const panelCssOverride = pickPanelCssOption(options);
173
194
  let cachedCustomCss = null;
@@ -214,7 +235,7 @@ function reactPlugin(options = {}) {
214
235
  cachedCustomCssLoaded = true;
215
236
  return cachedCustomCss = null;
216
237
  }
217
- return new Elysia({ name: "@elysia-rack/react" }).decorate("react", { render }).get("/__rack/:name", async ({ params, status }) => {
238
+ let app = new Elysia({ name: "@elysia-rack/react" }).decorate("react", { render }).get("/__rack/:name", async ({ params, status }) => {
218
239
  const type = PANEL_ASSETS[params.name];
219
240
  if (!type)
220
241
  return status(404, "Panel asset not found");
@@ -250,6 +271,10 @@ function reactPlugin(options = {}) {
250
271
  const descriptor = responseValue;
251
272
  return render(descriptor.path, descriptor.props);
252
273
  });
274
+ for (const view of views) {
275
+ app = app.get(view.path, () => render(view.path, view.props));
276
+ }
277
+ return app;
253
278
  }
254
279
  export {
255
280
  reactPlugin,
@@ -6,6 +6,7 @@ export interface PanelProps {
6
6
  label?: string;
7
7
  pluralLabel?: string;
8
8
  group?: string;
9
+ icon?: string;
9
10
  };
10
11
  operations?: {
11
12
  list?: boolean;
@@ -0,0 +1,10 @@
1
+ import { type LucideProps } from "lucide-react";
2
+ export interface RackIconProps extends Omit<LucideProps, "name"> {
3
+ /** Lucide icon name (kebab-case or PascalCase). Falls back to the raw value. */
4
+ name?: string;
5
+ }
6
+ /**
7
+ * Render a Lucide icon from metadata.
8
+ * Unknown names fall back to the raw string so existing emoji icons keep working.
9
+ */
10
+ export declare function RackIcon({ name, ...props }: RackIconProps): import("react").JSX.Element | null;
@@ -1,7 +1,8 @@
1
+ import type { ReactNode } from "react";
1
2
  export interface MastheadProps {
2
3
  title: string;
3
- group?: string;
4
- resource?: string;
5
- total?: number;
4
+ icon?: string;
5
+ detail?: ReactNode;
6
+ actions?: ReactNode;
6
7
  }
7
- export declare function Masthead({ title, group, resource, total }: MastheadProps): import("react").JSX.Element;
8
+ export declare function Masthead({ title, icon, detail, actions }: MastheadProps): import("react").JSX.Element;
@@ -1,2 +1,2 @@
1
- import type { ReactRack } from "./types";
1
+ import { ReactRack } from "./types";
2
2
  export declare function createRenderer(pages: ReactRack.PageRegistry): (path: string, props: unknown) => Promise<Response>;
@@ -7,9 +7,23 @@ export declare namespace ReactRack {
7
7
  props?: Props;
8
8
  }
9
9
  type PageComponent<Props = any> = ComponentType<Props>;
10
- type PageRegistry = Record<string, () => Promise<{
10
+ type PageLoader = () => Promise<{
11
11
  default: PageComponent;
12
- }>>;
12
+ }>;
13
+ /** A page is either a component or a loader wrapped with `lazyPage()`. */
14
+ type PageEntry = PageComponent | PageLoader;
15
+ type PageRegistry = Record<string, PageEntry>;
16
+ /** Mark a `() => import(...)` loader so it is not mistaken for a component. */
17
+ function lazyPage(loader: PageLoader): PageLoader;
18
+ /** @internal */
19
+ function isLazyPage(entry: PageEntry): entry is PageLoader;
20
+ /** A page registered directly with its URL — no `page()` call needed. */
21
+ interface ReactView<Props = any> {
22
+ /** URL path the route is mounted at. */
23
+ path: string;
24
+ component: PageEntry;
25
+ props?: Props;
26
+ }
13
27
  /** Override source for panel.css */
14
28
  type PanelCssOverride = string | {
15
29
  path?: string;
@@ -17,6 +31,8 @@ export declare namespace ReactRack {
17
31
  };
18
32
  interface ReactPluginOptions {
19
33
  pages?: PageRegistry | PageRegistry[];
34
+ /** Pages auto-mounted at their `path`; `page()` is not needed for these. */
35
+ views?: ReactView[];
20
36
  /**
21
37
  * Override `panel.css` served at `/__rack/panel.css`.
22
38
  * - `string`: file path if the file exists, otherwise raw CSS string.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elysia-rack",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
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",
@@ -54,8 +54,9 @@
54
54
  "dev": "bun run src/playground.ts",
55
55
  "test": "bun test --coverage",
56
56
  "typecheck": "tsc --noEmit",
57
- "build": "bun run build:js && bun run build:dts && bun run build:client",
58
- "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",
57
+ "build": "bun run build:css && bun run build:js && bun run build:dts && bun run build:client",
58
+ "build:css": "tailwindcss -i src/react/page/panel.css -o dist/panel.css --minify",
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",
59
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",
60
61
  "build:dts": "tsc -p tsconfig.build.json",
61
62
  "prepublishOnly": "bun run typecheck && bun run build"
@@ -73,6 +74,7 @@
73
74
  },
74
75
  "devDependencies": {
75
76
  "@electric-sql/pglite": "^0.5.8",
77
+ "@tailwindcss/cli": "4.3.3",
76
78
  "@types/bun": "latest",
77
79
  "@types/react": "^19.2.18",
78
80
  "@types/react-dom": "^19.2.5",
@@ -85,5 +87,8 @@
85
87
  "funding": {
86
88
  "type": "ko-fi",
87
89
  "url": "https://ko-fi.com/notoofly_manu"
90
+ },
91
+ "dependencies": {
92
+ "lucide-react": "^1.72.0"
88
93
  }
89
94
  }
@@ -1,225 +0,0 @@
1
- import {
2
- Breadcrumb,
3
- Masthead,
4
- Sidebar
5
- } from "./index-xsfqad54.js";
6
- import {
7
- getRackTree
8
- } from "./index-4wxvmd8a.js";
9
- import"./index-37x76zdn.js";
10
-
11
- // src/react/page/Dashboard.tsx
12
- import { jsxDEV } from "react/jsx-dev-runtime";
13
- function itemLabel(r) {
14
- return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
15
- }
16
- function toSidebarItem(node, activeId) {
17
- return {
18
- id: node.metadata.id,
19
- label: itemLabel(node),
20
- icon: node.metadata.icon,
21
- href: node.path,
22
- active: node.metadata.id === activeId,
23
- children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, activeId))
24
- };
25
- }
26
- function buildGroupsFromTree(tree, activeId) {
27
- const byGroup = new Map;
28
- for (const root of tree) {
29
- if (root.metadata.hidden)
30
- continue;
31
- const name = root.metadata.group ?? "General";
32
- let group = byGroup.get(name);
33
- if (!group) {
34
- group = { name, items: [] };
35
- byGroup.set(name, group);
36
- }
37
- group.items.push(toSidebarItem(root, activeId));
38
- }
39
- return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
40
- }
41
- async function Dashboard(props) {
42
- const tree = getRackTree().filter((r) => !r.metadata.hidden);
43
- const groups = buildGroupsFromTree(tree, props.resource);
44
- const totalResources = tree.length + tree.reduce((a, n) => a + n.children.length, 0);
45
- const totalGroups = groups.length;
46
- return /* @__PURE__ */ jsxDEV("div", {
47
- className: "koran-paper font-koran-body text-foreground flex min-h-screen flex-col",
48
- children: /* @__PURE__ */ jsxDEV("div", {
49
- className: "flex flex-1 gap-6 p-4 sm:p-6",
50
- children: [
51
- /* @__PURE__ */ jsxDEV(Sidebar, {
52
- title: props.name ?? "Panel",
53
- groups
54
- }, undefined, false, undefined, this),
55
- /* @__PURE__ */ jsxDEV("main", {
56
- className: "flex min-w-0 flex-1 flex-col",
57
- children: [
58
- /* @__PURE__ */ jsxDEV(Breadcrumb, {
59
- trail: [{ label: "Dashboard" }]
60
- }, undefined, false, undefined, this),
61
- /* @__PURE__ */ jsxDEV("div", {
62
- className: "mx-auto w-full max-w-5xl px-4 pb-10 sm:px-6",
63
- children: [
64
- /* @__PURE__ */ jsxDEV(Masthead, {
65
- title: props.name ?? "ERP Demo",
66
- group: "Dashboard",
67
- resource: "dashboard",
68
- total: totalResources
69
- }, undefined, false, undefined, this),
70
- /* @__PURE__ */ jsxDEV("div", {
71
- className: "grid grid-cols-2 gap-3 pb-6 lg:grid-cols-4",
72
- children: [
73
- /* @__PURE__ */ jsxDEV("div", {
74
- className: "bg-card border-border rounded-lg border px-4 py-3",
75
- children: [
76
- /* @__PURE__ */ jsxDEV("p", {
77
- className: "text-text-muted text-xs font-semibold tracking-widest uppercase",
78
- children: "Resources"
79
- }, undefined, false, undefined, this),
80
- /* @__PURE__ */ jsxDEV("p", {
81
- className: "font-koran text-2xl font-black",
82
- children: totalResources
83
- }, undefined, false, undefined, this),
84
- /* @__PURE__ */ jsxDEV("p", {
85
- className: "text-text-muted text-xs",
86
- children: [
87
- totalGroups,
88
- " groups"
89
- ]
90
- }, undefined, true, undefined, this)
91
- ]
92
- }, undefined, true, undefined, this),
93
- /* @__PURE__ */ jsxDEV("div", {
94
- className: "bg-card border-border rounded-lg border px-4 py-3",
95
- children: [
96
- /* @__PURE__ */ jsxDEV("p", {
97
- className: "text-text-muted text-xs font-semibold tracking-widest uppercase",
98
- children: "Modules"
99
- }, undefined, false, undefined, this),
100
- /* @__PURE__ */ jsxDEV("p", {
101
- className: "font-koran text-2xl font-black",
102
- children: totalGroups
103
- }, undefined, false, undefined, this),
104
- /* @__PURE__ */ jsxDEV("p", {
105
- className: "text-text-muted text-xs",
106
- children: "Core → Governance"
107
- }, undefined, false, undefined, this)
108
- ]
109
- }, undefined, true, undefined, this),
110
- /* @__PURE__ */ jsxDEV("div", {
111
- className: "bg-card border-border rounded-lg border px-4 py-3",
112
- children: [
113
- /* @__PURE__ */ jsxDEV("p", {
114
- className: "text-text-muted text-xs font-semibold tracking-widest uppercase",
115
- children: "Status"
116
- }, undefined, false, undefined, this),
117
- /* @__PURE__ */ jsxDEV("p", {
118
- className: "font-koran text-2xl font-black",
119
- children: "Ready"
120
- }, undefined, false, undefined, this),
121
- /* @__PURE__ */ jsxDEV("p", {
122
- className: "text-text-muted text-xs",
123
- children: "PGlite in-memory"
124
- }, undefined, false, undefined, this)
125
- ]
126
- }, undefined, true, undefined, this),
127
- /* @__PURE__ */ jsxDEV("div", {
128
- className: "bg-card border-border rounded-lg border px-4 py-3",
129
- children: [
130
- /* @__PURE__ */ jsxDEV("p", {
131
- className: "text-text-muted text-xs font-semibold tracking-widest uppercase",
132
- children: "Tree"
133
- }, undefined, false, undefined, this),
134
- /* @__PURE__ */ jsxDEV("p", {
135
- className: "font-koran text-2xl font-black",
136
- children: tree.length
137
- }, undefined, false, undefined, this),
138
- /* @__PURE__ */ jsxDEV("p", {
139
- className: "text-text-muted text-xs",
140
- children: "root nodes"
141
- }, undefined, false, undefined, this)
142
- ]
143
- }, undefined, true, undefined, this)
144
- ]
145
- }, undefined, true, undefined, this),
146
- /* @__PURE__ */ jsxDEV("div", {
147
- className: "border-border bg-card rounded-lg border",
148
- children: [
149
- /* @__PURE__ */ jsxDEV("div", {
150
- className: "border-border flex items-center justify-between border-b px-4 py-3",
151
- children: [
152
- /* @__PURE__ */ jsxDEV("h2", {
153
- className: "text-sm font-semibold tracking-widest uppercase",
154
- children: "Resource Tree"
155
- }, undefined, false, undefined, this),
156
- /* @__PURE__ */ jsxDEV("span", {
157
- className: "text-text-muted text-xs",
158
- children: [
159
- totalResources,
160
- " resources"
161
- ]
162
- }, undefined, true, undefined, this)
163
- ]
164
- }, undefined, true, undefined, this),
165
- /* @__PURE__ */ jsxDEV("div", {
166
- className: "divide-border divide-y",
167
- children: [
168
- groups.map((g) => /* @__PURE__ */ jsxDEV("div", {
169
- className: "px-4 py-3",
170
- children: [
171
- /* @__PURE__ */ jsxDEV("p", {
172
- className: "text-xs font-semibold tracking-widest uppercase text-form-label",
173
- children: g.name
174
- }, undefined, false, undefined, this),
175
- /* @__PURE__ */ jsxDEV("ul", {
176
- className: "mt-2 flex flex-wrap gap-2",
177
- children: g.items.map((it) => /* @__PURE__ */ jsxDEV("li", {
178
- children: [
179
- /* @__PURE__ */ jsxDEV("a", {
180
- href: it.href,
181
- className: "bg-secondary text-secondary-foreground inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium hover:underline",
182
- children: [
183
- /* @__PURE__ */ jsxDEV("span", {
184
- children: it.icon ?? "•"
185
- }, undefined, false, undefined, this),
186
- " ",
187
- it.label
188
- ]
189
- }, undefined, true, undefined, this),
190
- (it.children?.length ?? 0) > 0 && /* @__PURE__ */ jsxDEV("span", {
191
- className: "text-text-muted ml-1 text-xs",
192
- children: [
193
- "+",
194
- it.children?.length ?? 0,
195
- " child"
196
- ]
197
- }, undefined, true, undefined, this)
198
- ]
199
- }, it.id, true, undefined, this))
200
- }, undefined, false, undefined, this)
201
- ]
202
- }, g.name, true, undefined, this)),
203
- groups.length === 0 && /* @__PURE__ */ jsxDEV("p", {
204
- className: "text-text-muted px-4 py-6 text-center text-sm",
205
- children: "Belum ada resource terdaftar — pastikan rack() sudah dipanggil sebelum dashboard render."
206
- }, undefined, false, undefined, this)
207
- ]
208
- }, undefined, true, undefined, this)
209
- ]
210
- }, undefined, true, undefined, this),
211
- /* @__PURE__ */ jsxDEV("footer", {
212
- className: "border-border text-text-muted mt-6 border-t pt-3 text-center text-xs tracking-widest uppercase",
213
- children: "Printed by elysia-rack — ERP blueprint"
214
- }, undefined, false, undefined, this)
215
- ]
216
- }, undefined, true, 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
- };
@@ -1,225 +0,0 @@
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
- };