elysia-rack 0.1.2 → 0.2.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.
- package/README.md +183 -27
- package/dist/Dashboard-s8nfyf7s.js +39 -0
- package/dist/Panel-3yrk3krj.js +914 -0
- package/dist/Panel-b7jhgvnm.js +916 -0
- package/dist/Panel-pkvqf5v8.js +912 -0
- package/dist/panel-app.js +498 -1
- package/dist/panel-theme.js +38 -1
- package/dist/rack/index.js +13 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +12 -4
- package/dist/react/page/Dashboard.d.ts +0 -3
- package/dist/react/types.d.ts +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -153,25 +153,25 @@ Conventions:
|
|
|
153
153
|
|
|
154
154
|
## Dashboard
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
`dashboard()` is now a simple *Coming Soon* placeholder. The real navigation lives in `panel` — each `rack()` panel shows a sidebar with the entire tree.
|
|
157
|
+
|
|
158
|
+
Recommended setup:
|
|
159
159
|
|
|
160
160
|
```ts
|
|
161
161
|
import { dashboard, rack } from "elysia-rack";
|
|
162
|
-
import {
|
|
162
|
+
import { reactPlugin } from "elysia-rack/react";
|
|
163
163
|
|
|
164
164
|
const app = new Elysia()
|
|
165
|
-
.use(reactPlugin(
|
|
166
|
-
.use(dashboard({ title: "My Panel", path: "/" }))
|
|
165
|
+
.use(reactPlugin()) // pages are optional — defaults to dashboard + panel
|
|
166
|
+
.use(dashboard({ title: "My Panel", path: "/" })) // → Coming Soon at /
|
|
167
167
|
.use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
|
|
168
168
|
.use(rack("/catalog/variants", { model, metadata: { id: "variants", group: "Catalog", order: 2, parent: "products" } }));
|
|
169
|
+
// → Panel at /catalog/products now shows sidebar with Catalog > Products > Variants
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
### Tree metadata
|
|
172
173
|
|
|
173
|
-
`metadata.parent` builds a hierarchy. Children are rendered nested under their
|
|
174
|
-
parent in the sidebar and kept ordered by `order` then label.
|
|
174
|
+
`metadata.parent` builds a hierarchy. Children are rendered nested under their parent in `panel`’s sidebar and kept ordered by `order` then label.
|
|
175
175
|
|
|
176
176
|
```ts
|
|
177
177
|
rack("/catalog/products", { metadata: { id: "products", group: "Catalog", order: 1 } });
|
|
@@ -180,25 +180,179 @@ rack("/catalog/variants", { metadata: { id: "variants", parent: "products", grou
|
|
|
180
180
|
|
|
181
181
|
Helpers: `listRacks()`, `getRack(id)`, `getRackTree()`, `flatRackTree()`, `buildRackTree(racks)`, `clearRacks()` from `elysia-rack`.
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
You can still render the dashboard manually:
|
|
184
184
|
|
|
185
185
|
```ts
|
|
186
|
-
import { page,
|
|
186
|
+
import { page, reactPlugin } from "elysia-rack/react";
|
|
187
187
|
|
|
188
188
|
const app = new Elysia()
|
|
189
|
-
.use(reactPlugin(
|
|
189
|
+
.use(reactPlugin())
|
|
190
190
|
.use(rack("/catalog/products", { model, metadata: { ... } }))
|
|
191
|
-
.get("/", ({
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
.get("/", () => page("/dashboard", { name: "My Shop" }));
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`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.
|
|
195
|
+
|
|
196
|
+
## Tutorial — make it yours
|
|
197
|
+
|
|
198
|
+
This guide walks you through `rack()`, `dashboard()` and `react()` the way you would build a real app. Copy, run, tweak.
|
|
199
|
+
|
|
200
|
+
### 1) `rack()` — describe a resource, get an API + panel
|
|
201
|
+
|
|
202
|
+
Start with the only required thing: which table/model to use.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { rack } from "elysia-rack";
|
|
206
|
+
|
|
207
|
+
rack("/catalog/products", {
|
|
208
|
+
model: { drizzle: { db, table: products } } // or { prisma: { client: prisma, model: prisma.product } }
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
You already get `GET /`, `QUERY /data`, `POST /`, `PUT/PATCH/DELETE /:id` and a panel. Now make it yours step by step.
|
|
213
|
+
|
|
214
|
+
**Give it a name and a place in the menu.** `metadata` is what the dashboard reads from memory.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
rack("/catalog/products", {
|
|
218
|
+
model: { drizzle: { db, table: products } },
|
|
219
|
+
metadata: {
|
|
220
|
+
id: "products", // unique id, defaults to path
|
|
221
|
+
label: "Product", // singular
|
|
222
|
+
pluralLabel: "Products", // shown in sidebar
|
|
223
|
+
group: "Catalog", // sidebar group
|
|
224
|
+
order: 1, // smaller = higher
|
|
225
|
+
icon: "📦",
|
|
226
|
+
parent: "catalog-root", // nest under another id
|
|
227
|
+
hidden: false
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Decide what users can do.** Turn operations on/off. If you disable `create`, the “+ New” button disappears automatically.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
rack("/catalog/products", {
|
|
236
|
+
model,
|
|
237
|
+
operations: { list: true, detail: true, create: true, replace: true, update: true, delete: false }
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Shape the list experience.** Which fields can be searched, filtered, sorted? What’s the default?
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
rack("/catalog/products", {
|
|
245
|
+
model,
|
|
246
|
+
query: {
|
|
247
|
+
searchable: ["name", "sku"],
|
|
248
|
+
filterable: ["status"],
|
|
249
|
+
sortable: ["name", "price", "createdAt"],
|
|
250
|
+
defaultSort: { field: "createdAt", direction: "desc" },
|
|
251
|
+
pagination: { default: 20, max: 100 }
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**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`.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { t } from "elysia";
|
|
260
|
+
|
|
261
|
+
rack("/catalog/products", {
|
|
262
|
+
model,
|
|
263
|
+
validation: {
|
|
264
|
+
create: t.Object({ name: t.String({ minLength: 3 }) }),
|
|
265
|
+
update: t.Partial(t.Object({ name: t.String() })),
|
|
266
|
+
params: t.Object({ id: t.String() }),
|
|
267
|
+
query: t.Object({ search: t.Optional(t.String()) })
|
|
268
|
+
},
|
|
269
|
+
authorization: {
|
|
270
|
+
list: true,
|
|
271
|
+
delete: async ({ request }) => request.headers.get("x-api-key") === "secret"
|
|
272
|
+
// or: delete: "products.delete"
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Tweak the small things** when you need them:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
rack("/catalog/products", {
|
|
281
|
+
model,
|
|
282
|
+
page: { enabled: true, path: "/panel" }, // change or disable GET / panel
|
|
283
|
+
settings: { primaryKey: "slug", softDelete: true, deletedAtField: "deletedAt", returning: true },
|
|
284
|
+
openapi: { tags: ["Catalog"], description: "Products", operations: { list: { summary: "List products" } } },
|
|
285
|
+
idempotency: { required: true, header: "Idempotency-Key", ttl: 3600, store: myRedisStore }
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
That’s the whole `rack()` surface. Start with `model`, then add `metadata` → `query` → `validation/authorization` as your app grows.
|
|
290
|
+
|
|
291
|
+
### 2) `dashboard()` — one line, now Coming Soon
|
|
292
|
+
|
|
293
|
+
`dashboard()` mounts `GET /` that renders a *Coming Soon* page. The tree you registered with `rack()` is not shown here — it lives in `panel`.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { dashboard, rack } from "elysia-rack";
|
|
297
|
+
import { reactPlugin } from "elysia-rack/react";
|
|
298
|
+
|
|
299
|
+
const app = new Elysia()
|
|
300
|
+
.use(reactPlugin()) // 1) UI engine — pages optional
|
|
301
|
+
.use(dashboard({ title: "My Shop", path: "/" })) // 2) Coming Soon at /
|
|
302
|
+
.use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
|
|
303
|
+
.use(rack("/catalog/variants", { model, metadata: { id: "variants", parent: "products", group: "Catalog", order: 2 } }));
|
|
304
|
+
// visit /catalog/products → panel shows sidebar with Catalog > Products > Variants
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Options — all optional:
|
|
308
|
+
|
|
309
|
+
- `path` (default `"/"`) — where the dashboard lives
|
|
310
|
+
- `title` (default `"Panel"`) — header in the sidebar
|
|
311
|
+
- `pagePath` (default `"/dashboard"`) — which React page to render
|
|
312
|
+
|
|
313
|
+
Need it without the helper? The helper is just sugar for:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import { page } from "elysia-rack/react";
|
|
317
|
+
.get("/", ({ query }) => page("/dashboard", { name: "My Shop", resource: query.resource as string }))
|
|
197
318
|
```
|
|
198
319
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
320
|
+
Helpers to inspect the tree yourself: `listRacks()`, `getRack(id)`, `getRackTree()`, `flatRackTree()`, `buildRackTree(racks)`, `clearRacks()`.
|
|
321
|
+
|
|
322
|
+
### 3) `react()` — pages and look
|
|
323
|
+
|
|
324
|
+
`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.
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { pages, reactPlugin, page } from "elysia-rack/react";
|
|
328
|
+
|
|
329
|
+
// Option A: just use defaults
|
|
330
|
+
new Elysia().use(reactPlugin());
|
|
331
|
+
|
|
332
|
+
// Option B: extend defaults with your own page
|
|
333
|
+
const myPages = { "/hello": () => import("./Hello") };
|
|
334
|
+
new Elysia().use(reactPlugin({ pages: myPages })); // merged with defaults
|
|
335
|
+
|
|
336
|
+
// Option C: merge multiple registries — array will be merged
|
|
337
|
+
const extra = { "/admin": () => import("./Admin") };
|
|
338
|
+
new Elysia().use(reactPlugin({ pages: [pages, myPages, extra] }));
|
|
339
|
+
|
|
340
|
+
// With a style override
|
|
341
|
+
new Elysia().use(reactPlugin({
|
|
342
|
+
pages: myPages, // or [pages, myPages]
|
|
343
|
+
css: "./my-panel.css", // file path
|
|
344
|
+
// css: { path: "./my-panel.css" },
|
|
345
|
+
// css: { content: ":root{--color-primary:red}" },
|
|
346
|
+
// panelCss: "body{...}" // alias
|
|
347
|
+
})).get("/hello", () => page("/hello", { name: "Ada" }));
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
What each option does:
|
|
351
|
+
|
|
352
|
+
- `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.
|
|
353
|
+
- `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.
|
|
354
|
+
|
|
355
|
+
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
356
|
|
|
203
357
|
## HTML template
|
|
204
358
|
|
|
@@ -227,13 +381,15 @@ Override the template by editing `src/react/app.html` (and `dist/app.html` for t
|
|
|
227
381
|
Override `panel.css` at `react()` config without forking:
|
|
228
382
|
|
|
229
383
|
```ts
|
|
230
|
-
import {
|
|
231
|
-
|
|
232
|
-
// file path (resolved from cwd/dist), raw string, or object
|
|
233
|
-
reactPlugin({
|
|
234
|
-
reactPlugin({
|
|
235
|
-
reactPlugin({
|
|
236
|
-
reactPlugin({
|
|
384
|
+
import { reactPlugin } from "elysia-rack/react";
|
|
385
|
+
|
|
386
|
+
// file path (resolved from cwd/dist), raw string, or object — pages optional
|
|
387
|
+
reactPlugin({ css: "./my-panel.css" });
|
|
388
|
+
reactPlugin({ css: { path: "./my-panel.css" } });
|
|
389
|
+
reactPlugin({ css: { content: ":root{--color-primary:red}" } });
|
|
390
|
+
reactPlugin({ panelCss: "body{...}" }); // alias
|
|
391
|
+
// with custom pages: reactPlugin({ pages: myPages, css: "./my.css" })
|
|
392
|
+
// or array: reactPlugin({ pages: [pages, myPages], css: "./my.css" })
|
|
237
393
|
```
|
|
238
394
|
|
|
239
395
|
If a file path exists it is served; otherwise a raw CSS string is served.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import"./index-37x76zdn.js";
|
|
2
|
+
|
|
3
|
+
// src/react/page/Dashboard.tsx
|
|
4
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
5
|
+
async function Dashboard(props) {
|
|
6
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
7
|
+
className: "koran-paper font-koran-body text-foreground flex min-h-screen items-center justify-center p-6",
|
|
8
|
+
children: /* @__PURE__ */ jsxDEV("div", {
|
|
9
|
+
className: "bg-card border-border max-w-lg rounded-xl border px-8 py-12 text-center shadow-sm",
|
|
10
|
+
children: [
|
|
11
|
+
/* @__PURE__ */ jsxDEV("p", {
|
|
12
|
+
className: "text-text-muted text-xs font-semibold tracking-widest uppercase",
|
|
13
|
+
children: "Dashboard"
|
|
14
|
+
}, undefined, false, undefined, this),
|
|
15
|
+
/* @__PURE__ */ jsxDEV("h1", {
|
|
16
|
+
className: "font-koran mt-2 text-4xl font-black tracking-tight",
|
|
17
|
+
children: "Coming Soon"
|
|
18
|
+
}, undefined, false, undefined, this),
|
|
19
|
+
/* @__PURE__ */ jsxDEV("p", {
|
|
20
|
+
className: "text-text-muted mt-3 text-sm leading-relaxed",
|
|
21
|
+
children: [
|
|
22
|
+
props.name ? `${props.name} — ` : "",
|
|
23
|
+
"This dashboard is coming soon. Your panels are available via the sidebar in each resource view."
|
|
24
|
+
]
|
|
25
|
+
}, undefined, true, undefined, this),
|
|
26
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
27
|
+
className: "mt-6 flex justify-center",
|
|
28
|
+
children: /* @__PURE__ */ jsxDEV("span", {
|
|
29
|
+
className: "bg-primary text-primary-foreground rounded-md px-4 py-1.5 text-sm font-semibold",
|
|
30
|
+
children: "Stay tuned"
|
|
31
|
+
}, undefined, false, undefined, this)
|
|
32
|
+
}, undefined, false, undefined, this)
|
|
33
|
+
]
|
|
34
|
+
}, undefined, true, undefined, this)
|
|
35
|
+
}, undefined, false, undefined, this);
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
Dashboard as default
|
|
39
|
+
};
|