@pramen/cms-editor 0.0.52 → 0.0.53
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 +57 -31
- package/dist/{app.css → editor.css} +46 -1
- package/dist/{main.a0tgw8hg.js → editor.js} +102 -102
- package/package.json +5 -1
- package/src/api.ts +15 -3
- package/src/app-context.tsx +37 -10
- package/src/main.tsx +41 -8
- package/src/mount.ts +143 -0
- package/dist/config.js +0 -15
- package/dist/fonts/README.md +0 -18
- package/dist/fonts/nc-fontina-variable.woff2 +0 -0
- package/dist/fonts.css +0 -45
- package/dist/index.html +0 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/cms-editor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.53",
|
|
4
4
|
"description": "Visual block/page editor for @pramen/cms \u2014 a standalone React SPA that talks to the CMS handlers over HTTP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"homepage": "https://github.com/netvarec/pramen#readme",
|
|
12
12
|
"bugs": "https://github.com/netvarec/pramen/issues",
|
|
13
13
|
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
"./editor.js": "./dist/editor.js",
|
|
16
|
+
"./editor.css": "./dist/editor.css"
|
|
17
|
+
},
|
|
14
18
|
"files": [
|
|
15
19
|
"dist",
|
|
16
20
|
"src"
|
package/src/api.ts
CHANGED
|
@@ -36,14 +36,26 @@ export function isTokenExpired(token: string): boolean {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const LS = "pramen.cmsEditor";
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The session for this page load.
|
|
42
|
+
*
|
|
43
|
+
* A backend DECLARED by the shell (see `mount.ts`) wins over anything stored: the server
|
|
44
|
+
* that served the editor knows where its own API is, so there is nothing for a person to
|
|
45
|
+
* type and get wrong, and a stale url from an earlier deployment can't outlive it. Only
|
|
46
|
+
* the token is the browser's to remember.
|
|
47
|
+
*/
|
|
48
|
+
export function loadConfig(declared?: { url: string; tenant: string }): Config {
|
|
49
|
+
let stored: Partial<Config> = {};
|
|
40
50
|
try {
|
|
41
51
|
const raw = localStorage.getItem(LS);
|
|
42
|
-
if (raw)
|
|
52
|
+
if (raw) stored = JSON.parse(raw) as Partial<Config>;
|
|
43
53
|
} catch {
|
|
44
54
|
/* ignore */
|
|
45
55
|
}
|
|
46
|
-
|
|
56
|
+
const token = typeof stored.token === "string" ? stored.token : "";
|
|
57
|
+
if (declared) return { baseUrl: declared.url, tenant: declared.tenant, token };
|
|
58
|
+
return { baseUrl: stored.baseUrl ?? "http://localhost:8787", tenant: stored.tenant ?? "main", token };
|
|
47
59
|
}
|
|
48
60
|
export function saveConfig(cfg: Config): void {
|
|
49
61
|
localStorage.setItem(LS, JSON.stringify(cfg));
|
package/src/app-context.tsx
CHANGED
|
@@ -7,12 +7,19 @@ import { Button, Input } from "@podoba/react";
|
|
|
7
7
|
import { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { Api, clearConfig, isTokenExpired, loadConfig, saveConfig, type Config } from "./api";
|
|
9
9
|
import { BRAND, SETUP_TITLE, type BrandConfig } from "./brand";
|
|
10
|
+
import { readBackend, type BackendHost } from "./mount";
|
|
10
11
|
import { DEFAULT_CAPABILITIES, type CmsCapabilities, type CollectionMeta, type JsonValue } from "./types";
|
|
11
12
|
|
|
12
13
|
declare global {
|
|
13
14
|
interface Window {
|
|
14
|
-
/** Runtime config
|
|
15
|
+
/** Runtime config written by the SHELL that served the editor — a server-rendered
|
|
16
|
+
* inline script, not a file anyone hand-edits (see `PramenAdmin.astro` in
|
|
17
|
+
* @pramen/cms-astro, and the dev preview in scripts/build.ts). */
|
|
15
18
|
PRAMEN_CMS_EDITOR?: {
|
|
19
|
+
/** Which CMS Worker to call, and as which tenant. Declared by the shell because the
|
|
20
|
+
* server already knows — it is what makes the Setup screen a token field and
|
|
21
|
+
* nothing else. `url: ""` means the CMS is on this same origin. */
|
|
22
|
+
backend?: { url?: string; tenant?: string };
|
|
16
23
|
signInUrl?: string;
|
|
17
24
|
/** Hide the Pages tab for deployments that use collections only — no block/page
|
|
18
25
|
* building. The tab is otherwise always shown and lands on an empty list, which
|
|
@@ -32,7 +39,11 @@ declare global {
|
|
|
32
39
|
}
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
/**
|
|
42
|
+
/** The backend the shell declared, or `undefined` when it declared none (the dev preview's
|
|
43
|
+
* standalone mode). Present ⇒ the Setup screen asks for a token and nothing else. */
|
|
44
|
+
const BACKEND = readBackend(globalThis as BackendHost);
|
|
45
|
+
|
|
46
|
+
/** External sign-in URL, if the host configured one. When set, an
|
|
36
47
|
* unauthenticated OR expired session is redirected here instead of the built-in Setup
|
|
37
48
|
* screen — for deployments whose auth (magic-link, SSO, …) lives on a separate page.
|
|
38
49
|
*
|
|
@@ -88,14 +99,16 @@ export function useApp(): AppContextValue {
|
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
export function AppProvider({ children }: { children: React.ReactNode }) {
|
|
91
|
-
const [cfg, setCfg] = useState<Config>(loadConfig());
|
|
102
|
+
const [cfg, setCfg] = useState<Config>(() => loadConfig(BACKEND));
|
|
92
103
|
const [me, setMe] = useState<Me | null>(null);
|
|
93
104
|
const [collections, setCollections] = useState<CollectionMeta[]>([]);
|
|
94
105
|
const [cms, setCms] = useState<CmsCapabilities>(DEFAULT_CAPABILITIES);
|
|
95
106
|
const [error, setError] = useState("");
|
|
96
|
-
// A usable session =
|
|
97
|
-
// no session: otherwise the editor mounts and every RPC 403s into an error
|
|
98
|
-
|
|
107
|
+
// A usable session = somewhere to call + a token that is NOT expired. An expired token
|
|
108
|
+
// counts as no session: otherwise the editor mounts and every RPC 403s into an error
|
|
109
|
+
// banner. "Somewhere to call" is satisfied by a declared backend even when its url is the
|
|
110
|
+
// empty string, which is the same-origin case and a perfectly good place to call.
|
|
111
|
+
const authValid = Boolean(BACKEND || cfg.baseUrl) && Boolean(cfg.token) && !isTokenExpired(cfg.token);
|
|
99
112
|
const api = useMemo(() => new Api(cfg, SIGN_IN_URL ? redirectToSignIn : undefined), [cfg]);
|
|
100
113
|
|
|
101
114
|
// Unsaved-changes guard for in-app navigation. A ref, not state: the guard is read at
|
|
@@ -171,14 +184,28 @@ function Setup({ cfg, onSave }: { cfg: Config; onSave: (c: Config) => void }) {
|
|
|
171
184
|
<h1 className="mb-4 text-display text-fg">
|
|
172
185
|
{BRAND.name} <span className="text-fg-subtle">{SETUP_TITLE}</span>
|
|
173
186
|
</h1>
|
|
187
|
+
{/* Two screens, because there are two things a deployment can leave unanswered. When
|
|
188
|
+
the shell declared a backend there is nothing to point at — asking for a URL that
|
|
189
|
+
is already known invites someone to type the mount path instead of the origin and
|
|
190
|
+
get a stream of "non-JSON response" back. */}
|
|
174
191
|
<p className="mb-6 text-sm text-fg-muted">
|
|
175
|
-
|
|
192
|
+
{BACKEND ? (
|
|
193
|
+
<>Paste an editor/reviewer JWT to sign in.</>
|
|
194
|
+
) : (
|
|
195
|
+
<>
|
|
196
|
+
Point at your Worker and paste an editor/reviewer JWT. CORS must allow this origin (<code>CORS_ORIGINS</code>).
|
|
197
|
+
</>
|
|
198
|
+
)}
|
|
176
199
|
</p>
|
|
177
200
|
<div className="flex flex-col gap-4">
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
{BACKEND ? null : (
|
|
202
|
+
<>
|
|
203
|
+
<Input label="Worker base URL" value={c.baseUrl} onChange={(baseUrl) => setC({ ...c, baseUrl })} placeholder="https://your-worker.workers.dev" />
|
|
204
|
+
<Input label="Tenant" value={c.tenant} onChange={(tenant) => setC({ ...c, tenant })} placeholder="main" />
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
180
207
|
<Input label="Bearer token (editor or reviewer)" value={c.token} onChange={(token) => setC({ ...c, token })} placeholder="eyJ…" />
|
|
181
|
-
<Button className="mt-2 w-full" onPress={() => onSave(c)} isDisabled={!c.baseUrl || !c.token}>
|
|
208
|
+
<Button className="mt-2 w-full" onPress={() => onSave(c)} isDisabled={(!BACKEND && !c.baseUrl) || !c.token}>
|
|
182
209
|
Connect
|
|
183
210
|
</Button>
|
|
184
211
|
</div>
|
package/src/main.tsx
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { BuzolaProvider } from "@buzola/router";
|
|
2
|
-
import { StrictMode } from "react";
|
|
1
|
+
import { BuzolaProvider, Router, createBrowserNavigationAdapter } from "@buzola/router";
|
|
2
|
+
import { StrictMode, useRef } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
import { pageRegistry, routes } from "virtual:buzola/routes";
|
|
5
5
|
import { AppProvider } from "./app-context";
|
|
6
6
|
import { DOCUMENT_TITLE } from "./brand";
|
|
7
|
+
import { isWithinBasePath, readBasePath, scopeToBasePath } from "./mount";
|
|
7
8
|
|
|
8
|
-
// Styling is podoba:
|
|
9
|
-
//
|
|
9
|
+
// Styling is podoba: the compiled Tailwind (podoba preset, with @podoba/tokens' variables
|
|
10
|
+
// and the web font inlined) is a single stylesheet the SHELL links — see scripts/build.ts
|
|
11
|
+
// for what is built, and @pramen/cms-astro for the shell that serves it.
|
|
10
12
|
|
|
11
|
-
// The <title>
|
|
12
|
-
// can only be the default. Re-apply the configured
|
|
13
|
-
//
|
|
13
|
+
// The shell's <title> is written before the app boots, so on a deployment that has
|
|
14
|
+
// configured a wordmark it can only be the default. Re-apply the configured one here; the
|
|
15
|
+
// server-rendered tag stays the pre-hydration fallback.
|
|
14
16
|
//
|
|
15
17
|
// `DOCUMENT_TITLE`, not `${BRAND.title} editor`: appending a fixed English noun to the one
|
|
16
18
|
// string this feature exists to hand over would put a foreign word in a rebranded client's
|
|
@@ -18,11 +20,42 @@ import { DOCUMENT_TITLE } from "./brand";
|
|
|
18
20
|
document.title = DOCUMENT_TITLE;
|
|
19
21
|
|
|
20
22
|
const el = document.getElementById("app");
|
|
23
|
+
const basePath = readBasePath(el);
|
|
24
|
+
|
|
25
|
+
// The prefix and the URL the shell was served at come from the same place, so a mismatch
|
|
26
|
+
// means the shell and the route that rendered it have drifted. Worth one line: the symptom
|
|
27
|
+
// is otherwise a silent 404 on the host site for every click in the topbar, with an empty
|
|
28
|
+
// console — buzola's own "no route matched" warning is dead here, because `_404.tsx`'s
|
|
29
|
+
// catch-all guarantees a match.
|
|
30
|
+
if (!isWithinBasePath(location.pathname, basePath)) {
|
|
31
|
+
console.warn(`pramen/cms-editor: mounted for "${basePath}" but served at "${location.pathname}" — navigation will leave the editor.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The router, built lazily so it exists only once there is a session to route.
|
|
35
|
+
*
|
|
36
|
+
* `AppProvider` renders the Setup screen (or hands off to sign-in) WITHOUT its children
|
|
37
|
+
* when there is no valid session, and `createBrowserNavigationAdapter()` throws outright on
|
|
38
|
+
* a browser with no Navigation API. Constructing at module scope would turn that into a
|
|
39
|
+
* blank page before `createRoot` ever ran; here it stays what it was — the Setup screen,
|
|
40
|
+
* where a first admin can still paste a JWT.
|
|
41
|
+
*/
|
|
42
|
+
function EditorRouter({ basePath }: { basePath: string }) {
|
|
43
|
+
const router = useRef<Router | null>(null);
|
|
44
|
+
router.current ??= new Router({
|
|
45
|
+
routes,
|
|
46
|
+
// Scoped, so a co-hosted editor intercepts navigation within its own mount only.
|
|
47
|
+
adapter: scopeToBasePath(createBrowserNavigationAdapter(), basePath),
|
|
48
|
+
pageRegistry,
|
|
49
|
+
basePath,
|
|
50
|
+
});
|
|
51
|
+
return <BuzolaProvider router={router.current} />;
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
if (el)
|
|
22
55
|
createRoot(el).render(
|
|
23
56
|
<StrictMode>
|
|
24
57
|
<AppProvider>
|
|
25
|
-
<
|
|
58
|
+
<EditorRouter basePath={basePath} />
|
|
26
59
|
</AppProvider>
|
|
27
60
|
</StrictMode>,
|
|
28
61
|
);
|
package/src/mount.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Where the editor is mounted, and which backend it talks to — read off the shell that
|
|
2
|
+
// served it.
|
|
3
|
+
//
|
|
4
|
+
// The SPA's routes are authored from "/" ("/media", "/pages/:pageId", …), so an editor
|
|
5
|
+
// served under a path prefix needs buzola's Router to prepend it. That prefix is NOT
|
|
6
|
+
// something a human configures: the server that ROUTED the request is the same one that
|
|
7
|
+
// renders the shell, so it stamps the prefix onto the mount node and the two can never
|
|
8
|
+
// disagree. `@pramen/cms-astro` injects its admin route and writes this attribute from a
|
|
9
|
+
// single constant; the dev preview server does the same.
|
|
10
|
+
//
|
|
11
|
+
// A `data-*` attribute rather than a global, for the one value that must be right:
|
|
12
|
+
// `dataset` is a string or undefined BY CONSTRUCTION, so there is no non-string value to
|
|
13
|
+
// guard against, and it cannot be set for an element other than the one being mounted.
|
|
14
|
+
//
|
|
15
|
+
// Deliberately free of React and of any runtime buzola import (the adapter types below are
|
|
16
|
+
// type-only, hence erased), so every rule here can be exercised without a DOM.
|
|
17
|
+
|
|
18
|
+
import type { BuzolaNavigateEvent, NavigationAdapter } from "@buzola/router";
|
|
19
|
+
|
|
20
|
+
/** A mount path must be a chain of path segments rooted at the origin.
|
|
21
|
+
*
|
|
22
|
+
* Rooted, and required to say so: buzola prepends the base path to every href it builds, so
|
|
23
|
+
* anything that is not already an absolute path resolves somewhere unintended. A leading
|
|
24
|
+
* slash is NOT added for a value missing one — that convenience is what turns a pasted
|
|
25
|
+
* `https://host/admin` into `/https://host/admin`, which is a real path and would be
|
|
26
|
+
* mounted. `//cdn.example.com` is rejected for the same reason: protocol-relative, and the
|
|
27
|
+
* natural product of `"/" + prefix` where the prefix already carried a slash.
|
|
28
|
+
*
|
|
29
|
+
* Segments only, too: a value carrying `?`, `#` or `\` is not a mount path, and each breaks
|
|
30
|
+
* routing in its own silent way (a query swallows the rest of every href; a backslash
|
|
31
|
+
* resolves off-site while looking local). */
|
|
32
|
+
const MOUNT_PATH = /^\/[^/\\?#][^\\?#]*$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalize the mount node's declared prefix to buzola's shape: `""` for the origin root,
|
|
36
|
+
* else a leading slash and no trailing one.
|
|
37
|
+
*
|
|
38
|
+
* Nothing declared means the root, which is how the editor has always been served. A
|
|
39
|
+
* declared-but-unusable value is WARNED about and falls back to the root rather than
|
|
40
|
+
* routing every link through a stray prefix — the value is server-generated, so if one
|
|
41
|
+
* ever fails this check something upstream is wrong and a green deploy is the worst place
|
|
42
|
+
* to discover it (`brand.ts` warns for the same reason).
|
|
43
|
+
*/
|
|
44
|
+
export function resolveBasePath(raw?: string | null): string {
|
|
45
|
+
const trimmed = (raw ?? "").trim();
|
|
46
|
+
if (trimmed === "" || trimmed === "/") return "";
|
|
47
|
+
if (!MOUNT_PATH.test(trimmed)) {
|
|
48
|
+
console.warn(`pramen/cms-editor: ignoring unusable mount path ${JSON.stringify(trimmed)} — mounting at the origin root.`);
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
return trimmed.replace(/\/+$/, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read the prefix the shell stamped onto the mount node. */
|
|
55
|
+
export function readBasePath(el: { dataset: DOMStringMap } | null): string {
|
|
56
|
+
return resolveBasePath(el?.dataset.basePath);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether a pathname is INSIDE a mount prefix — the prefix itself, or something below it.
|
|
61
|
+
*
|
|
62
|
+
* Anchored at a segment boundary on purpose. buzola's own `stripBasePath` is a bare
|
|
63
|
+
* `startsWith`, so with a prefix of `/cms` the host's `/cmsmedia` strips to `media` and
|
|
64
|
+
* renders the editor's Media library at a URL that has nothing to do with the editor.
|
|
65
|
+
*/
|
|
66
|
+
export function isWithinBasePath(pathname: string, basePath: string): boolean {
|
|
67
|
+
if (!basePath) return true;
|
|
68
|
+
return pathname === basePath || pathname.startsWith(`${basePath}/`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Scope a navigation adapter to one mount prefix.
|
|
73
|
+
*
|
|
74
|
+
* `Router.start()` intercepts every navigation that matches a route, and `_404.tsx`
|
|
75
|
+
* registers the catch-all `/:__notFound+` — so EVERY same-origin path matches. At the
|
|
76
|
+
* origin root that is correct (the editor is the whole origin). Co-hosted under a prefix
|
|
77
|
+
* it is not: a click on the host site's own `/blog` would be cancelled by `event.intercept`
|
|
78
|
+
* and render the editor's "Nothing lives here" while the address bar reads `/blog`.
|
|
79
|
+
*
|
|
80
|
+
* Filtering at the adapter means the router never SEES an off-prefix navigation, so the
|
|
81
|
+
* browser handles it natively — no route table changes, no buzola fork.
|
|
82
|
+
*/
|
|
83
|
+
export function scopeToBasePath(inner: NavigationAdapter, basePath: string): NavigationAdapter {
|
|
84
|
+
if (!basePath) return inner;
|
|
85
|
+
type Handler = (event: BuzolaNavigateEvent) => void;
|
|
86
|
+
// Keyed by the caller's handler so `removeEventListener` can find the wrapper it added;
|
|
87
|
+
// without this the router's `start()` teardown would leave the listener attached.
|
|
88
|
+
const wrappers = new Map<Handler, Handler>();
|
|
89
|
+
return {
|
|
90
|
+
getCurrentURL: () => inner.getCurrentURL(),
|
|
91
|
+
navigate: (url, options) => inner.navigate(url, options),
|
|
92
|
+
back: () => inner.back(),
|
|
93
|
+
forward: () => inner.forward(),
|
|
94
|
+
getState: () => inner.getState(),
|
|
95
|
+
addEventListener(type, handler) {
|
|
96
|
+
const wrapper: Handler = (event) => {
|
|
97
|
+
// A malformed destination is not ours to claim.
|
|
98
|
+
let pathname: string;
|
|
99
|
+
try {
|
|
100
|
+
pathname = new URL(event.destination.url).pathname;
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (isWithinBasePath(pathname, basePath)) handler(event);
|
|
105
|
+
};
|
|
106
|
+
wrappers.set(handler, wrapper);
|
|
107
|
+
inner.addEventListener(type, wrapper);
|
|
108
|
+
},
|
|
109
|
+
removeEventListener(type, handler) {
|
|
110
|
+
const wrapper = wrappers.get(handler);
|
|
111
|
+
if (!wrapper) return;
|
|
112
|
+
wrappers.delete(handler);
|
|
113
|
+
inner.removeEventListener(type, wrapper);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The backend the shell declared: which Worker to call, and as which tenant.
|
|
119
|
+
*
|
|
120
|
+
* When present these are NOT read from (or written to) localStorage — the server knows
|
|
121
|
+
* where its own API is, and a stale stored value from an earlier deployment would win
|
|
122
|
+
* forever otherwise. Only the session token is the browser's to remember. */
|
|
123
|
+
export interface DeclaredBackend {
|
|
124
|
+
/** Origin of the CMS Worker. `""` means same-origin. */
|
|
125
|
+
url: string;
|
|
126
|
+
tenant: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** What the shell may set under `window.PRAMEN_CMS_EDITOR.backend`. */
|
|
130
|
+
export interface BackendHost {
|
|
131
|
+
PRAMEN_CMS_EDITOR?: { backend?: { url?: unknown; tenant?: unknown } };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Read the declared backend off a host global, or `undefined` when the shell declared
|
|
135
|
+
* none (the dev preview's standalone mode, where the Setup screen asks for it). */
|
|
136
|
+
export function readBackend(host: BackendHost | undefined): DeclaredBackend | undefined {
|
|
137
|
+
const declared = host?.PRAMEN_CMS_EDITOR?.backend;
|
|
138
|
+
// A url is what makes the declaration meaningful; `""` (same-origin) is a real value, so
|
|
139
|
+
// this tests for the STRING, not for truthiness.
|
|
140
|
+
if (typeof declared?.url !== "string") return undefined;
|
|
141
|
+
const tenant = typeof declared.tenant === "string" && declared.tenant.trim() !== "" ? declared.tenant.trim() : "main";
|
|
142
|
+
return { url: declared.url.replace(/\/+$/, ""), tenant };
|
|
143
|
+
}
|
package/dist/config.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Runtime configuration for the pramen CMS editor. Override this file in your host app —
|
|
2
|
-
// no rebuild needed. Every field is optional:
|
|
3
|
-
//
|
|
4
|
-
// window.PRAMEN_CMS_EDITOR = {
|
|
5
|
-
// // Send unauthenticated / expired sessions to your own sign-in page instead of the
|
|
6
|
-
// // built-in Setup screen (?setup=1 still forces Setup, to paste a first-admin JWT):
|
|
7
|
-
// signInUrl: "/signin/",
|
|
8
|
-
// // The wordmark in the topbar, on the Setup screen and in the browser tab. Set this
|
|
9
|
-
// // when you deploy the editor for a client — the default says "pramen", which is the
|
|
10
|
-
// // framework's name, not theirs. `suffix: null` drops the "· cms" half.
|
|
11
|
-
// brand: { name: "Acme", suffix: "cms" },
|
|
12
|
-
// hidePages: true, // collections-only deployments
|
|
13
|
-
// extraNav: [{ label: "Curation", href: "/curate" }],
|
|
14
|
-
// };
|
|
15
|
-
window.PRAMEN_CMS_EDITOR = window.PRAMEN_CMS_EDITOR || {};
|
package/dist/fonts/README.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Font binaries
|
|
2
|
-
|
|
3
|
-
`@font-face` in [`../fonts.css`](../fonts.css) references the file below by exact
|
|
4
|
-
name. Drop the woff2 here and it's bundled and served by any consumer that
|
|
5
|
-
imports `@podoba/tokens/fonts.css`.
|
|
6
|
-
|
|
7
|
-
NC Fontina is a **variable font** — a single file covers the whole weight axis:
|
|
8
|
-
|
|
9
|
-
| File | Axis | Covers |
|
|
10
|
-
| ---------------------------- | ------- | ------------------------------------------------- |
|
|
11
|
-
| `nc-fontina-variable.woff2` | `wght` | 400 `font-normal` · 500 `font-medium` · 600 `font-semibold` · 700 `font-bold` |
|
|
12
|
-
|
|
13
|
-
The `@font-face` declares `font-weight: 100 900` (a range). Narrow it in
|
|
14
|
-
`fonts.css` to the font's real `wght` axis if it's smaller (e.g. `400 700`).
|
|
15
|
-
|
|
16
|
-
Italics: this upright file only. If NC Fontina has a separate italic file, add a
|
|
17
|
-
second `@font-face` with `font-style: italic`; otherwise the browser synthesizes
|
|
18
|
-
obliques where `italic` is used (e.g. the rich-text editor).
|
|
Binary file
|
package/dist/fonts.css
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @podoba/tokens/fonts.css — bundled web fonts for the design system.
|
|
3
|
-
*
|
|
4
|
-
* HAND-AUTHORED (unlike variables.css, which is generated). This file ships the
|
|
5
|
-
* actual font binaries via @font-face and points the family-name token at them,
|
|
6
|
-
* so any consumer gets the real typeface with a single import:
|
|
7
|
-
*
|
|
8
|
-
* import "@podoba/tokens/variables.css";
|
|
9
|
-
* import "@podoba/tokens/fonts.css"; // ← add this alongside variables.css
|
|
10
|
-
*
|
|
11
|
-
* NC Fontina is our primary UI sans. The @font-face `src` uses relative URLs, so
|
|
12
|
-
* bundlers (Vite/webpack/Next) resolve and fingerprint `./fonts/*.woff2` for you.
|
|
13
|
-
* `font-display: swap` renders fallback text immediately and swaps in NC Fontina
|
|
14
|
-
* when it loads — no invisible-text flash.
|
|
15
|
-
*
|
|
16
|
-
* Drop the binary into ./fonts/ as nc-fontina-variable.woff2 (see
|
|
17
|
-
* ./fonts/README.md). This is a VARIABLE font: one file covers the whole weight
|
|
18
|
-
* axis, so `font-weight` is a RANGE. NC Fontina's wght axis is 200–700 (verified
|
|
19
|
-
* via fontTools); the components use font-normal (400), font-medium (500),
|
|
20
|
-
* font-semibold (600), font-bold (700) — all inside that range.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
@font-face {
|
|
24
|
-
font-family: 'NC Fontina';
|
|
25
|
-
font-style: normal;
|
|
26
|
-
font-weight: 200 700;
|
|
27
|
-
font-display: swap;
|
|
28
|
-
src: url('./fonts/nc-fontina-variable.woff2') format('woff2');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/*
|
|
32
|
-
* Point the family-name token at NC Fontina. This override lives HERE, not in the
|
|
33
|
-
* auto-generated variables.css (which would clobber it on the next token sync).
|
|
34
|
-
* Because fonts.css is imported after variables.css, this :root rule wins the
|
|
35
|
-
* cascade. The fallback stack mirrors the generated --font-sans so metrics stay
|
|
36
|
-
* close before NC Fontina loads / if it fails.
|
|
37
|
-
*
|
|
38
|
-
* font-feature-settings turns on NC Fontina's stylistic sets ss02–ss05 (the
|
|
39
|
-
* default letterforms the brand uses: jy/at/g ligatures and the @ alternate).
|
|
40
|
-
* It's an inherited property, so declaring it on :root cascades to every element.
|
|
41
|
-
*/
|
|
42
|
-
:root {
|
|
43
|
-
--font-sans: 'NC Fontina', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
44
|
-
font-feature-settings: 'ss02' 1, 'ss03' 1, 'ss04' 1, 'ss05' 1;
|
|
45
|
-
}
|
package/dist/index.html
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>pramen · cms editor</title>
|
|
7
|
-
<link rel="stylesheet" href="/fonts.css" />
|
|
8
|
-
<link rel="stylesheet" href="/app.css" />
|
|
9
|
-
<!-- Runtime config, loaded before the app so window.PRAMEN_CMS_EDITOR is set at boot.
|
|
10
|
-
A default config.js ships in dist; a host overrides it to set e.g. signInUrl or
|
|
11
|
-
the wordmark. The <title> below is the pre-hydration fallback — the app re-applies
|
|
12
|
-
the configured brand on boot, since this file is baked before any config exists. -->
|
|
13
|
-
<script src="/config.js"></script>
|
|
14
|
-
</head>
|
|
15
|
-
<body>
|
|
16
|
-
<div id="app"></div>
|
|
17
|
-
<script type="module" src="/main.a0tgw8hg.js"></script>
|
|
18
|
-
</body>
|
|
19
|
-
</html>
|