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 +44 -3
- package/bin/lib/config-merge.js +44 -0
- package/bin/update.js +547 -0
- package/config/server.json +10 -6
- package/package.json +3 -2
- package/public/css/site.css +1 -1
- package/server/services/markdown.js +8 -2
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 <
|
|
40
|
+
Usage: npx domma-cms <command> [options]
|
|
41
41
|
|
|
42
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/config/server.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domma-cms",
|
|
3
|
-
"version": "0.
|
|
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",
|
package/public/css/site.css
CHANGED
|
@@ -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",
|
|
968
|
-
* "gradient-
|
|
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
|