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 CHANGED
@@ -153,25 +153,25 @@ Conventions:
153
153
 
154
154
  ## Dashboard
155
155
 
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()`.
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 { pages, reactPlugin } from "elysia-rack/react";
162
+ import { reactPlugin } from "elysia-rack/react";
163
163
 
164
164
  const app = new Elysia()
165
- .use(reactPlugin({ pages }))
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
- The dashboard page itself can also be used manually:
183
+ You can still render the dashboard manually:
184
184
 
185
185
  ```ts
186
- import { page, pages, reactPlugin } from "elysia-rack/react";
186
+ import { page, reactPlugin } from "elysia-rack/react";
187
187
 
188
188
  const app = new Elysia()
189
- .use(reactPlugin({ pages }))
189
+ .use(reactPlugin())
190
190
  .use(rack("/catalog/products", { model, metadata: { ... } }))
191
- .get("/", ({ query }) =>
192
- page("/dashboard", {
193
- name: "My Shop",
194
- resource: typeof query.resource === "string" ? query.resource : undefined,
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
- 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).
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 { pages, reactPlugin } from "elysia-rack/react";
231
-
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
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
+ };