elysia-rack 0.1.2 → 0.2.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 +198 -27
- package/dist/Dashboard-0da8dvbv.js +225 -0
- package/dist/Dashboard-7gmx8c9y.js +248 -0
- package/dist/Dashboard-s8nfyf7s.js +39 -0
- package/dist/Dashboard-xkh2g9f7.js +248 -0
- package/dist/Panel-3yrk3krj.js +914 -0
- package/dist/Panel-b7jhgvnm.js +916 -0
- package/dist/Panel-n0kstkyw.js +735 -0
- package/dist/Panel-pkvqf5v8.js +912 -0
- package/dist/Panel-wjxjtbmr.js +753 -0
- package/dist/Panel-za4av616.js +753 -0
- package/dist/index-6v0ns1xn.js +19 -0
- package/dist/index-h3zbq796.js +39 -0
- package/dist/index-xsfqad54.js +187 -0
- package/dist/panel-app.js +498 -1
- package/dist/panel-theme.js +38 -1
- package/dist/rack/dashboard.d.ts +3 -0
- package/dist/rack/index.js +14 -1
- package/dist/react/donate.d.ts +10 -0
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +16 -4
- package/dist/react/page/Dashboard.d.ts +1 -2
- package/dist/react/page/Panel.d.ts +1 -0
- package/dist/react/types.d.ts +6 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# elysia-rack
|
|
2
2
|
|
|
3
|
+
[](https://ko-fi.com/notoofly_manu) [](https://www.npmjs.com/package/elysia-rack)
|
|
4
|
+
|
|
3
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+.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
@@ -153,25 +155,25 @@ Conventions:
|
|
|
153
155
|
|
|
154
156
|
## Dashboard
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|
|
159
|
+
|
|
160
|
+
Recommended setup:
|
|
159
161
|
|
|
160
162
|
```ts
|
|
161
163
|
import { dashboard, rack } from "elysia-rack";
|
|
162
|
-
import {
|
|
164
|
+
import { reactPlugin } from "elysia-rack/react";
|
|
163
165
|
|
|
164
166
|
const app = new Elysia()
|
|
165
|
-
.use(reactPlugin(
|
|
166
|
-
.use(dashboard({ title: "My Panel", path: "/" }))
|
|
167
|
+
.use(reactPlugin()) // pages are optional — defaults to dashboard + panel
|
|
168
|
+
.use(dashboard({ title: "My Panel", path: "/" })) // → Coming Soon at /
|
|
167
169
|
.use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
|
|
168
170
|
.use(rack("/catalog/variants", { model, metadata: { id: "variants", group: "Catalog", order: 2, parent: "products" } }));
|
|
171
|
+
// → Panel at /catalog/products now shows sidebar with Catalog > Products > Variants
|
|
169
172
|
```
|
|
170
173
|
|
|
171
174
|
### Tree metadata
|
|
172
175
|
|
|
173
|
-
`metadata.parent` builds a hierarchy. Children are rendered nested under their
|
|
174
|
-
parent in the sidebar and kept ordered by `order` then label.
|
|
176
|
+
`metadata.parent` builds a hierarchy. Children are rendered nested under their parent in `panel`’s sidebar and kept ordered by `order` then label.
|
|
175
177
|
|
|
176
178
|
```ts
|
|
177
179
|
rack("/catalog/products", { metadata: { id: "products", group: "Catalog", order: 1 } });
|
|
@@ -180,25 +182,182 @@ rack("/catalog/variants", { metadata: { id: "variants", parent: "products", grou
|
|
|
180
182
|
|
|
181
183
|
Helpers: `listRacks()`, `getRack(id)`, `getRackTree()`, `flatRackTree()`, `buildRackTree(racks)`, `clearRacks()` from `elysia-rack`.
|
|
182
184
|
|
|
183
|
-
|
|
185
|
+
### Donate — disable on website
|
|
186
|
+
Donate link (Ko-fi) tampil di Dashboard & Panel footer. Disable via `dashboard({ donate: false })` atau global `reactPlugin({ donate: false })` (default `true`).
|
|
187
|
+
|
|
188
|
+
You can still render the dashboard manually:
|
|
184
189
|
|
|
185
190
|
```ts
|
|
186
|
-
import { page,
|
|
191
|
+
import { page, reactPlugin } from "elysia-rack/react";
|
|
187
192
|
|
|
188
193
|
const app = new Elysia()
|
|
189
|
-
.use(reactPlugin(
|
|
194
|
+
.use(reactPlugin())
|
|
190
195
|
.use(rack("/catalog/products", { model, metadata: { ... } }))
|
|
191
|
-
.get("/", ({
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
.get("/", () => page("/dashboard", { name: "My Shop" }));
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`panel` now handles the sidebar + breadcrumb. Each panel loads the full tree via `getRackTree()`, highlights the active resource (`props.resource`), and links via `href: node.path`. No iframe needed.
|
|
200
|
+
|
|
201
|
+
## Tutorial — make it yours
|
|
202
|
+
|
|
203
|
+
This guide walks you through `rack()`, `dashboard()` and `react()` the way you would build a real app. Copy, run, tweak.
|
|
204
|
+
|
|
205
|
+
### 1) `rack()` — describe a resource, get an API + panel
|
|
206
|
+
|
|
207
|
+
Start with the only required thing: which table/model to use.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { rack } from "elysia-rack";
|
|
211
|
+
|
|
212
|
+
rack("/catalog/products", {
|
|
213
|
+
model: { drizzle: { db, table: products } } // or { prisma: { client: prisma, model: prisma.product } }
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
You already get `GET /`, `QUERY /data`, `POST /`, `PUT/PATCH/DELETE /:id` and a panel. Now make it yours step by step.
|
|
218
|
+
|
|
219
|
+
**Give it a name and a place in the menu.** `metadata` is what the dashboard reads from memory.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
rack("/catalog/products", {
|
|
223
|
+
model: { drizzle: { db, table: products } },
|
|
224
|
+
metadata: {
|
|
225
|
+
id: "products", // unique id, defaults to path
|
|
226
|
+
label: "Product", // singular
|
|
227
|
+
pluralLabel: "Products", // shown in sidebar
|
|
228
|
+
group: "Catalog", // sidebar group
|
|
229
|
+
order: 1, // smaller = higher
|
|
230
|
+
icon: "📦",
|
|
231
|
+
parent: "catalog-root", // nest under another id
|
|
232
|
+
hidden: false
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Decide what users can do.** Turn operations on/off. If you disable `create`, the “+ New” button disappears automatically.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
rack("/catalog/products", {
|
|
241
|
+
model,
|
|
242
|
+
operations: { list: true, detail: true, create: true, replace: true, update: true, delete: false }
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Shape the list experience.** Which fields can be searched, filtered, sorted? What’s the default?
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
rack("/catalog/products", {
|
|
250
|
+
model,
|
|
251
|
+
query: {
|
|
252
|
+
searchable: ["name", "sku"],
|
|
253
|
+
filterable: ["status"],
|
|
254
|
+
sortable: ["name", "price", "createdAt"],
|
|
255
|
+
defaultSort: { field: "createdAt", direction: "desc" },
|
|
256
|
+
pagination: { default: 20, max: 100 }
|
|
257
|
+
}
|
|
258
|
+
});
|
|
197
259
|
```
|
|
198
260
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
261
|
+
**Add safety.** Validation and authorization are per-operation. A string like `"products.delete"` checks the `x-permissions` header; a function gives you the full `Request`.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { t } from "elysia";
|
|
265
|
+
|
|
266
|
+
rack("/catalog/products", {
|
|
267
|
+
model,
|
|
268
|
+
validation: {
|
|
269
|
+
create: t.Object({ name: t.String({ minLength: 3 }) }),
|
|
270
|
+
update: t.Partial(t.Object({ name: t.String() })),
|
|
271
|
+
params: t.Object({ id: t.String() }),
|
|
272
|
+
query: t.Object({ search: t.Optional(t.String()) })
|
|
273
|
+
},
|
|
274
|
+
authorization: {
|
|
275
|
+
list: true,
|
|
276
|
+
delete: async ({ request }) => request.headers.get("x-api-key") === "secret"
|
|
277
|
+
// or: delete: "products.delete"
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Tweak the small things** when you need them:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
rack("/catalog/products", {
|
|
286
|
+
model,
|
|
287
|
+
page: { enabled: true, path: "/panel" }, // change or disable GET / panel
|
|
288
|
+
settings: { primaryKey: "slug", softDelete: true, deletedAtField: "deletedAt", returning: true },
|
|
289
|
+
openapi: { tags: ["Catalog"], description: "Products", operations: { list: { summary: "List products" } } },
|
|
290
|
+
idempotency: { required: true, header: "Idempotency-Key", ttl: 3600, store: myRedisStore }
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
That’s the whole `rack()` surface. Start with `model`, then add `metadata` → `query` → `validation/authorization` as your app grows.
|
|
295
|
+
|
|
296
|
+
### 2) `dashboard()` — one line, now Coming Soon
|
|
297
|
+
|
|
298
|
+
`dashboard()` mounts `GET /` that renders a *Coming Soon* page. The tree you registered with `rack()` is not shown here — it lives in `panel`.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { dashboard, rack } from "elysia-rack";
|
|
302
|
+
import { reactPlugin } from "elysia-rack/react";
|
|
303
|
+
|
|
304
|
+
const app = new Elysia()
|
|
305
|
+
.use(reactPlugin()) // 1) UI engine — pages optional
|
|
306
|
+
.use(dashboard({ title: "My Shop", path: "/" })) // 2) Coming Soon at /
|
|
307
|
+
.use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
|
|
308
|
+
.use(rack("/catalog/variants", { model, metadata: { id: "variants", parent: "products", group: "Catalog", order: 2 } }));
|
|
309
|
+
// visit /catalog/products → panel shows sidebar with Catalog > Products > Variants
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Options — all optional:
|
|
313
|
+
|
|
314
|
+
- `path` (default `"/"`) — where the dashboard lives
|
|
315
|
+
- `title` (default `"Panel"`) — header in the sidebar
|
|
316
|
+
- `pagePath` (default `"/dashboard"`) — which React page to render
|
|
317
|
+
|
|
318
|
+
Need it without the helper? The helper is just sugar for:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
import { page } from "elysia-rack/react";
|
|
322
|
+
.get("/", ({ query }) => page("/dashboard", { name: "My Shop", resource: query.resource as string }))
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Helpers to inspect the tree yourself: `listRacks()`, `getRack(id)`, `getRackTree()`, `flatRackTree()`, `buildRackTree(racks)`, `clearRacks()`.
|
|
326
|
+
|
|
327
|
+
### 3) `react()` — pages and look
|
|
328
|
+
|
|
329
|
+
`reactPlugin` is the bridge between Elysia and React. `pages` is now optional and mergeable — defaults already include `"/dashboard"` and `"/panel"`, and any custom pages you pass are merged in. You can pass a single registry or an array.
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import { pages, reactPlugin, page } from "elysia-rack/react";
|
|
333
|
+
|
|
334
|
+
// Option A: just use defaults
|
|
335
|
+
new Elysia().use(reactPlugin());
|
|
336
|
+
|
|
337
|
+
// Option B: extend defaults with your own page
|
|
338
|
+
const myPages = { "/hello": () => import("./Hello") };
|
|
339
|
+
new Elysia().use(reactPlugin({ pages: myPages })); // merged with defaults
|
|
340
|
+
|
|
341
|
+
// Option C: merge multiple registries — array will be merged
|
|
342
|
+
const extra = { "/admin": () => import("./Admin") };
|
|
343
|
+
new Elysia().use(reactPlugin({ pages: [pages, myPages, extra] }));
|
|
344
|
+
|
|
345
|
+
// With a style override
|
|
346
|
+
new Elysia().use(reactPlugin({
|
|
347
|
+
pages: myPages, // or [pages, myPages]
|
|
348
|
+
css: "./my-panel.css", // file path
|
|
349
|
+
// css: { path: "./my-panel.css" },
|
|
350
|
+
// css: { content: ":root{--color-primary:red}" },
|
|
351
|
+
// panelCss: "body{...}" // alias
|
|
352
|
+
})).get("/hello", () => page("/hello", { name: "Ada" }));
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
What each option does:
|
|
356
|
+
|
|
357
|
+
- `pages` *(optional, `PageRegistry | PageRegistry[]`)* — registry of lazy page components. If omitted, defaults are used. If you pass one or an array, they are merged with defaults so `"/dashboard"` + `"/panel"` stay available. Later entries win on key collision.
|
|
358
|
+
- `css` / `panelCss` — replace `dist/panel.css` served at `/__rack/panel.css`. If the string points to an existing file it’s used, otherwise it’s treated as raw CSS. Useful for quick theming without forking.
|
|
359
|
+
|
|
360
|
+
Under the hood `reactPlugin` also serves `/__rack/panel-theme.js` and `/__rack/panel-app.js` and turns any `page()` return value into streamed HTML using `src/react/app.html` (`{{title}}` + `<!--app-->`).
|
|
202
361
|
|
|
203
362
|
## HTML template
|
|
204
363
|
|
|
@@ -227,13 +386,15 @@ Override the template by editing `src/react/app.html` (and `dist/app.html` for t
|
|
|
227
386
|
Override `panel.css` at `react()` config without forking:
|
|
228
387
|
|
|
229
388
|
```ts
|
|
230
|
-
import {
|
|
231
|
-
|
|
232
|
-
// file path (resolved from cwd/dist), raw string, or object
|
|
233
|
-
reactPlugin({
|
|
234
|
-
reactPlugin({
|
|
235
|
-
reactPlugin({
|
|
236
|
-
reactPlugin({
|
|
389
|
+
import { reactPlugin } from "elysia-rack/react";
|
|
390
|
+
|
|
391
|
+
// file path (resolved from cwd/dist), raw string, or object — pages optional
|
|
392
|
+
reactPlugin({ css: "./my-panel.css" });
|
|
393
|
+
reactPlugin({ css: { path: "./my-panel.css" } });
|
|
394
|
+
reactPlugin({ css: { content: ":root{--color-primary:red}" } });
|
|
395
|
+
reactPlugin({ panelCss: "body{...}" }); // alias
|
|
396
|
+
// with custom pages: reactPlugin({ pages: myPages, css: "./my.css" })
|
|
397
|
+
// or array: reactPlugin({ pages: [pages, myPages], css: "./my.css" })
|
|
237
398
|
```
|
|
238
399
|
|
|
239
400
|
If a file path exists it is served; otherwise a raw CSS string is served.
|
|
@@ -286,6 +447,16 @@ bun run typecheck # tsc --noEmit
|
|
|
286
447
|
bun run build # dist/ (js via bun build + .d.ts via tsc)
|
|
287
448
|
```
|
|
288
449
|
|
|
450
|
+
## Donate
|
|
451
|
+
|
|
452
|
+
Support pengembangan elysia-rack — traktir kopi via Ko-fi:
|
|
453
|
+
|
|
454
|
+
[](https://ko-fi.com/notoofly_manu)
|
|
455
|
+
|
|
456
|
+
> https://ko-fi.com/notoofly_manu
|
|
457
|
+
|
|
458
|
+
Terima kasih sudah mendukung open-source!
|
|
459
|
+
|
|
289
460
|
## License
|
|
290
461
|
|
|
291
462
|
MIT © notoofly
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
};
|