@bygghammar/cms-platform 0.45.0 → 0.46.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.46.1
4
+
5
+ - Page builder host contract: `renderEditor` now also receives `initialBlocks` (the server baseline) alongside the live canvas.
6
+
7
+ ## 0.46.0
8
+
9
+ - Page builder is package-owned: new `backoffice/PageBuilderView` (rename, public URL, locale switch, save/publish, scheduling, version history, canvas and SEO slots) plus a `PageBuilderRoute` driven by a new `pages` host group.
10
+ - New `backoffice/SectionEditorView` + `SectionEditorRoute` (form/JSON shared-content editor with reset-to-default), driven by a new `sections` host group.
11
+ - New `backoffice/DashboardView` + `DashboardRoute` (shortcut cards and the translation sweep) and `backoffice/ResetPasswordView` + `ResetPasswordRoute`, driven by new `dashboard` and `authAdapters` host groups.
12
+ - New `config/define-site`: one validated `defineSite({ config, seo, content, trips })` contract, so a fresh site fails at startup with a readable message instead of half-way through a screen.
13
+ - New `server/schema-drift`: `SCHEMA_MIGRATIONS`, `SCHEMA_BASELINE` and `compareSchema()`; the version screen now shows the schema baseline, migration count and a drift warning.
14
+ - New `create-cms-site` scaffold (`npx create-cms-site --key mysite --name "My Site"`) emitting only the site-local files a fresh copy needs.
15
+
3
16
  ## 0.45.0
4
17
 
5
18
  - Version screen: added a live ticking "Current time" card showing the current date and time in UTC.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bygghammar/cms-platform",
3
- "version": "0.45.0",
3
+ "version": "0.46.1",
4
4
  "description": "Shared CMS platform: blocks, block types, i18n core, UI primitives and design-token contract used by every Bygghammar/Voyara site.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -159,7 +159,14 @@
159
159
  "./backoffice/routes": "./src/backoffice/routes/index.tsx",
160
160
  "./backoffice/routes/host": "./src/backoffice/routes/host.ts",
161
161
  "./backoffice/routes/media-forms": "./src/backoffice/routes/media-forms.tsx",
162
- "./backoffice/inspectors": "./src/backoffice/inspectors/index.tsx"
162
+ "./backoffice/inspectors": "./src/backoffice/inspectors/index.tsx",
163
+ "./backoffice/PageBuilderView": "./src/backoffice/PageBuilderView.tsx",
164
+ "./backoffice/SectionEditorView": "./src/backoffice/SectionEditorView.tsx",
165
+ "./backoffice/DashboardView": "./src/backoffice/DashboardView.tsx",
166
+ "./backoffice/ResetPasswordView": "./src/backoffice/ResetPasswordView.tsx",
167
+ "./backoffice/routes/pages": "./src/backoffice/routes/pages.tsx",
168
+ "./config/define-site": "./src/config/define-site.ts",
169
+ "./server/schema-drift": "./src/server/schema-drift.ts"
163
170
  },
164
171
  "dependencies": {
165
172
  "fflate": "^0.8.3"
@@ -233,6 +240,7 @@
233
240
  }
234
241
  },
235
242
  "bin": {
236
- "cms-platform-migrations": "scripts/sync-migrations.mjs"
243
+ "cms-platform-migrations": "scripts/sync-migrations.mjs",
244
+ "create-cms-site": "./scripts/create-cms-site.mjs"
237
245
  }
238
246
  }
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-cms-site — scaffolds the site-local files a fresh CMS copy needs.
4
+ *
5
+ * Everything else (backoffice screens, server cores, blocks, i18n core) comes
6
+ * from @bygghammar/cms-platform, so this emits only branding, styles, the site
7
+ * definition and the thin route wrappers.
8
+ *
9
+ * Usage: node scripts/create-cms-site.mjs --key mysite --name "My Site" [--dir .]
10
+ */
11
+
12
+ import { mkdir, writeFile, readdir } from "node:fs/promises";
13
+ import { existsSync } from "node:fs";
14
+ import path from "node:path";
15
+
16
+ const args = process.argv.slice(2);
17
+ function arg(name, fallback) {
18
+ const i = args.indexOf(`--${name}`);
19
+ return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
20
+ }
21
+
22
+ const key = arg("key");
23
+ const name = arg("name", key);
24
+ const root = path.resolve(arg("dir", "."));
25
+ const backofficePath = arg("backoffice", "/kontoret");
26
+ const locale = arg("locale", "sv");
27
+
28
+ if (!key) {
29
+ console.error('Usage: create-cms-site --key mysite --name "My Site" [--dir .]');
30
+ process.exit(1);
31
+ }
32
+
33
+ const files = {};
34
+
35
+ files["src/site.config.ts"] = `/**
36
+ * ${name} — the single code-level description of this site.
37
+ *
38
+ * Shared platform code reads this through the package instead of hardcoding a
39
+ * site name, locale set, backoffice path or module set.
40
+ */
41
+
42
+ import { defineSite } from "@bygghammar/cms-platform/config/define-site";
43
+
44
+ import { SITE_ROUTES } from "@/lib/i18n/routes";
45
+ import { site as siteContent } from "@/lib/site-content";
46
+
47
+ export const SITE = defineSite({
48
+ config: {
49
+ key: "${key}",
50
+ name: "${name}",
51
+ origin: import.meta.env["VITE_PUBLIC_SITE_URL"] ?? "https://${key}.example",
52
+ locales: ["${locale}"],
53
+ defaultLocale: "${locale}",
54
+ backofficePath: "${backofficePath}",
55
+ backofficeLocale: "en",
56
+ company: {
57
+ legalName: "",
58
+ locationName: "${name}",
59
+ street: "",
60
+ postCode: "",
61
+ city: "",
62
+ country: "",
63
+ orgNumber: "",
64
+ email: "",
65
+ phone: "",
66
+ },
67
+ brand: { sea: "#2f6f8f", brass: "#a8763e", ink: "#14181c" },
68
+ features: {
69
+ trips: false,
70
+ courses: false,
71
+ reviews: false,
72
+ email: true,
73
+ tracking: true,
74
+ backup: true,
75
+ domains: false,
76
+ forms: true,
77
+ },
78
+ },
79
+ seo: SITE_ROUTES,
80
+ content: siteContent,
81
+ });
82
+
83
+ export const SITE_CONFIG = SITE.config;
84
+ `;
85
+
86
+ files["src/lib/i18n/routes.ts"] = `/** Route + SEO registry: page key → slug per locale. */
87
+ export const SITE_ROUTES = {
88
+ home: { slug: { ${locale}: "" } },
89
+ };
90
+ `;
91
+
92
+ files["src/lib/site-content.ts"] = `/** Code defaults for the shared content sections. */
93
+ export const site = {
94
+ hero: { title: { ${locale}: "${name}" }, body: { ${locale}: "" } },
95
+ };
96
+ `;
97
+
98
+ files["src/styles.css"] = `@import "tailwindcss";
99
+ @import "@bygghammar/cms-platform/theme.css";
100
+
101
+ /* Site-specific brand tokens go here — never hardcode colours in components. */
102
+ `;
103
+
104
+ files["src/lib/backoffice/host.tsx"] = `/**
105
+ * Registers this site's server functions and site facts with the shared backoffice.
106
+ * The package owns the screens; this module says which server functions they call.
107
+ */
108
+
109
+ import { registerBackofficeHost } from "@bygghammar/cms-platform/backoffice/routes/host";
110
+
111
+ import { SITE_CONFIG } from "@/site.config";
112
+
113
+ // Fill in the server-function imports for each module you enable, following the
114
+ // contract in @bygghammar/cms-platform/backoffice/routes/host.
115
+ export const backofficeHost = registerBackofficeHost({
116
+ siteConfig: SITE_CONFIG,
117
+ locales: SITE_CONFIG.locales,
118
+ allRoles: ["superadmin", "admin", "editor"],
119
+ company: SITE_CONFIG.company as unknown as Record<string, string>,
120
+ fns: {} as never,
121
+ content: {} as never,
122
+ components: {} as never,
123
+ });
124
+ `;
125
+
126
+ files["README.SITE.md"] = `# ${name}
127
+
128
+ Site-local files only. Every backoffice screen, block, server core and i18n
129
+ helper comes from \`@bygghammar/cms-platform\`.
130
+
131
+ Next steps:
132
+
133
+ 1. \`npm install @bygghammar/cms-platform\`
134
+ 2. Apply the canonical schema: \`npx cms-platform-migrations sync\`
135
+ 3. Fill in \`src/lib/backoffice/host.tsx\` with this site's server functions.
136
+ 4. Add the thin route wrappers under \`src/routes/${backofficePath.replace("/", "")}.*.tsx\`.
137
+ 5. Set \`VITE_PUBLIC_SITE_URL\` per environment; never store it in the database.
138
+ `;
139
+
140
+ const existing = existsSync(root) ? await readdir(root) : [];
141
+ if (existing.includes("src") && !args.includes("--force")) {
142
+ console.error(`Refusing to overwrite an existing src/ in ${root} — pass --force.`);
143
+ process.exit(1);
144
+ }
145
+
146
+ for (const [rel, content] of Object.entries(files)) {
147
+ const target = path.join(root, rel);
148
+ await mkdir(path.dirname(target), { recursive: true });
149
+ await writeFile(target, content, "utf8");
150
+ console.log(`created ${rel}`);
151
+ }
152
+
153
+ console.log(`\nScaffolded ${Object.keys(files).length} files for "${name}" in ${root}.`);
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Backoffice dashboard — host-agnostic.
3
+ *
4
+ * Shortcut cards, the translation sweep panel and a link to the public site.
5
+ * A site supplies its cards and (optionally) its translation server call.
6
+ */
7
+
8
+ import { useState, type ComponentType } from "react";
9
+ import { ExternalLink, Languages } from "lucide-react";
10
+
11
+ /** One dashboard shortcut card. */
12
+ export type DashboardCard = {
13
+ to: string;
14
+ title: string;
15
+ body: string;
16
+ icon: ComponentType<{ className?: string }>;
17
+ };
18
+
19
+ /** Translation sweep modes offered on the dashboard. */
20
+ export type TranslateMode = "missing" | "stale" | "force";
21
+
22
+ /** Everything the dashboard needs from its host. */
23
+ export type DashboardDeps = {
24
+ cards: readonly DashboardCard[];
25
+ renderLink: (args: {
26
+ to: string;
27
+ className?: string;
28
+ children: React.ReactNode;
29
+ }) => React.ReactNode;
30
+ /** Target locale for the translation sweep; the panel is hidden without it. */
31
+ translateTarget?: string;
32
+ translate?: (args: {
33
+ target: string;
34
+ mode: TranslateMode;
35
+ }) => Promise<{ translated: number; updatedRows: number; skippedManual: number }>;
36
+ notify: { success: (m: string) => void; error: (m: string) => void };
37
+ };
38
+
39
+ /** Host-agnostic backoffice dashboard. */
40
+ export function DashboardView({ deps }: { deps: DashboardDeps }) {
41
+ const [busy, setBusy] = useState(false);
42
+ const [mode, setMode] = useState<TranslateMode>("stale");
43
+
44
+ const onTranslate = async () => {
45
+ if (!deps.translate || !deps.translateTarget) return;
46
+ setBusy(true);
47
+ try {
48
+ const r = await deps.translate({ target: deps.translateTarget, mode });
49
+ deps.notify.success(
50
+ `Done: ${r.translated} new/updated across ${r.updatedRows} rows. ` +
51
+ `Manual translations skipped: ${r.skippedManual}.`,
52
+ );
53
+ } catch (e) {
54
+ deps.notify.error(e instanceof Error ? e.message : "Translation failed");
55
+ } finally {
56
+ setBusy(false);
57
+ }
58
+ };
59
+
60
+ return (
61
+ <div className="p-8 md:p-12 w-full">
62
+ <div className="text-[0.7rem] uppercase tracking-[0.22em] text-ink/55">Dashboard</div>
63
+ <h1 className="mt-2 font-serif text-3xl md:text-4xl text-ink">Welcome to the backoffice.</h1>
64
+ <p className="mt-4 max-w-xl text-ink/65">
65
+ Everything you edit here is saved directly to the database and appears on the public site
66
+ after refresh. Customer-submitted forms and bookings are never touched by content changes.
67
+ </p>
68
+
69
+ <div className="mt-10 grid gap-4 md:grid-cols-3">
70
+ {deps.cards.map((c) => (
71
+ <div key={c.to}>
72
+ {deps.renderLink({
73
+ to: c.to,
74
+ className:
75
+ "group block border border-ink/10 bg-white p-6 hover:border-sea transition-colors",
76
+ children: (
77
+ <>
78
+ <c.icon className="size-6 text-ink/70 group-hover:text-sea" />
79
+ <div className="mt-4 font-serif text-lg text-ink">{c.title}</div>
80
+ <p className="mt-2 text-sm text-ink/60">{c.body}</p>
81
+ </>
82
+ ),
83
+ })}
84
+ </div>
85
+ ))}
86
+ </div>
87
+
88
+ {deps.translate && deps.translateTarget && (
89
+ <div className="mt-10 border border-ink/10 bg-white p-6">
90
+ <div className="flex items-start gap-4">
91
+ <Languages className="size-6 text-ink/70 shrink-0" />
92
+ <div className="flex-1">
93
+ <div className="font-serif text-lg text-ink">Translations</div>
94
+ <p className="mt-1 text-sm text-ink/60">
95
+ Fill missing translations for all CMS fields (sections, SEO). Manually edited fields
96
+ are always protected — the automation never touches them.
97
+ </p>
98
+ <div className="mt-4 flex flex-wrap items-center gap-3">
99
+ <label className="text-[0.65rem] uppercase tracking-[0.22em] text-ink/55">
100
+ Mode
101
+ </label>
102
+ <select
103
+ value={mode}
104
+ onChange={(e) => setMode(e.target.value as TranslateMode)}
105
+ disabled={busy}
106
+ className="border border-ink/20 bg-white px-3 py-2 text-xs uppercase tracking-[0.22em] text-ink"
107
+ >
108
+ <option value="missing">Missing only</option>
109
+ <option value="stale">Missing + stale (source changed)</option>
110
+ <option value="force">Force refresh all auto fields</option>
111
+ </select>
112
+ <button
113
+ type="button"
114
+ onClick={onTranslate}
115
+ disabled={busy}
116
+ className="inline-flex items-center gap-2 border border-ink bg-ink px-4 py-2 text-xs uppercase tracking-[0.22em] text-white hover:bg-sea hover:border-sea disabled:opacity-50"
117
+ >
118
+ {busy ? "Translating…" : "Run translation"}
119
+ </button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ )}
125
+
126
+ <div className="mt-10">
127
+ <a
128
+ href="/"
129
+ target="_blank"
130
+ rel="noreferrer"
131
+ className="inline-flex items-center gap-2 text-xs uppercase tracking-[0.22em] text-ink/70 hover:text-ink"
132
+ >
133
+ Open public website
134
+ <ExternalLink className="size-3.5" />
135
+ </a>
136
+ </div>
137
+ </div>
138
+ );
139
+ }