domma-cms 0.5.4 → 0.6.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/bin/cli.js CHANGED
@@ -37,21 +37,44 @@ const positional = args.filter(a => !a.startsWith('--'));
37
37
 
38
38
  if (flags.has('--help') || args.includes('-h')) {
39
39
  console.log(`
40
- Usage: npx domma-cms <project-name> [options]
40
+ Usage: npx domma-cms <command> [options]
41
41
 
42
- Options:
42
+ Commands:
43
+ <project-name> Scaffold a new Domma CMS project
44
+ update Update an existing project to the latest version
45
+
46
+ Scaffold options:
43
47
  --no-install Skip npm install
44
48
  --no-setup Skip the interactive setup wizard
45
49
  --no-seed Skip seeding default pages, forms, and collections
46
50
  --help Show this help message
47
51
 
48
- Example:
52
+ Update options:
53
+ --yes Skip confirmation prompt
54
+ --no-backup Skip creating a backup before updating
55
+ --no-install Skip npm install after updating
56
+ --dry-run Preview what would change without writing anything
57
+
58
+ Examples:
49
59
  npx domma-cms my-blog
50
60
  npx domma-cms my-blog --no-install --no-setup
61
+ npx domma-cms update
62
+ npx domma-cms update --dry-run
63
+ npx domma-cms update --yes --no-backup
51
64
  `);
52
65
  process.exit(0);
53
66
  }
54
67
 
68
+ // ---------------------------------------------------------------------------
69
+ // Route subcommands
70
+ // ---------------------------------------------------------------------------
71
+
72
+ if (positional[0] === 'update') {
73
+ const {default: runUpdate} = await import('./update.js');
74
+ await runUpdate(positional.slice(1), flags);
75
+ process.exit(0);
76
+ }
77
+
55
78
  const projectName = positional[0];
56
79
 
57
80
  if (!projectName) {
@@ -147,6 +170,10 @@ step('Copying plugins');
147
170
  copyDir('plugins');
148
171
  done();
149
172
 
173
+ step('Copying CLAUDE.md');
174
+ cpSync(path.join(PACKAGE_ROOT, 'CLAUDE.md'), path.join(target, 'CLAUDE.md'));
175
+ done();
176
+
150
177
  // ---------------------------------------------------------------------------
151
178
  // Reset data files inside copied plugins
152
179
  // ---------------------------------------------------------------------------
@@ -292,6 +319,7 @@ const GITIGNORE = `node_modules/
292
319
  content/users/
293
320
  content/media/
294
321
  *.log
322
+ .domma-backups/
295
323
  `;
296
324
 
297
325
  step('Writing .gitignore');
@@ -351,6 +379,19 @@ if (noSeed) {
351
379
  console.log('');
352
380
  }
353
381
 
382
+ // ---------------------------------------------------------------------------
383
+ // Write .domma version marker
384
+ // ---------------------------------------------------------------------------
385
+
386
+ step('Writing .domma marker');
387
+ const marker = {
388
+ cmsVersion: sourcePkg.version,
389
+ scaffoldedAt: new Date().toISOString(),
390
+ lastUpdatedAt: null
391
+ };
392
+ writeFileSync(path.join(target, '.domma'), JSON.stringify(marker, null, 2) + '\n', 'utf8');
393
+ done();
394
+
354
395
  // ---------------------------------------------------------------------------
355
396
  // Success
356
397
  // ---------------------------------------------------------------------------
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Domma CMS — Config Merge Utility
3
+ * Merges new keys from an upstream config into an existing user config,
4
+ * without ever overwriting values the user already has.
5
+ */
6
+
7
+ /**
8
+ * Deep-merge new keys from `upstream` into `existing`.
9
+ * Existing values are never modified — only missing keys are added.
10
+ *
11
+ * @param {object} existing - The user's current config object
12
+ * @param {object} upstream - The upstream (new version) config object
13
+ * @param {string} [_prefix] - Internal: key path prefix for reporting
14
+ * @returns {{ merged: object, added: string[] }} Merged object + list of added key paths
15
+ */
16
+ export function deepMergeNewKeys(existing, upstream, _prefix = '') {
17
+ const merged = {...existing};
18
+ const added = [];
19
+
20
+ for (const [key, upstreamVal] of Object.entries(upstream)) {
21
+ const fullKey = _prefix ? `${_prefix}.${key}` : key;
22
+
23
+ if (!(key in existing)) {
24
+ // Key is entirely missing — add it wholesale
25
+ merged[key] = upstreamVal;
26
+ added.push(fullKey);
27
+ } else if (
28
+ upstreamVal !== null &&
29
+ typeof upstreamVal === 'object' &&
30
+ !Array.isArray(upstreamVal) &&
31
+ typeof existing[key] === 'object' &&
32
+ existing[key] !== null &&
33
+ !Array.isArray(existing[key])
34
+ ) {
35
+ // Both sides are plain objects — recurse
36
+ const child = deepMergeNewKeys(existing[key], upstreamVal, fullKey);
37
+ merged[key] = child.merged;
38
+ added.push(...child.added);
39
+ }
40
+ // Otherwise: existing value wins — no action
41
+ }
42
+
43
+ return {merged, added};
44
+ }
package/bin/update.js ADDED
@@ -0,0 +1,547 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Domma CMS — Project Update
4
+ * Usage: npx domma-cms update [--yes] [--no-backup] [--no-install] [--dry-run]
5
+ *
6
+ * Run from inside an existing Domma CMS project directory.
7
+ * Pulls upstream changes from the installed CLI package into the project,
8
+ * preserving all user content, config, and custom plugins.
9
+ */
10
+
11
+ import {cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync,} from 'node:fs';
12
+ import {spawnSync} from 'node:child_process';
13
+ import path from 'node:path';
14
+ import {createInterface} from 'node:readline';
15
+ import {fileURLToPath} from 'node:url';
16
+ import {deepMergeNewKeys} from './lib/config-merge.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Paths
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function step(label) {
31
+ process.stdout.write(` ${label}…`);
32
+ }
33
+
34
+ function done(note = '') {
35
+ console.log(note ? ` done. ${note}` : ' done.');
36
+ }
37
+
38
+ function info(msg) {
39
+ console.log(` ${msg}`);
40
+ }
41
+
42
+ function warn(msg) {
43
+ console.log(` ⚠ ${msg}`);
44
+ }
45
+
46
+ function readJson(filePath) {
47
+ return JSON.parse(readFileSync(filePath, 'utf8'));
48
+ }
49
+
50
+ function writeJson(filePath, obj) {
51
+ writeFileSync(filePath, JSON.stringify(obj, null, 4) + '\n', 'utf8');
52
+ }
53
+
54
+ /**
55
+ * Prompt the user for a yes/no confirmation.
56
+ * @param {string} question
57
+ * @returns {Promise<boolean>}
58
+ */
59
+ function confirm(question) {
60
+ return new Promise(resolve => {
61
+ const rl = createInterface({input: process.stdin, output: process.stdout});
62
+ rl.question(` ${question} [y/N] `, answer => {
63
+ rl.close();
64
+ resolve(/^y(es)?$/i.test(answer.trim()));
65
+ });
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Recursively list all relative file paths under a directory.
71
+ * @param {string} dir - Absolute directory path
72
+ * @param {string} [base] - Base path for relative output (defaults to dir)
73
+ * @returns {string[]}
74
+ */
75
+ function listFiles(dir, base = dir) {
76
+ if (!existsSync(dir)) return [];
77
+ const result = [];
78
+ for (const entry of readdirSync(dir, {withFileTypes: true})) {
79
+ const full = path.join(dir, entry.name);
80
+ if (entry.isDirectory()) {
81
+ result.push(...listFiles(full, base));
82
+ } else {
83
+ result.push(path.relative(base, full));
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Upstream dirs that are fully replaced on update
91
+ // ---------------------------------------------------------------------------
92
+
93
+ const UPSTREAM_DIRS = ['server', 'admin', 'public', 'scripts'];
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Structural config files that get new-key merging (never full replace)
97
+ // ---------------------------------------------------------------------------
98
+
99
+ const STRUCTURAL_CONFIGS = [
100
+ 'config/server.json',
101
+ 'config/auth.json',
102
+ 'config/content.json',
103
+ 'config/presets.json',
104
+ ];
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Main update function
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /**
111
+ * @param {string[]} _positional - Remaining positional args (unused)
112
+ * @param {Set<string>} flags - Parsed CLI flags
113
+ */
114
+ export default async function update(_positional, flags) {
115
+ const isDryRun = flags.has('--dry-run');
116
+ const skipConfirm = flags.has('--yes');
117
+ const skipBackup = flags.has('--no-backup');
118
+ const skipInstall = flags.has('--no-install');
119
+
120
+ const cwd = process.cwd();
121
+
122
+ // -------------------------------------------------------------------------
123
+ // 1. Detect project
124
+ // -------------------------------------------------------------------------
125
+
126
+ console.log('');
127
+ console.log(' ┌──────────────────────────────────────────┐');
128
+ console.log(' │ │');
129
+ console.log(' │ Domma CMS — Project Updater │');
130
+ console.log(' │ │');
131
+ console.log(' └──────────────────────────────────────────┘');
132
+ console.log('');
133
+
134
+ // Refuse to run inside the CLI package itself
135
+ if (path.resolve(cwd) === path.resolve(PACKAGE_ROOT)) {
136
+ console.error(' ✗ Run this command from inside your scaffolded project, not the CLI package.\n');
137
+ process.exit(1);
138
+ }
139
+
140
+ const requiredFiles = ['package.json', 'server/server.js', 'config'];
141
+ for (const f of requiredFiles) {
142
+ if (!existsSync(path.join(cwd, f))) {
143
+ console.error(` ✗ Not a Domma CMS project directory (missing ${f}).\n`);
144
+ console.error(' Run this command from inside your project folder.\n');
145
+ process.exit(1);
146
+ }
147
+ }
148
+
149
+ // Scaffolded projects are always private — reject if not
150
+ try {
151
+ const cwdPkg = readJson(path.join(cwd, 'package.json'));
152
+ if (!cwdPkg.private) {
153
+ console.error(' ✗ This does not appear to be a scaffolded Domma CMS project (package.json is not private).\n');
154
+ process.exit(1);
155
+ }
156
+ } catch { /* malformed package.json — let it proceed */
157
+ }
158
+
159
+ // -------------------------------------------------------------------------
160
+ // 2. Read version information
161
+ // -------------------------------------------------------------------------
162
+
163
+ const markerPath = path.join(cwd, '.domma');
164
+ let installedVersion = 'unknown';
165
+ let markerData = null;
166
+
167
+ if (existsSync(markerPath)) {
168
+ try {
169
+ markerData = readJson(markerPath);
170
+ installedVersion = markerData.cmsVersion ?? 'unknown';
171
+ } catch {
172
+ warn('Could not read .domma marker — treating as unknown version.');
173
+ }
174
+ } else {
175
+ warn('No .domma marker found. This project was scaffolded before version tracking was introduced.');
176
+ warn('The updater will proceed, but cannot guarantee what was previously installed.');
177
+ console.log('');
178
+ }
179
+
180
+ const sourcePkg = readJson(path.join(PACKAGE_ROOT, 'package.json'));
181
+ const incomingVersion = sourcePkg.version;
182
+
183
+ info(`Current: ${installedVersion}`);
184
+ info(`Available: ${incomingVersion}`);
185
+ console.log('');
186
+
187
+ if (installedVersion !== 'unknown' && installedVersion === incomingVersion) {
188
+ info('✓ Already up to date.');
189
+ console.log('');
190
+ process.exit(0);
191
+ }
192
+
193
+ // -------------------------------------------------------------------------
194
+ // 3. Dry-run mode — preview changes and exit
195
+ // -------------------------------------------------------------------------
196
+
197
+ if (isDryRun) {
198
+ info('Dry run — no files will be modified.\n');
199
+
200
+ info('Upstream directories that would be replaced:');
201
+ for (const dir of UPSTREAM_DIRS) {
202
+ info(` → ${dir}/`);
203
+ }
204
+ console.log('');
205
+
206
+ info('Structural configs that would be merged (new keys only):');
207
+ for (const cfg of STRUCTURAL_CONFIGS) {
208
+ if (existsSync(path.join(cwd, cfg))) {
209
+ const existing = readJson(path.join(cwd, cfg));
210
+ const upstream = existsSync(path.join(PACKAGE_ROOT, cfg))
211
+ ? readJson(path.join(PACKAGE_ROOT, cfg))
212
+ : {};
213
+ const {added} = deepMergeNewKeys(existing, upstream);
214
+ if (added.length > 0) {
215
+ info(` ${cfg}: would add ${added.length} key(s): ${added.join(', ')}`);
216
+ } else {
217
+ info(` ${cfg}: no new keys`);
218
+ }
219
+ }
220
+ }
221
+ console.log('');
222
+
223
+ info('Plugins that would be updated:');
224
+ const pluginsDirSrc = path.join(PACKAGE_ROOT, 'plugins');
225
+ const pluginsDirDst = path.join(cwd, 'plugins');
226
+ if (existsSync(pluginsDirSrc)) {
227
+ const upstreamPlugins = readdirSync(pluginsDirSrc, {withFileTypes: true})
228
+ .filter(e => e.isDirectory())
229
+ .map(e => e.name);
230
+ for (const name of upstreamPlugins) {
231
+ const exists = existsSync(path.join(pluginsDirDst, name));
232
+ info(` → plugins/${name}/ (${exists ? 'update' : 'add'})`);
233
+ }
234
+ }
235
+ console.log('');
236
+
237
+ info('Package.json: CMS deps would be replaced; user-added deps preserved.');
238
+ console.log('');
239
+ process.exit(0);
240
+ }
241
+
242
+ // -------------------------------------------------------------------------
243
+ // 4. Confirmation prompt
244
+ // -------------------------------------------------------------------------
245
+
246
+ if (!skipConfirm) {
247
+ const confirmed = await confirm(`Update project from ${installedVersion} to ${incomingVersion}?`);
248
+ if (!confirmed) {
249
+ info('Update cancelled.');
250
+ console.log('');
251
+ process.exit(0);
252
+ }
253
+ console.log('');
254
+ }
255
+
256
+ // -------------------------------------------------------------------------
257
+ // 5. Backup
258
+ // -------------------------------------------------------------------------
259
+
260
+ let backupPath = null;
261
+
262
+ if (!skipBackup) {
263
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
264
+ backupPath = path.join(cwd, '.domma-backups', timestamp);
265
+ step('Creating backup');
266
+ mkdirSync(backupPath, {recursive: true});
267
+
268
+ // Back up upstream dirs
269
+ for (const dir of UPSTREAM_DIRS) {
270
+ const src = path.join(cwd, dir);
271
+ if (existsSync(src)) {
272
+ cpSync(src, path.join(backupPath, dir), {recursive: true});
273
+ }
274
+ }
275
+
276
+ // Back up package.json and structural configs
277
+ for (const f of ['package.json', ...STRUCTURAL_CONFIGS]) {
278
+ const src = path.join(cwd, f);
279
+ if (existsSync(src)) {
280
+ const dest = path.join(backupPath, f);
281
+ mkdirSync(path.dirname(dest), {recursive: true});
282
+ cpSync(src, dest);
283
+ }
284
+ }
285
+
286
+ // Back up plugins dir (code only, not data)
287
+ const pluginsDir = path.join(cwd, 'plugins');
288
+ if (existsSync(pluginsDir)) {
289
+ cpSync(pluginsDir, path.join(backupPath, 'plugins'), {recursive: true});
290
+ }
291
+
292
+ done();
293
+ } else {
294
+ warn('Skipping backup (--no-backup).');
295
+ }
296
+
297
+ // -------------------------------------------------------------------------
298
+ // 6. Replace upstream directories
299
+ // -------------------------------------------------------------------------
300
+
301
+ step('Replacing upstream code');
302
+ for (const dir of UPSTREAM_DIRS) {
303
+ const dest = path.join(cwd, dir);
304
+ const src = path.join(PACKAGE_ROOT, dir);
305
+ if (!existsSync(src)) continue;
306
+ if (existsSync(dest)) rmSync(dest, {recursive: true, force: true});
307
+ cpSync(src, dest, {recursive: true});
308
+ }
309
+ // Replace CLAUDE.md (CMS-owned guidance file)
310
+ const claudeMdSrc = path.join(PACKAGE_ROOT, 'CLAUDE.md');
311
+ if (existsSync(claudeMdSrc)) {
312
+ cpSync(claudeMdSrc, path.join(cwd, 'CLAUDE.md'));
313
+ }
314
+ done();
315
+
316
+ // -------------------------------------------------------------------------
317
+ // 7. Update plugins
318
+ // -------------------------------------------------------------------------
319
+
320
+ step('Updating plugins');
321
+ const pluginsDirSrc = path.join(PACKAGE_ROOT, 'plugins');
322
+ const pluginsDirDst = path.join(cwd, 'plugins');
323
+ const newPlugins = [];
324
+
325
+ if (existsSync(pluginsDirSrc)) {
326
+ mkdirSync(pluginsDirDst, {recursive: true});
327
+ const upstreamPlugins = readdirSync(pluginsDirSrc, {withFileTypes: true})
328
+ .filter(e => e.isDirectory())
329
+ .map(e => e.name);
330
+
331
+ for (const name of upstreamPlugins) {
332
+ const srcPlugin = path.join(pluginsDirSrc, name);
333
+ const dstPlugin = path.join(pluginsDirDst, name);
334
+ const isNew = !existsSync(dstPlugin);
335
+
336
+ if (isNew) {
337
+ // Brand-new plugin — copy wholesale, apply scaffold.reset
338
+ cpSync(srcPlugin, dstPlugin, {recursive: true});
339
+ try {
340
+ const manifest = readJson(path.join(dstPlugin, 'plugin.json'));
341
+ for (const {path: relPath, content} of (manifest.scaffold?.reset ?? [])) {
342
+ const absPath = path.join(dstPlugin, relPath);
343
+ mkdirSync(path.dirname(absPath), {recursive: true});
344
+ writeFileSync(absPath, content + '\n', 'utf8');
345
+ }
346
+ } catch { /* no manifest — skip reset */
347
+ }
348
+ newPlugins.push(name);
349
+ } else {
350
+ // Existing plugin — identify paths to preserve
351
+ let preservePaths = new Set(['data']);
352
+ try {
353
+ const manifest = readJson(path.join(srcPlugin, 'plugin.json'));
354
+ for (const {path: relPath} of (manifest.scaffold?.reset ?? [])) {
355
+ preservePaths.add(relPath);
356
+ }
357
+ } catch { /* no manifest */
358
+ }
359
+
360
+ // Back up preserved paths from current install
361
+ const preserved = new Map();
362
+ for (const rel of preservePaths) {
363
+ const abs = path.join(dstPlugin, rel);
364
+ if (!existsSync(abs)) continue;
365
+
366
+ if (statSync(abs).isDirectory()) {
367
+ // Preserve all files under this directory
368
+ for (const f of listFiles(abs, dstPlugin)) {
369
+ preserved.set(f, readFileSync(path.join(dstPlugin, f)));
370
+ }
371
+ } else {
372
+ // scaffold.reset entry — preserve the single file
373
+ preserved.set(rel, readFileSync(abs));
374
+ }
375
+ }
376
+
377
+ // Replace plugin directory with upstream
378
+ rmSync(dstPlugin, {recursive: true, force: true});
379
+ cpSync(srcPlugin, dstPlugin, {recursive: true});
380
+
381
+ // Restore preserved files
382
+ for (const [rel, content] of preserved) {
383
+ const abs = path.join(dstPlugin, rel);
384
+ mkdirSync(path.dirname(abs), {recursive: true});
385
+ writeFileSync(abs, content);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ done();
391
+
392
+ // -------------------------------------------------------------------------
393
+ // 8. Register new plugins in plugins.json (disabled by default)
394
+ // -------------------------------------------------------------------------
395
+
396
+ const pluginsJsonPath = path.join(cwd, 'config/plugins.json');
397
+ if (newPlugins.length > 0 && existsSync(pluginsJsonPath)) {
398
+ try {
399
+ const pluginsCfg = readJson(pluginsJsonPath);
400
+ for (const name of newPlugins) {
401
+ if (!(name in pluginsCfg)) {
402
+ pluginsCfg[name] = {enabled: false, settings: {}};
403
+ }
404
+ }
405
+ writeJson(pluginsJsonPath, pluginsCfg);
406
+ } catch { /* if plugins.json is malformed, skip */
407
+ }
408
+ }
409
+
410
+ // -------------------------------------------------------------------------
411
+ // 9. Merge structural configs (new keys only)
412
+ // -------------------------------------------------------------------------
413
+
414
+ step('Merging structural configs');
415
+ const mergedKeys = {};
416
+
417
+ for (const cfgFile of STRUCTURAL_CONFIGS) {
418
+ const dstPath = path.join(cwd, cfgFile);
419
+ const srcPath = path.join(PACKAGE_ROOT, cfgFile);
420
+ if (!existsSync(dstPath) || !existsSync(srcPath)) continue;
421
+
422
+ try {
423
+ const existing = readJson(dstPath);
424
+ const upstream = readJson(srcPath);
425
+ const {merged, added} = deepMergeNewKeys(existing, upstream);
426
+ if (added.length > 0) {
427
+ writeJson(dstPath, merged);
428
+ mergedKeys[cfgFile] = added;
429
+ }
430
+ } catch { /* malformed JSON — skip */
431
+ }
432
+ }
433
+ done();
434
+
435
+ // -------------------------------------------------------------------------
436
+ // 10. Update package.json: replace CMS deps, preserve user-added deps
437
+ // -------------------------------------------------------------------------
438
+
439
+ step('Updating package.json');
440
+ const projectPkgPath = path.join(cwd, 'package.json');
441
+ try {
442
+ const projectPkg = readJson(projectPkgPath);
443
+ const cmsDeps = sourcePkg.dependencies ?? {};
444
+ const cmsOptional = sourcePkg.optionalDependencies ?? {};
445
+ const userDeps = projectPkg.dependencies ?? {};
446
+
447
+ // Preserve deps the user added (not in CMS deps at time of scaffold)
448
+ // We identify "user-added" as: present in project but not in the new upstream CMS deps
449
+ const userAddedDeps = {};
450
+ for (const [pkg, ver] of Object.entries(userDeps)) {
451
+ if (!(pkg in cmsDeps) && !(pkg in cmsOptional)) {
452
+ userAddedDeps[pkg] = ver;
453
+ }
454
+ }
455
+
456
+ // Replace CMS scripts, but never copy prepublishOnly
457
+ const newScripts = {...sourcePkg.scripts};
458
+ delete newScripts.prepublishOnly;
459
+
460
+ projectPkg.dependencies = {...cmsDeps, ...userAddedDeps};
461
+ projectPkg.optionalDependencies = cmsOptional;
462
+ projectPkg.scripts = newScripts;
463
+
464
+ writeFileSync(projectPkgPath, JSON.stringify(projectPkg, null, 2) + '\n', 'utf8');
465
+ } catch { /* malformed package.json — skip */
466
+ }
467
+ done();
468
+
469
+ // -------------------------------------------------------------------------
470
+ // 11. Ensure .domma-backups/ is in .gitignore
471
+ // -------------------------------------------------------------------------
472
+
473
+ const gitignorePath = path.join(cwd, '.gitignore');
474
+ const backupEntry = '.domma-backups/';
475
+ if (existsSync(gitignorePath)) {
476
+ const current = readFileSync(gitignorePath, 'utf8');
477
+ if (!current.includes(backupEntry)) {
478
+ writeFileSync(gitignorePath, current.trimEnd() + '\n' + backupEntry + '\n', 'utf8');
479
+ }
480
+ } else {
481
+ writeFileSync(gitignorePath, backupEntry + '\n', 'utf8');
482
+ }
483
+
484
+ // -------------------------------------------------------------------------
485
+ // 12. Write updated .domma marker
486
+ // -------------------------------------------------------------------------
487
+
488
+ step('Updating .domma marker');
489
+ const newMarker = {
490
+ cmsVersion: incomingVersion,
491
+ scaffoldedAt: markerData?.scaffoldedAt ?? null,
492
+ lastUpdatedAt: new Date().toISOString(),
493
+ };
494
+ writeFileSync(markerPath, JSON.stringify(newMarker, null, 2) + '\n', 'utf8');
495
+ done();
496
+
497
+ // -------------------------------------------------------------------------
498
+ // 13. npm install
499
+ // -------------------------------------------------------------------------
500
+
501
+ console.log('');
502
+
503
+ if (skipInstall) {
504
+ warn('Skipping npm install (--no-install).');
505
+ } else {
506
+ info('Running npm install…');
507
+ console.log('');
508
+ const result = spawnSync('npm', ['install'], {cwd, stdio: 'inherit'});
509
+ if (result.status !== 0) {
510
+ console.error('\n ✗ npm install failed.\n');
511
+ process.exit(result.status ?? 1);
512
+ }
513
+ console.log('');
514
+ }
515
+
516
+ // -------------------------------------------------------------------------
517
+ // 14. Summary
518
+ // -------------------------------------------------------------------------
519
+
520
+ console.log(' ┌──────────────────────────────────────────┐');
521
+ console.log(' │ ✓ Update complete! │');
522
+ console.log(' └──────────────────────────────────────────┘');
523
+ console.log('');
524
+
525
+ info(` ${installedVersion} → ${incomingVersion}`);
526
+ console.log('');
527
+
528
+ if (backupPath) {
529
+ info(`Backup saved to: .domma-backups/${path.basename(backupPath)}/`);
530
+ }
531
+
532
+ if (newPlugins.length > 0) {
533
+ info(`New plugins added (disabled by default): ${newPlugins.join(', ')}`);
534
+ }
535
+
536
+ const allMergedKeys = Object.entries(mergedKeys);
537
+ if (allMergedKeys.length > 0) {
538
+ info('New config keys added:');
539
+ for (const [file, keys] of allMergedKeys) {
540
+ info(` ${file}: ${keys.join(', ')}`);
541
+ }
542
+ }
543
+
544
+ console.log('');
545
+ info('Restart your dev server to apply changes: npm run dev');
546
+ console.log('');
547
+ }
@@ -1,6 +1,10 @@
1
- {
2
- "port": 4096,
3
- "host": "0.0.0.0",
4
- "cors": { "origin": false },
5
- "uploads": { "maxFileSize": 10485760 }
6
- }
1
+ {
2
+ "port": 4096,
3
+ "host": "0.0.0.0",
4
+ "cors": {
5
+ "origin": false
6
+ },
7
+ "uploads": {
8
+ "maxFileSize": 10485760
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.5.4",
3
+ "version": "0.6.1",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -22,7 +22,8 @@
22
22
  "plugins/",
23
23
  "scripts/",
24
24
  "docs/",
25
- "CHANGELOG.md"
25
+ "CHANGELOG.md",
26
+ "CLAUDE.md"
26
27
  ],
27
28
  "scripts": {
28
29
  "build": "node scripts/build.js",
@@ -1 +1 @@
1
- body,button,input,select,textarea{font-family:Roboto,sans-serif}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:2rem;font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:1.5rem}.page-body h3{font-size:1.25rem}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}h3.accordion-header{margin:0}.accordion-button{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font:inherit}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.site-main:has(.page-body>.hero-breakout:first-child){padding-top:0}body[data-layout=landing]>.site-main{padding-top:0}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.page-body .card-header-icon-inline{display:flex;align-items:center;gap:.6rem}.page-body .card-header-icon-inline [data-icon]{flex-shrink:0;line-height:0}.page-body .card-header-icon-inline [data-icon] svg,.page-body .card-header-icon-inline>svg{display:block;width:1.25rem;height:1.25rem}.page-body .card-header-icon-stacked{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding-top:.25rem}.page-body .card-header-icon-stacked [data-icon],.page-body .card-header-icon-stacked svg{width:2rem;height:2rem}.hero.hero-dark{background:linear-gradient(135deg,#1f2937,#111827);color:#e2e8f0}.hero .hero-content{position:relative;z-index:2}.hero.hero-left .hero-content{text-align:left;align-items:flex-start;max-width:62%}@media(max-width:768px){.hero.hero-left .hero-content{max-width:100%}}.hero .hero-cta{display:flex;gap:.85rem;flex-wrap:wrap;margin-top:1.75rem}.hero .hero-cta a{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.35rem;border-radius:6px;font-size:.95rem;font-weight:500;text-decoration:none;transition:background .2s ease,border-color .2s ease,transform .15s ease,box-shadow .2s ease}.hero .hero-cta a:first-child{background:#ffffffeb;color:#111;border:1px solid transparent}.hero .hero-cta a:first-child:hover{background:#fff;box-shadow:0 4px 16px #00000040;transform:translateY(-2px)}.hero .hero-cta a:last-child{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}.hero .hero-cta a:last-child:hover{border-color:#ffffffbf;background:#ffffff14;transform:translateY(-2px)}.hero .hero-label{display:inline-block;margin-bottom:.9rem;padding:.2rem .8rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:#ffffffb3;border:1px solid rgba(255,255,255,.22)}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.dm-breadcrumbs{position:fixed;z-index:200;display:inline-flex;align-items:center;gap:.2rem;padding:.3rem .8rem;border-radius:999px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:#00000047;border:1px solid rgba(255,255,255,.11);box-shadow:0 2px 10px #00000038;font-size:.72rem;font-weight:500;letter-spacing:.01em;line-height:1.4;max-width:calc(100vw - 2rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-breadcrumbs .dm-breadcrumbs-item{color:#ffffffa6}.dm-breadcrumbs .dm-breadcrumbs-link{display:inline-flex;align-items:center;gap:.25rem;color:#ffffff8c;text-decoration:none;transition:color .15s}.dm-breadcrumbs .dm-breadcrumbs-home-icon{flex-shrink:0;vertical-align:middle}.dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#fffffff2}.dm-breadcrumbs .dm-breadcrumbs-current{color:#ffffffeb;font-weight:600}.dm-breadcrumbs .dm-breadcrumbs-separator{color:#ffffff47;font-size:.8em;line-height:1;margin:0 .05rem}[data-mode=light] .dm-breadcrumbs{background:#ffffff8c;border-color:#00000012;box-shadow:0 2px 10px #00000014}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-item,[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link{color:#0000008c}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#000000e6}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-current{color:#000000d9}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-separator{color:#00000040}.dm-collection-display{margin:1.5rem 0}.dm-collection-list{display:flex;flex-direction:column;gap:0}.dm-collection-list-item{padding:1rem 0;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08))}.dm-collection-list-item:last-child{border-bottom:none}.dm-collection-list-item strong{display:block;font-size:1rem;margin-bottom:.25rem}.dm-collection-list-item p{margin:0;color:var(--text-muted, #888);font-size:.9rem}.dm-collection-empty p{color:var(--text-muted, #888);font-style:italic}
1
+ body,button,input,select,textarea{font-family:Roboto,sans-serif}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:2rem;font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:1.5rem}.page-body h3{font-size:1.25rem}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}h3.accordion-header{margin:0}.accordion-button{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font:inherit}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.site-main:has(.page-body>.hero-breakout:first-child){padding-top:0}body[data-layout=landing]>.site-main{padding-top:0}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.page-body .card-header-icon-inline{display:flex;align-items:center;gap:.6rem}.page-body .card-header-icon-inline [data-icon]{flex-shrink:0;line-height:0}.page-body .card-header-icon-inline [data-icon] svg,.page-body .card-header-icon-inline>svg{display:block;width:1.25rem;height:1.25rem}.page-body .card-header-icon-stacked{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding-top:.25rem}.page-body .card-header-icon-stacked [data-icon],.page-body .card-header-icon-stacked svg{width:2rem;height:2rem}.hero.hero-dark{background:linear-gradient(135deg,#1f2937,#111827);color:#e2e8f0}.hero .hero-content{position:relative;z-index:2}.hero.hero-left .hero-content{text-align:left;align-items:flex-start;max-width:62%}@media(max-width:768px){.hero.hero-left .hero-content{max-width:100%}}.hero .hero-cta{display:flex;gap:.85rem;flex-wrap:wrap;margin-top:1.75rem}.hero .hero-cta a{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.35rem;border-radius:6px;font-size:.95rem;font-weight:500;text-decoration:none;transition:background .2s ease,border-color .2s ease,transform .15s ease,box-shadow .2s ease}.hero .hero-cta a:first-child{background:#ffffffeb;color:#111;border:1px solid transparent}.hero .hero-cta a:first-child:hover{background:#fff;box-shadow:0 4px 16px #00000040;transform:translateY(-2px)}.hero .hero-cta a:last-child{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}.hero .hero-cta a:last-child:hover{border-color:#ffffffbf;background:#ffffff14;transform:translateY(-2px)}.hero .hero-label{display:inline-block;margin-bottom:.9rem;padding:.2rem .8rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:#ffffffb3;border:1px solid rgba(255,255,255,.22)}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.dm-breadcrumbs{position:fixed;z-index:200;display:inline-flex;align-items:center;gap:.2rem;padding:.3rem .8rem;border-radius:999px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:#00000047;border:1px solid rgba(255,255,255,.11);box-shadow:0 2px 10px #00000038;font-size:.72rem;font-weight:500;letter-spacing:.01em;line-height:1.4;max-width:calc(100vw - 2rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-breadcrumbs .dm-breadcrumbs-item{color:#ffffffa6}.dm-breadcrumbs .dm-breadcrumbs-link{display:inline-flex;align-items:center;gap:.25rem;color:#ffffff8c;text-decoration:none;transition:color .15s}.dm-breadcrumbs .dm-breadcrumbs-home-icon{flex-shrink:0;vertical-align:middle}.dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#fffffff2}.dm-breadcrumbs .dm-breadcrumbs-current{color:#ffffffeb;font-weight:600}.dm-breadcrumbs .dm-breadcrumbs-separator{color:#ffffff47;font-size:.8em;line-height:1;margin:0 .05rem}[data-mode=light] .dm-breadcrumbs{background:#ffffff8c;border-color:#00000012;box-shadow:0 2px 10px #00000014}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-item,[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link{color:#0000008c}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#000000e6}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-current{color:#000000d9}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-separator{color:#00000040}.dm-collection-display{margin:1.5rem 0}.dm-collection-list{display:flex;flex-direction:column;gap:0}.dm-collection-list-item{padding:1rem 0;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08))}.dm-collection-list-item:last-child{border-bottom:none}.dm-collection-list-item strong{display:block;font-size:1rem;margin-bottom:.25rem}.dm-collection-list-item p{margin:0;color:var(--text-muted, #888);font-size:.9rem}.dm-collection-empty p{color:var(--text-muted, #888);font-style:italic}.hero-gradient-purple{background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:#1e1b4b}.hero-gradient-blue{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e3a5f}.hero-gradient-green{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#064e3b}.hero-gradient-sunset{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#78350f}.hero-gradient-ocean{background:linear-gradient(135deg,#e0f2fe,#bae6fd);color:#0c4a6e}.hero-gradient-rose{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#831843}.hero-gradient-forest{background:linear-gradient(135deg,#dcfce7,#bbf7d0);color:#14532d}.hero-gradient-night{background:linear-gradient(135deg,#334155,#1e293b);color:#e2e8f0}.hero-gradient-ocean-light{background:linear-gradient(135deg,#e0f2fe,#caf0f8);color:#1e293b}.hero-gradient-ocean-dark{background:linear-gradient(135deg,#0c4a6e,#164e63);color:#e2e8f0}.hero-gradient-forest-light{background:linear-gradient(135deg,#d1fae5,#c6f6dc);color:#1e293b}.hero-gradient-forest-dark{background:linear-gradient(135deg,#1a4731,#166534);color:#e2e8f0}.hero-gradient-sunset-light{background:linear-gradient(135deg,#fde8d8,#fddcc9);color:#1e293b}.hero-gradient-sunset-dark{background:linear-gradient(135deg,#6b3727,#7c4036);color:#f5ede8}.hero-gradient-royal-light{background:linear-gradient(135deg,#e8f0fd,#dce8fc);color:#1e293b}.hero-gradient-royal-dark{background:linear-gradient(135deg,#1e3465,#263d7a);color:#e2e8f0}.hero-gradient-lemon-light{background:linear-gradient(135deg,#fefce8,#fef9c3);color:#1e293b}.hero-gradient-lemon-dark{background:linear-gradient(135deg,#5c4d1a,#6b5920);color:#fefce8}.hero-gradient-silver-light{background:linear-gradient(135deg,#f1f5f9,#e2e8f0);color:#1e293b}.hero-gradient-silver-dark{background:linear-gradient(135deg,#2d3748,#374151);color:#e2e8f0}.hero-gradient-charcoal-light{background:linear-gradient(135deg,#eceff1,#e1e7eb);color:#1e293b}.hero-gradient-charcoal-dark{background:linear-gradient(135deg,#2c3843,#374451);color:#e2e8f0}.hero-gradient-christmas-light{background:linear-gradient(135deg,#fde8ea,#fdd5d8);color:#1e293b}.hero-gradient-christmas-dark{background:linear-gradient(135deg,#5c0f1d,#7a1525);color:#fde8ea}.hero-gradient-unicorn-light{background:linear-gradient(135deg,#f5e8fd,#edd6fb);color:#1e293b}.hero-gradient-unicorn-dark{background:linear-gradient(135deg,#3d1a5a,#4a2068);color:#f5e8fd}.hero-gradient-dreamy-light{background:linear-gradient(135deg,#f5ede8,#eeddd4);color:#1e293b}.hero-gradient-dreamy-dark{background:linear-gradient(135deg,#3d2820,#503328);color:#f5ede8}.hero-gradient-grayve-light{background:linear-gradient(135deg,#e0f7f9,#cbf2f5);color:#1e293b}.hero-gradient-grayve-dark{background:linear-gradient(135deg,#00363d,#00444d);color:#e0f7f9}.hero-gradient-mint-light{background:linear-gradient(135deg,#d8f5ea,#c5efdd);color:#1e293b}.hero-gradient-mint-dark{background:linear-gradient(135deg,#134d33,#195f3f);color:#d8f5ea}.hero-gradient-wedding-light{background:linear-gradient(135deg,#faf3e0,#f5e9c7);color:#1e293b}.hero-gradient-wedding-dark{background:linear-gradient(135deg,#5c4418,#6f5320);color:#faf3e0}
@@ -964,8 +964,14 @@ function processTableBlocks(markdown) {
964
964
  * title - Hero heading (.hero-title)
965
965
  * tagline - Subtitle text (.hero-subtitle)
966
966
  * size - "sm", "lg", "full" → .hero-sm / .hero-lg / .hero-full
967
- * variant - "dark", "primary", "gradient-blue", "gradient-purple",
968
- * "gradient-sunset", "gradient-ocean" → .hero-{variant}
967
+ * variant - "dark", "primary",
968
+ * upstream 8: "gradient-purple", "gradient-blue", "gradient-green",
969
+ * "gradient-sunset", "gradient-ocean", "gradient-rose",
970
+ * "gradient-forest", "gradient-night"
971
+ * theme-specific (26): "gradient-{theme}-{mode}" where theme is one of
972
+ * ocean, forest, sunset, royal, lemon, silver, charcoal, christmas,
973
+ * unicorn, dreamy, grayve, mint, wedding — and mode is light or dark
974
+ * → .hero-{variant}
969
975
  * image - URL for background-image + adds .hero-cover
970
976
  * overlay - "light", "dark", "darker", "gradient", "gradient-reverse" → .hero-overlay-{overlay}
971
977
  * align - "center" (default) or "left" → .hero-center / .hero-left