@pramen/cms 0.0.61 → 0.0.64

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/src/panel.ts ADDED
@@ -0,0 +1,105 @@
1
+ // A custom admin PANEL — a project's own React screen, rendered inside the editor's chrome
2
+ // at a real route with a real nav entry.
3
+ //
4
+ // WHY A SECOND KIND, WHEN `adminPage()` EXISTS
5
+ //
6
+ // Block Kit (`adminPage()`) is a server-driven vocabulary: the handler returns JSON, the
7
+ // editor renders it, and the whole page comes back on every interaction. That is exactly
8
+ // right for a list-and-form screen, and it is the reason no project JavaScript runs in the
9
+ // admin. But the properties that make it safe are the same ones that cap it — an input
10
+ // cannot fire an interaction, every control is disabled for the round trip (so focus and
11
+ // caret are lost on each keystroke that matters), a table row cannot expand, and there is
12
+ // no link, no redirect, no dialog, no autofocus and no date input. Those are not gaps to
13
+ // patch one element at a time; a screen that needs local interaction needs local code.
14
+ //
15
+ // The alternative a project reaches for when Block Kit runs out is what this replaces: a
16
+ // standalone React SPA served next to the editor with the chrome rebuilt by hand. It goes
17
+ // out of the application, it does not have the same layout, and every chrome fix has to be
18
+ // made twice.
19
+ //
20
+ // So a panel is the SAME registry entry as a Block Kit page with the render moved to the
21
+ // browser. Same slug space, same `/apps/:slug` route, same "Apps" band in the nav, and —
22
+ // the part that matters — the same server-side role filter: a panel the caller may not open
23
+ // is absent from `listAdminPages`, so there is no nav entry to click and no route to reach.
24
+ //
25
+ // WHAT LIVES WHERE. The server owns everything a nav entry is made of (slug, label, icon,
26
+ // position, roles); the browser bundle owns only the component. That split is deliberate:
27
+ // if the bundle declared the label and the position, a client that failed to load would
28
+ // take the nav entry with it, and a client that loaded would be declaring its own placement
29
+ // with nothing to check it against. A panel whose bundle never registers is a listed entry
30
+ // that renders a diagnostic — which is a legible failure — rather than a section that
31
+ // silently ceases to exist.
32
+ //
33
+ // WHAT IS GIVEN UP. Block Kit's headline property is that no project JavaScript ever runs in
34
+ // the admin (#33). A panel gives that up, deliberately and only where a deployment asks for
35
+ // it: the bundle runs in the editor's own page with the editor's own session in scope, so it
36
+ // can read the stored token and call anything the caller can. `roles` below and the ACL still
37
+ // bound what the SERVER will do, and `PanelApi` is a small surface to write against, but
38
+ // neither is a sandbox — a panel is part of the admin, not a guest in it. That is the reason
39
+ // `adminPage()` remains the first thing to reach for and this the second.
40
+ //
41
+ // The same reconciliation is written from the other side at the top of `blockkit.ts` — why
42
+ // "do not ship the component tree" and this are one position rather than two, and which of
43
+ // the pair to reach for first. Read together; changing one without the other leaves the
44
+ // framework arguing with itself.
45
+ //
46
+ // A LEAF module, like `href.ts` and `nav.ts`: `blockkit.ts` imports it to widen the
47
+ // registry, and it imports nothing back.
48
+
49
+ /** One custom admin panel: a nav entry the browser bundle fills in.
50
+ *
51
+ * There is no `render` here and there is not meant to be one — the rendering half is a
52
+ * React component the deployment's panel bundle registers under the same `slug`. Everything
53
+ * that decides whether the entry EXISTS is here, on the server, where it can be enforced.
54
+ */
55
+ export interface AdminPanelDef {
56
+ /** Discriminates a panel from an `AdminPageDef` in the one registry they share. Set by
57
+ * {@link adminPanel}; it is a required field rather than an inferred one so a hand-built
58
+ * object literal cannot be a half-declared panel. */
59
+ readonly kind: "panel";
60
+ /** URL + registry key: served at `/apps/:slug` in the editor, and the id the browser
61
+ * bundle registers its component under. */
62
+ readonly slug: string;
63
+ /** Nav label. */
64
+ readonly label: string;
65
+ /** Optional nav icon (emoji or short string). */
66
+ readonly icon?: string;
67
+ /** Where it sits in the nav — see `NAV_ORDER`. Defaults to `NAV_ORDER.adminPages`. */
68
+ readonly navOrder?: number;
69
+ /** Roles that may open it. Defaults to the deployment's `editorRoles`, exactly as a Block
70
+ * Kit page's does — one registry, one gate.
71
+ *
72
+ * This is the ONLY authorization a panel gets for free. A panel's own code runs in the
73
+ * browser, so every call it makes is an ordinary RPC under the caller's own identity and
74
+ * ACL; this list decides who is shown the screen, not what the screen may do. */
75
+ readonly roles?: readonly string[];
76
+ }
77
+
78
+ /**
79
+ * Declare a custom admin panel. Spread the result into `createAdminPageHandlers` alongside
80
+ * any `adminPage()`s:
81
+ *
82
+ * const curation = adminPanel("curation", {
83
+ * label: "Curation",
84
+ * icon: "🎛",
85
+ * navOrder: NAV_ORDER.media + 10,
86
+ * roles: ["editor", "admin"],
87
+ * });
88
+ *
89
+ * handlers = { ...createAdminPageHandlers([desk, curation], { editorRoles }) };
90
+ *
91
+ * The matching component is registered by the deployment's panel bundle — see the
92
+ * "Custom admin panels" section of the CMS docs.
93
+ */
94
+ export function adminPanel(slug: string, opts: Omit<AdminPanelDef, "slug" | "kind">): AdminPanelDef {
95
+ return { ...opts, kind: "panel", slug };
96
+ }
97
+
98
+ /** Whether a registry entry is a panel (and so has no server-side render).
99
+ *
100
+ * Reads the discriminant rather than testing for the ABSENCE of `render`: "no render" is
101
+ * also what a malformed page looks like, and `validateAdminPages` has to be able to tell a
102
+ * panel from a page someone forgot to finish. */
103
+ export function isAdminPanel(def: { readonly kind?: string }): def is AdminPanelDef {
104
+ return def.kind === "panel";
105
+ }