@pramen/cms-editor 0.0.21 → 0.0.23

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.
@@ -0,0 +1,68 @@
1
+ /* AUTO-GENERATED by @tools/sync-tokens — DO NOT EDIT BY HAND. */
2
+ /* Source of truth: the API → packages/tokens/src/tokens.json. */
3
+ /* Regenerate: bun --filter @tools/sync-tokens start */
4
+
5
+ :root {
6
+ --color-accent-green-light: #b1fed4;
7
+ --color-accent-mint: #dff6ec;
8
+ --color-accent-pink: #ff69b4;
9
+ --color-accent-strong: #4bcf9f;
10
+ --color-accent-yellow: #ffcc6a;
11
+ --color-border: #eceae1;
12
+ --color-brand-green: #75e7b8;
13
+ --color-brand-primary: #0d0d0d;
14
+ --color-brand-secondary: #6eddb1;
15
+ --color-danger: #dc2626;
16
+ --color-danger-fg: #ffffff;
17
+ --color-fg: #0d0d0d;
18
+ --color-fg-inverted: #ffffff;
19
+ --color-fg-muted: #7d786f;
20
+ --color-fg-subtle: #aba89c;
21
+ --color-neutral-600: #333333;
22
+ --color-ring: #0d0d0d;
23
+ --color-success: #4bcf9f;
24
+ --color-success-fg: #0d0d0d;
25
+ --color-surface: #ffffff;
26
+ --color-surface-card: #f7f6f2;
27
+ --color-surface-inverted: #242423;
28
+ --color-surface-muted: #eceae1;
29
+ --color-surface-placeholder: #333333;
30
+ --font-mono: 'GT America Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
31
+ --font-sans: 'GT America', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
32
+ --font-size-body: 1rem;
33
+ --font-size-callout: 0.9375rem;
34
+ --font-size-caption: 0.6875rem;
35
+ --font-size-compact: 0.8125rem;
36
+ --font-size-display: 1.75rem;
37
+ --font-size-heading1: 1.75rem;
38
+ --font-size-heading2: 1.375rem;
39
+ --font-size-heading3: 1.25rem;
40
+ --font-size-heading4: 1.125rem;
41
+ --font-size-heading5: 1rem;
42
+ --font-size-headline: 1.375rem;
43
+ --font-size-label: 0.75rem;
44
+ --font-size-micro: 0.625rem;
45
+ --font-size-subtitle: 1.125rem;
46
+ --font-size-title: 1.25rem;
47
+ --line-height-heading1: 30px;
48
+ --line-height-heading2: 24px;
49
+ --line-height-heading3: 22px;
50
+ --line-height-heading4: 22px;
51
+ --line-height-heading5: 20px;
52
+ --radius-2xl: 1rem;
53
+ --radius-lg: 0.5rem;
54
+ --radius-md: 0.375rem;
55
+ --radius-panel: 1.1rem;
56
+ --radius-sm: 0.125rem;
57
+ --radius-xl: 0.75rem;
58
+ --space-lg: 1.5rem;
59
+ --space-md: 1rem;
60
+ --space-nav-x: 0.8125rem;
61
+ --space-sm: 0.5rem;
62
+ --space-xl: 2rem;
63
+ --space-xs: 0.25rem;
64
+ --tracking-normal: -0.13px;
65
+ --tracking-tight: -0.2px;
66
+ --tracking-wide: -0.56px;
67
+ --tracking-wider: -0.6px;
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pramen/cms-editor",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "Visual block/page editor for @pramen/cms — a standalone React SPA that talks to the CMS handlers over HTTP.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,14 +15,22 @@
15
15
  "scripts": {
16
16
  "build": "bun run scripts/build.ts",
17
17
  "dev": "bun run scripts/build.ts --watch",
18
+ "codegen": "bun run scripts/codegen.ts",
18
19
  "prepublishOnly": "bun run build"
19
20
  },
20
21
  "dependencies": {
22
+ "@buzola/router": "^0.0.12",
23
+ "@podoba/react": "^0.0.3",
24
+ "@podoba/tokens": "^0.0.3",
25
+ "@podoba/tailwind": "^0.0.3",
21
26
  "react": "^19.0.0",
22
27
  "react-dom": "^19.0.0"
23
28
  },
24
29
  "devDependencies": {
30
+ "@buzola/bun-plugin": "^0.0.12",
31
+ "@buzola/codegen": "^0.0.12",
25
32
  "@types/react": "^19.0.0",
26
- "@types/react-dom": "^19.0.0"
33
+ "@types/react-dom": "^19.0.0",
34
+ "tailwindcss": "^3.4.19"
27
35
  }
28
36
  }
@@ -0,0 +1,86 @@
1
+ // App-wide context for the routed editor: the HTTP client, the persisted config, the
2
+ // signed-in identity (`me`), and a global error sink rendered by the root layout. Also
3
+ // owns the pre-route config gate — when there's no base URL + token yet, it renders the
4
+ // Setup screen instead of mounting the router at all.
5
+
6
+ import { Button, Input } from "@podoba/react";
7
+ import { createContext, use, useEffect, useMemo, useState } from "react";
8
+ import { Api, loadConfig, saveConfig, type Config } from "./api";
9
+
10
+ export interface Me {
11
+ userId?: string;
12
+ roles?: string[];
13
+ [k: string]: unknown;
14
+ }
15
+
16
+ interface AppContextValue {
17
+ api: Api;
18
+ cfg: Config;
19
+ me: Me | null;
20
+ isAdmin: boolean;
21
+ error: string;
22
+ setError: (s: string) => void;
23
+ /** Sign out — drop the token so the config gate falls back to Setup. */
24
+ reconfigure: () => void;
25
+ }
26
+
27
+ const AppContext = createContext<AppContextValue | null>(null);
28
+
29
+ export function useApp(): AppContextValue {
30
+ const v = use(AppContext);
31
+ if (!v) throw new Error("useApp must be used within <AppProvider>");
32
+ return v;
33
+ }
34
+
35
+ export function AppProvider({ children }: { children: React.ReactNode }) {
36
+ const [cfg, setCfg] = useState<Config>(loadConfig());
37
+ const [me, setMe] = useState<Me | null>(null);
38
+ const [error, setError] = useState("");
39
+ const api = useMemo(() => new Api(cfg), [cfg]);
40
+ const configured = Boolean(cfg.baseUrl && cfg.token);
41
+
42
+ useEffect(() => {
43
+ if (!configured) return;
44
+ // `me` gates the Users tab + drives Settings — a failing call is fine (leaves it {}).
45
+ api.call<Me>("me").then(setMe).catch(() => setMe({}));
46
+ }, [api, configured]);
47
+
48
+ if (!configured) {
49
+ return <Setup cfg={cfg} onSave={(c) => { saveConfig(c); setCfg(c); }} />;
50
+ }
51
+
52
+ const value: AppContextValue = {
53
+ api,
54
+ cfg,
55
+ me,
56
+ isAdmin: (me?.roles ?? []).includes("admin"),
57
+ error,
58
+ setError,
59
+ reconfigure: () => { setMe(null); setError(""); setCfg({ ...cfg, token: "" }); },
60
+ };
61
+ return <AppContext value={value}>{children}</AppContext>;
62
+ }
63
+
64
+ function Setup({ cfg, onSave }: { cfg: Config; onSave: (c: Config) => void }) {
65
+ const [c, setC] = useState(cfg);
66
+ return (
67
+ <div className="mx-auto mt-[14vh] w-full max-w-[520px] px-6">
68
+ <div className="rounded-panel border border-border bg-surface-card px-10 py-8 shadow-[0_24px_60px_rgba(30,20,10,0.08)]">
69
+ <h1 className="mb-4 text-display text-fg">
70
+ pramen <span className="text-fg-subtle">· cms editor</span>
71
+ </h1>
72
+ <p className="mb-6 text-sm text-fg-muted">
73
+ Point at your Worker and paste an editor/reviewer JWT. CORS must allow this origin (<code>CORS_ORIGINS</code>).
74
+ </p>
75
+ <div className="flex flex-col gap-4">
76
+ <Input label="Worker base URL" value={c.baseUrl} onChange={(baseUrl) => setC({ ...c, baseUrl })} placeholder="https://your-worker.workers.dev" />
77
+ <Input label="Tenant" value={c.tenant} onChange={(tenant) => setC({ ...c, tenant })} placeholder="main" />
78
+ <Input label="Bearer token (editor or reviewer)" value={c.token} onChange={(token) => setC({ ...c, token })} placeholder="eyJ…" />
79
+ <Button className="mt-2 w-full" onPress={() => onSave(c)} isDisabled={!c.baseUrl || !c.token}>
80
+ Connect
81
+ </Button>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ );
86
+ }
package/src/app.css ADDED
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -0,0 +1,78 @@
1
+ // This file is auto-generated by @buzola/codegen. Do not edit.
2
+ /* eslint-disable */
3
+
4
+ import { lazy, type ComponentType } from 'react';
5
+ import { buildRouteTree } from '@buzola/router';
6
+ import type { RouteConfig } from '@buzola/router';
7
+
8
+ import Route0 from './routes/_layout';
9
+ const Route1 = () => import('./routes/home').then(m => ({ default: m.default.component })) as Promise<{ default: ComponentType }>;
10
+ const Route2 = () => import('./routes/media').then(m => ({ default: m.default.component })) as Promise<{ default: ComponentType }>;
11
+ const Route3 = () => import('./routes/page').then(m => ({ default: m.default.component })) as Promise<{ default: ComponentType }>;
12
+ const Route4 = () => import('./routes/settings').then(m => ({ default: m.default.component })) as Promise<{ default: ComponentType }>;
13
+ const Route5 = () => import('./routes/users').then(m => ({ default: m.default.component })) as Promise<{ default: ComponentType }>;
14
+ const Route6 = () => import('./routes/_404').then(m => ({ default: m.default.component })) as Promise<{ default: ComponentType }>;
15
+
16
+ // Module augmentation for type-safe page-centric routing
17
+ declare module '@buzola/router' {
18
+ interface BuzolaPageMap {
19
+ 'home': {};
20
+ 'media': {};
21
+ 'page': { pageId: string; tab?: string };
22
+ 'settings': {};
23
+ 'users': {};
24
+ }
25
+ }
26
+
27
+ // Page registry — page ID → route pattern
28
+ export const pageRegistry: Record<string, string> = {
29
+ 'home': '/',
30
+ 'media': '/media',
31
+ 'page': '/pages/:pageId',
32
+ 'settings': '/settings',
33
+ 'users': '/users',
34
+ };
35
+
36
+ const routeConfigs: RouteConfig[] = [
37
+ {
38
+ path: '/',
39
+ component: Route0,
40
+ isLayout: true,
41
+ children: [
42
+ {
43
+ path: '/home',
44
+ matchPath: '/',
45
+ component: lazy(Route1),
46
+ preload: Route1,
47
+ },
48
+ {
49
+ path: '/media',
50
+ component: lazy(Route2),
51
+ preload: Route2,
52
+ },
53
+ {
54
+ path: '/page',
55
+ matchPath: '/pages/:pageId',
56
+ component: lazy(Route3),
57
+ preload: Route3,
58
+ },
59
+ {
60
+ path: '/settings',
61
+ component: lazy(Route4),
62
+ preload: Route4,
63
+ },
64
+ {
65
+ path: '/users',
66
+ component: lazy(Route5),
67
+ preload: Route5,
68
+ },
69
+ {
70
+ path: '/:__notFound+',
71
+ component: lazy(Route6),
72
+ preload: Route6,
73
+ },
74
+ ],
75
+ },
76
+ ];
77
+
78
+ export const routes = buildRouteTree(routeConfigs);