@cyber-dash-tech/revela 0.18.16 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/README.zh-CN.md +27 -13
- package/assets/img/lucent-01.jpg +0 -0
- package/assets/img/lucent-02.jpg +0 -0
- package/assets/img/lucent-03.jpg +0 -0
- package/assets/img/lucent-dark-01.jpg +0 -0
- package/assets/img/lucent-dark-02.jpg +0 -0
- package/assets/img/lucent-dark-03.jpg +0 -0
- package/assets/img/monet-01.jpg +0 -0
- package/assets/img/monet-02.jpg +0 -0
- package/assets/img/monet-03.jpg +0 -0
- package/assets/img/starter-01.jpg +0 -0
- package/assets/img/starter-02.jpg +0 -0
- package/assets/img/starter-03.jpg +0 -0
- package/assets/img/summit-01.jpg +0 -0
- package/assets/img/summit-02.jpg +0 -0
- package/assets/img/summit-03.jpg +0 -0
- package/designs/lucent/DESIGN.md +76 -0
- package/designs/lucent/design.css +283 -0
- package/designs/lucent-dark/DESIGN.md +278 -0
- package/designs/lucent-dark/assets/card-lens.jpg +0 -0
- package/designs/lucent-dark/assets/closing-background.jpg +0 -0
- package/designs/lucent-dark/assets/cover-background.jpg +0 -0
- package/designs/lucent-dark/assets/report-visual.jpg +0 -0
- package/designs/lucent-dark/assets/soft-texture.jpg +0 -0
- package/designs/lucent-dark/assets/toc-orb.png +0 -0
- package/designs/lucent-dark/design.css +417 -0
- package/designs/monet/DESIGN.md +14 -0
- package/designs/monet/assets/card-lens.jpg +0 -0
- package/designs/monet/assets/closing-background.jpg +0 -0
- package/designs/monet/assets/cover-background.jpg +0 -0
- package/designs/monet/assets/report-visual.jpg +0 -0
- package/designs/monet/assets/soft-texture.jpg +0 -0
- package/designs/monet/assets/toc-orb.png +0 -0
- package/designs/monet/design.css +340 -0
- package/designs/starter/DESIGN.md +14 -0
- package/designs/starter/assets/card-lens.jpg +0 -0
- package/designs/starter/assets/closing-background.jpg +0 -0
- package/designs/starter/assets/cover-background.jpg +0 -0
- package/designs/starter/assets/report-visual.jpg +0 -0
- package/designs/starter/assets/soft-texture.jpg +0 -0
- package/designs/starter/assets/toc-orb.png +0 -0
- package/designs/starter/design.css +322 -0
- package/designs/summit/DESIGN.md +18 -0
- package/designs/summit/assets/card-lens.jpg +0 -0
- package/designs/summit/assets/closing-background.jpg +0 -0
- package/designs/summit/assets/cover-background.jpg +0 -0
- package/designs/summit/assets/report-visual.jpg +0 -0
- package/designs/summit/assets/soft-texture.jpg +0 -0
- package/designs/summit/assets/toc-orb.png +0 -0
- package/designs/summit/design.css +334 -0
- package/lib/commands/designs-new.ts +13 -25
- package/lib/commands/designs-preview.ts +3 -8
- package/lib/deck-html/foundation.ts +8 -8
- package/lib/design/designs.ts +305 -14
- package/lib/narrative-state/deck-plan-artifact.ts +40 -3
- package/lib/page-templates/built-in-preview.html +373 -0
- package/lib/page-templates/contracts.ts +2 -0
- package/lib/page-templates/css.ts +2 -0
- package/lib/page-templates/foundation.ts +41 -0
- package/lib/page-templates/index.ts +6 -0
- package/lib/page-templates/registry.ts +3 -0
- package/lib/page-templates/render.ts +1202 -0
- package/lib/page-templates/templates/agenda.ts +4 -0
- package/lib/page-templates/templates/chart-takeaways.ts +4 -0
- package/lib/page-templates/templates/claim-supporting-visual.ts +4 -0
- package/lib/page-templates/templates/closing.ts +4 -0
- package/lib/page-templates/templates/cover.ts +4 -0
- package/lib/page-templates/templates/executive-summary.ts +4 -0
- package/lib/page-templates/templates/index.ts +19 -0
- package/lib/page-templates/templates/key-message-evidence.ts +4 -0
- package/lib/page-templates/templates/metric-highlight.ts +4 -0
- package/lib/page-templates/templates/problem-context.ts +4 -0
- package/lib/page-templates/templates/process-steps.ts +4 -0
- package/lib/page-templates/templates/recommendation-decision.ts +4 -0
- package/lib/page-templates/templates/risks-tradeoffs.ts +4 -0
- package/lib/page-templates/templates/section-divider.ts +4 -0
- package/lib/page-templates/templates/shared.ts +11 -0
- package/lib/page-templates/templates/table-comparison.ts +4 -0
- package/lib/page-templates/templates/timeline-roadmap.ts +4 -0
- package/lib/page-templates/vocabulary.ts +158 -0
- package/lib/prompt-builder.ts +5 -5
- package/lib/qa/artifact.ts +66 -1
- package/lib/qa/compliance.ts +5 -1
- package/lib/runtime/index.ts +99 -3
- package/package.json +7 -15
- package/plugins/revela/.codex-plugin/plugin.json +1 -1
- package/plugins/revela/hooks/revela_guard.ts +35 -0
- package/plugins/revela/mcp/revela-server.ts +101 -5
- package/plugins/revela/skills/revela-design/SKILL.md +21 -16
- package/plugins/revela/skills/revela-make-deck/SKILL.md +24 -15
- package/designs/lucent/preview.html +0 -529
- package/designs/monet/preview.html +0 -190
- package/designs/starter/preview.html +0 -335
- package/designs/summit/preview.html +0 -186
package/lib/design/designs.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* User designs are stored in ~/.config/revela/designs/<name>/.
|
|
5
5
|
* Built-in designs are shipped read-only with this package under designs/<name>/.
|
|
6
|
-
* Each design directory contains DESIGN.md (required)
|
|
6
|
+
* Each design directory contains DESIGN.md (required), design.css for CSS-native styling,
|
|
7
|
+
* and optionally legacy preview.html. Current previews are generated from the built-in
|
|
8
|
+
* page-template fixture plus the design CSS.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import {
|
|
@@ -17,8 +19,9 @@ import {
|
|
|
17
19
|
statSync,
|
|
18
20
|
writeFileSync,
|
|
19
21
|
} from "fs"
|
|
20
|
-
import { dirname, join, resolve, basename, relative, sep } from "path"
|
|
22
|
+
import { dirname, join, normalize, resolve, basename, relative, sep } from "path"
|
|
21
23
|
import { tmpdir } from "os"
|
|
24
|
+
import { pathToFileURL } from "url"
|
|
22
25
|
import { parseFrontmatter } from "../frontmatter"
|
|
23
26
|
import { collectDirectoryEntries, extractEntriesToDirectory, normalizePackageArchiveEntries, readTarArchive, writeTarArchive } from "./archive"
|
|
24
27
|
import {
|
|
@@ -28,11 +31,14 @@ import {
|
|
|
28
31
|
saveConfig,
|
|
29
32
|
} from "../config"
|
|
30
33
|
import { childLog } from "../log"
|
|
34
|
+
import { PAGE_TEMPLATE_CLASSES, listPageTemplateVocabulary, templateDeckCss } from "../page-templates"
|
|
31
35
|
|
|
32
36
|
const designLog = childLog("designs")
|
|
33
37
|
|
|
34
38
|
// Seed directory: built-in designs shipped with this package.
|
|
35
39
|
const SEED_DIR = resolve(__dirname, "../..", "designs")
|
|
40
|
+
const BUILT_IN_PREVIEW_PATH = resolve(__dirname, "..", "page-templates", "built-in-preview.html")
|
|
41
|
+
const PREVIEW_FALLBACK_ASSET_DESIGN = "lucent"
|
|
36
42
|
|
|
37
43
|
export interface DesignInfo {
|
|
38
44
|
name: string
|
|
@@ -51,7 +57,8 @@ export interface CreateDesignPackageArgs {
|
|
|
51
57
|
name: string
|
|
52
58
|
base?: string
|
|
53
59
|
designMd: string
|
|
54
|
-
|
|
60
|
+
designCss?: string
|
|
61
|
+
previewHtml?: string
|
|
55
62
|
assets?: DesignPackageAssetInput[]
|
|
56
63
|
overwrite?: boolean
|
|
57
64
|
}
|
|
@@ -85,6 +92,7 @@ export interface ValidateDesignPackageResult {
|
|
|
85
92
|
name: string
|
|
86
93
|
path: string
|
|
87
94
|
hasDesignMd: boolean
|
|
95
|
+
hasDesignCss: boolean
|
|
88
96
|
hasPreview: boolean
|
|
89
97
|
hasMarkers: boolean
|
|
90
98
|
sections: string[]
|
|
@@ -144,6 +152,37 @@ export interface DesignPreviewInfo {
|
|
|
144
152
|
hasPreview: boolean
|
|
145
153
|
}
|
|
146
154
|
|
|
155
|
+
export interface MaterializeDesignPreviewArgs {
|
|
156
|
+
workspaceRoot: string
|
|
157
|
+
name: string
|
|
158
|
+
source?: "draft" | "installed" | "builtin"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface MaterializeDesignPreviewResult {
|
|
162
|
+
ok: true
|
|
163
|
+
name: string
|
|
164
|
+
source: "draft" | "installed" | "builtin"
|
|
165
|
+
designDir: string
|
|
166
|
+
previewDir: string
|
|
167
|
+
previewPath: string
|
|
168
|
+
previewUrl: string
|
|
169
|
+
designCssPath: string
|
|
170
|
+
files: string[]
|
|
171
|
+
warnings: string[]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface DesignCssSnapshotResult {
|
|
175
|
+
ok: true
|
|
176
|
+
design: string
|
|
177
|
+
sourcePath: string
|
|
178
|
+
snapshotDir: string
|
|
179
|
+
cssPath: string
|
|
180
|
+
href: string
|
|
181
|
+
assetCount: number
|
|
182
|
+
generatedFallback: boolean
|
|
183
|
+
warnings: string[]
|
|
184
|
+
}
|
|
185
|
+
|
|
147
186
|
// ---------------------------------------------------------------------------
|
|
148
187
|
// Seed
|
|
149
188
|
// ---------------------------------------------------------------------------
|
|
@@ -300,6 +339,115 @@ export function resolveDesignPreview(name?: string): DesignPreviewInfo {
|
|
|
300
339
|
}
|
|
301
340
|
}
|
|
302
341
|
|
|
342
|
+
export function materializeDesignPreview(args: MaterializeDesignPreviewArgs): MaterializeDesignPreviewResult {
|
|
343
|
+
const name = normalizeDesignName(args.name)
|
|
344
|
+
const { designDir, source } = resolveDesignPreviewSourceDir(args.workspaceRoot, name, args.source)
|
|
345
|
+
const previewDir = resolve(args.workspaceRoot, ".revela", "previews", "designs", name)
|
|
346
|
+
const previewPath = join(previewDir, "preview.html")
|
|
347
|
+
const designCssPath = join(previewDir, "design.css")
|
|
348
|
+
const warnings: string[] = []
|
|
349
|
+
|
|
350
|
+
if (!existsSync(BUILT_IN_PREVIEW_PATH)) {
|
|
351
|
+
throw new Error(`Built-in design preview fixture is missing: ${BUILT_IN_PREVIEW_PATH}`)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
rmSync(previewDir, { recursive: true, force: true })
|
|
355
|
+
mkdirSync(previewDir, { recursive: true })
|
|
356
|
+
|
|
357
|
+
const previewHtml = readFileSync(BUILT_IN_PREVIEW_PATH, "utf-8").replace(/data-design="built-in-preview"/g, `data-design="${name}"`)
|
|
358
|
+
writeFileSync(previewPath, previewHtml, "utf-8")
|
|
359
|
+
|
|
360
|
+
const cssRead = readDesignCssFromDir(name, designDir)
|
|
361
|
+
warnings.push(...cssRead.warnings)
|
|
362
|
+
writeFileSync(designCssPath, `${cssRead.css.trim()}\n`, "utf-8")
|
|
363
|
+
|
|
364
|
+
const sourceAssets = join(designDir, "assets")
|
|
365
|
+
const targetAssets = join(previewDir, "assets")
|
|
366
|
+
if (existsSync(sourceAssets)) cpSync(sourceAssets, targetAssets, { recursive: true })
|
|
367
|
+
copyMissingBuiltInPreviewAssets(previewHtml, targetAssets)
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
name,
|
|
372
|
+
source,
|
|
373
|
+
designDir,
|
|
374
|
+
previewDir,
|
|
375
|
+
previewPath,
|
|
376
|
+
previewUrl: pathToFileURL(previewPath).href,
|
|
377
|
+
designCssPath,
|
|
378
|
+
files: listDesignPackageFiles(previewDir),
|
|
379
|
+
warnings,
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function resolveDesignPackageDir(name?: string): string {
|
|
384
|
+
const designName = normalizeDesignName(name || activeDesign())
|
|
385
|
+
const designDir = resolveDesignDir(designName)
|
|
386
|
+
if (!designDir) throw new Error(`Design '${designName}' is not installed`)
|
|
387
|
+
return designDir
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function readDesignCss(name?: string): { css: string; path?: string; generatedFallback: boolean; warnings: string[] } {
|
|
391
|
+
const designName = normalizeDesignName(name || activeDesign())
|
|
392
|
+
const designDir = resolveDesignPackageDir(designName)
|
|
393
|
+
const cssPath = join(designDir, "design.css")
|
|
394
|
+
if (existsSync(cssPath)) {
|
|
395
|
+
return { css: readFileSync(cssPath, "utf-8"), path: cssPath, generatedFallback: false, warnings: [] }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const foundation = getDesignSection("foundation", designName)
|
|
399
|
+
const cssBlocks = extractCssCodeBlocks(foundation)
|
|
400
|
+
if (cssBlocks.length === 0) throw new Error(`Design '${designName}' has no design.css and no CSS fallback in @design:foundation.`)
|
|
401
|
+
return {
|
|
402
|
+
css: [
|
|
403
|
+
"/* Generated compatibility CSS from DESIGN.md. Add design.css to make this design fully CSS-package native. */",
|
|
404
|
+
...cssBlocks,
|
|
405
|
+
templateDeckCss({ designName, designAssetBasePath: "./assets" }),
|
|
406
|
+
].join("\n\n"),
|
|
407
|
+
generatedFallback: true,
|
|
408
|
+
warnings: [`Design '${designName}' has no design.css; generated a deck-local compatibility CSS snapshot from DESIGN.md.`],
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function materializeDesignCssSnapshot(input: {
|
|
413
|
+
workspaceRoot: string
|
|
414
|
+
outputPath: string
|
|
415
|
+
designName?: string
|
|
416
|
+
}): DesignCssSnapshotResult {
|
|
417
|
+
const designName = normalizeDesignName(input.designName || activeDesign())
|
|
418
|
+
const designDir = resolveDesignPackageDir(designName)
|
|
419
|
+
const outputDir = dirname(normalize(input.outputPath))
|
|
420
|
+
const snapshotRelDir = normalize(join(outputDir, "_revela-design", designName)).replace(/\\/g, "/")
|
|
421
|
+
const snapshotDir = resolve(input.workspaceRoot, snapshotRelDir)
|
|
422
|
+
const cssPath = join(snapshotDir, "design.css")
|
|
423
|
+
const cssRead = readDesignCss(designName)
|
|
424
|
+
|
|
425
|
+
mkdirSync(snapshotDir, { recursive: true })
|
|
426
|
+
writeFileSync(cssPath, cssRead.css.endsWith("\n") ? cssRead.css : `${cssRead.css}\n`, "utf-8")
|
|
427
|
+
|
|
428
|
+
const assetsDir = join(designDir, "assets")
|
|
429
|
+
let assetCount = 0
|
|
430
|
+
if (existsSync(assetsDir) && statSync(assetsDir).isDirectory()) {
|
|
431
|
+
const targetAssets = join(snapshotDir, "assets")
|
|
432
|
+
rmSync(targetAssets, { recursive: true, force: true })
|
|
433
|
+
cpSync(assetsDir, targetAssets, { recursive: true })
|
|
434
|
+
assetCount = listDesignPackageFiles(targetAssets).length
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const href = normalize(relative(outputDir || ".", normalize(join(snapshotRelDir, "design.css")))).replace(/\\/g, "/")
|
|
438
|
+
return {
|
|
439
|
+
ok: true,
|
|
440
|
+
design: designName,
|
|
441
|
+
sourcePath: cssRead.path || join(designDir, "DESIGN.md"),
|
|
442
|
+
snapshotDir,
|
|
443
|
+
cssPath,
|
|
444
|
+
href: href.startsWith(".") ? href : `./${href}`,
|
|
445
|
+
assetCount,
|
|
446
|
+
generatedFallback: cssRead.generatedFallback,
|
|
447
|
+
warnings: cssRead.warnings,
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
303
451
|
/** Normalize and validate a design package name. */
|
|
304
452
|
export function normalizeDesignName(name: string): string {
|
|
305
453
|
const normalized = name.trim().toLowerCase()
|
|
@@ -313,10 +461,10 @@ export function normalizeDesignName(name: string): string {
|
|
|
313
461
|
export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesignPackageResult {
|
|
314
462
|
const name = normalizeDesignName(args.name)
|
|
315
463
|
const designMd = args.designMd?.trim()
|
|
464
|
+
const designCss = args.designCss?.trim()
|
|
316
465
|
const previewHtml = args.previewHtml?.trim()
|
|
317
466
|
|
|
318
467
|
if (!designMd) throw new Error("designMd is required")
|
|
319
|
-
if (!previewHtml) throw new Error("previewHtml is required")
|
|
320
468
|
|
|
321
469
|
const target = join(DESIGNS_DIR, name)
|
|
322
470
|
const existed = existsSync(target)
|
|
@@ -330,7 +478,8 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
|
|
|
330
478
|
}
|
|
331
479
|
mkdirSync(target, { recursive: true })
|
|
332
480
|
writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
|
|
333
|
-
writeFileSync(join(target, "
|
|
481
|
+
if (designCss) writeFileSync(join(target, "design.css"), `${designCss}\n`, "utf-8")
|
|
482
|
+
if (previewHtml) writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
|
|
334
483
|
writeDesignAssets(target, args.assets)
|
|
335
484
|
|
|
336
485
|
const validation = validateDesignPackage(name)
|
|
@@ -353,10 +502,10 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
|
|
|
353
502
|
export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDesignPackageResult {
|
|
354
503
|
const name = normalizeDesignName(args.name)
|
|
355
504
|
const designMd = args.designMd?.trim()
|
|
505
|
+
const designCss = args.designCss?.trim()
|
|
356
506
|
const previewHtml = args.previewHtml?.trim()
|
|
357
507
|
|
|
358
508
|
if (!designMd) throw new Error("designMd is required")
|
|
359
|
-
if (!previewHtml) throw new Error("previewHtml is required")
|
|
360
509
|
|
|
361
510
|
const target = designDraftDir(args.workspaceRoot, name)
|
|
362
511
|
const existed = existsSync(target)
|
|
@@ -370,7 +519,8 @@ export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDes
|
|
|
370
519
|
}
|
|
371
520
|
mkdirSync(target, { recursive: true })
|
|
372
521
|
writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
|
|
373
|
-
writeFileSync(join(target, "
|
|
522
|
+
if (designCss) writeFileSync(join(target, "design.css"), `${designCss}\n`, "utf-8")
|
|
523
|
+
if (previewHtml) writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
|
|
374
524
|
writeDesignAssets(target, args.assets)
|
|
375
525
|
|
|
376
526
|
const validation = validateDesignDraftPackage(args.workspaceRoot, name)
|
|
@@ -560,8 +710,10 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
560
710
|
}
|
|
561
711
|
|
|
562
712
|
const mdPath = join(dir, "DESIGN.md")
|
|
713
|
+
const cssPath = join(dir, "design.css")
|
|
563
714
|
const previewPath = join(dir, "preview.html")
|
|
564
715
|
const hasDesignMd = existsSync(mdPath)
|
|
716
|
+
const hasDesignCss = existsSync(cssPath)
|
|
565
717
|
const hasPreview = existsSync(previewPath)
|
|
566
718
|
let hasMarkers = false
|
|
567
719
|
let sections: string[] = []
|
|
@@ -571,7 +723,7 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
571
723
|
|
|
572
724
|
if (!existsSync(dir)) errors.push(`Design directory does not exist: ${dir}`)
|
|
573
725
|
if (!hasDesignMd) errors.push("DESIGN.md is missing")
|
|
574
|
-
if (!
|
|
726
|
+
if (!hasDesignCss) warnings.push("design.css is missing; DESIGN.md CSS fallback remains compatibility-only and should be migrated.")
|
|
575
727
|
if (existsSync(dir)) assets = listDesignAssetsInDir(dir)
|
|
576
728
|
|
|
577
729
|
if (hasDesignMd) {
|
|
@@ -595,11 +747,13 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
595
747
|
|
|
596
748
|
if (hasPreview) {
|
|
597
749
|
const preview = readFileSync(previewPath, "utf-8")
|
|
750
|
+
const designCss = hasDesignCss ? readFileSync(cssPath, "utf-8") : ""
|
|
598
751
|
if (!preview.includes('<section class="slide"')) errors.push("preview.html must include slide sections")
|
|
599
752
|
if (!preview.includes("slide-qa=")) errors.push("preview.html slides must include slide-qa attributes")
|
|
600
753
|
if (!preview.includes("slide-canvas")) errors.push("preview.html must include .slide-canvas")
|
|
601
|
-
if (!
|
|
602
|
-
|
|
754
|
+
if (hasDesignCss && !previewReferencesDesignCss(preview)) errors.push("preview.html must reference design.css")
|
|
755
|
+
if (!hasFixedSizeCssRule(`${preview}\n${designCss}`, "slide-canvas")) {
|
|
756
|
+
errors.push("preview.html or design.css must define .slide-canvas CSS with width: 1920px and height: 1080px")
|
|
603
757
|
}
|
|
604
758
|
if (!hasSlideRole(preview, "cover")) errors.push('preview.html must include a slide section with data-slide-role="cover"')
|
|
605
759
|
if (!hasSlideRole(preview, "closing")) errors.push('preview.html must include a slide section with data-slide-role="closing"')
|
|
@@ -611,16 +765,24 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
611
765
|
if (missingLayoutPreviews.length > 0) {
|
|
612
766
|
warnings.push(`preview.html should mark layout fixtures with data-preview-layout; missing: ${missingLayoutPreviews.join(", ")}`)
|
|
613
767
|
}
|
|
614
|
-
const designText = hasDesignMd ? readFileSync(mdPath, "utf-8") : ""
|
|
615
|
-
const tokenWarnings = designContractTokenWarnings(`${designText}\n${preview}`)
|
|
616
|
-
warnings.push(...tokenWarnings)
|
|
617
768
|
}
|
|
769
|
+
if (hasDesignCss) {
|
|
770
|
+
const designCss = readFileSync(cssPath, "utf-8")
|
|
771
|
+
const missingCoreClasses = requiredDesignCssClasses().filter((className) => !cssTextHasClassSelector(designCss, className))
|
|
772
|
+
if (missingCoreClasses.length > 0) errors.push(`design.css must style core template classes; missing: ${missingCoreClasses.map((item) => `.${item}`).join(", ")}`)
|
|
773
|
+
errors.push(...missingDesignCssAssetErrors(designCss, dir))
|
|
774
|
+
}
|
|
775
|
+
const designText = hasDesignMd ? readFileSync(mdPath, "utf-8") : ""
|
|
776
|
+
const designCss = hasDesignCss ? readFileSync(cssPath, "utf-8") : ""
|
|
777
|
+
const tokenWarnings = designContractTokenWarnings(`${designText}\n${designCss}`)
|
|
778
|
+
warnings.push(...tokenWarnings)
|
|
618
779
|
|
|
619
780
|
return {
|
|
620
781
|
ok: errors.length === 0,
|
|
621
782
|
name,
|
|
622
783
|
path: dir,
|
|
623
784
|
hasDesignMd,
|
|
785
|
+
hasDesignCss,
|
|
624
786
|
hasPreview,
|
|
625
787
|
hasMarkers,
|
|
626
788
|
sections,
|
|
@@ -641,15 +803,128 @@ function designContractTokenWarnings(text: string): string[] {
|
|
|
641
803
|
{ label: "surface", pattern: /--surface|surface token|border token|shadow token/i },
|
|
642
804
|
]
|
|
643
805
|
for (const check of checks) {
|
|
644
|
-
if (!check.pattern.test(text)) warnings.push(`DESIGN.md/
|
|
806
|
+
if (!check.pattern.test(text)) warnings.push(`DESIGN.md/design.css should document ${check.label} design tokens or an equivalent contract`)
|
|
645
807
|
}
|
|
646
808
|
return warnings
|
|
647
809
|
}
|
|
648
810
|
|
|
811
|
+
function previewReferencesDesignCss(preview: string): boolean {
|
|
812
|
+
return /<link\b[^>]*href=["'][^"']*design\.css["'][^>]*>/i.test(preview)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function requiredDesignCssClasses(): string[] {
|
|
816
|
+
return [
|
|
817
|
+
"slide-canvas",
|
|
818
|
+
"template-slide",
|
|
819
|
+
"template-frame",
|
|
820
|
+
"template-title",
|
|
821
|
+
"template-card",
|
|
822
|
+
"template-visual-slot-panel",
|
|
823
|
+
]
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function cssTextHasClassSelector(css: string, className: string): boolean {
|
|
827
|
+
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
828
|
+
const ruleRe = /([^{}]+)\{[^{}]*\}/g
|
|
829
|
+
let match: RegExpExecArray | null
|
|
830
|
+
while ((match = ruleRe.exec(withoutComments)) !== null) {
|
|
831
|
+
if (cssRuleHasClassSelector(match[1] ?? "", className)) return true
|
|
832
|
+
}
|
|
833
|
+
return false
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function missingDesignCssAssetErrors(css: string, designDir: string): string[] {
|
|
837
|
+
const errors: string[] = []
|
|
838
|
+
const seen = new Set<string>()
|
|
839
|
+
const urlRe = /url\(\s*["']?([^"')]+)["']?\s*\)/gi
|
|
840
|
+
let match: RegExpExecArray | null
|
|
841
|
+
while ((match = urlRe.exec(css)) !== null) {
|
|
842
|
+
const raw = match[1].trim()
|
|
843
|
+
if (!raw || raw.startsWith("data:") || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith("#")) continue
|
|
844
|
+
const normalized = raw.replace(/^\.\/+/, "")
|
|
845
|
+
if (!normalized.startsWith("assets/")) continue
|
|
846
|
+
if (seen.has(normalized)) continue
|
|
847
|
+
seen.add(normalized)
|
|
848
|
+
if (!existsSync(join(designDir, ...normalized.split("/")))) errors.push(`design.css references missing asset: ${normalized}`)
|
|
849
|
+
}
|
|
850
|
+
return errors
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function extractCssCodeBlocks(markdown: string): string[] {
|
|
854
|
+
const blocks: string[] = []
|
|
855
|
+
const fenceRe = /```css\n([\s\S]*?)```/g
|
|
856
|
+
let match: RegExpExecArray | null
|
|
857
|
+
while ((match = fenceRe.exec(markdown)) !== null) {
|
|
858
|
+
const body = match[1].trim()
|
|
859
|
+
if (body) blocks.push(body)
|
|
860
|
+
}
|
|
861
|
+
return blocks
|
|
862
|
+
}
|
|
863
|
+
|
|
649
864
|
function designDraftDir(workspaceRoot: string, name: string): string {
|
|
650
865
|
return resolve(workspaceRoot, ".revela", "drafts", "designs", name)
|
|
651
866
|
}
|
|
652
867
|
|
|
868
|
+
function resolveDesignPreviewSourceDir(
|
|
869
|
+
workspaceRoot: string,
|
|
870
|
+
name: string,
|
|
871
|
+
requested?: "draft" | "installed" | "builtin",
|
|
872
|
+
): { designDir: string; source: "draft" | "installed" | "builtin" } {
|
|
873
|
+
const draftDir = designDraftDir(workspaceRoot, name)
|
|
874
|
+
if (requested === "draft") {
|
|
875
|
+
if (!existsSync(draftDir)) throw new Error(`Design draft '${name}' does not exist: ${draftDir}`)
|
|
876
|
+
return { designDir: draftDir, source: "draft" }
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const installedDir = resolveDesignDir(name)
|
|
880
|
+
if (requested === "installed" || requested === "builtin") {
|
|
881
|
+
if (!installedDir) throw new Error(`Design '${name}' is not installed`)
|
|
882
|
+
const source = requested === "builtin" ? "builtin" : installedDir.startsWith(SEED_DIR + sep) ? "builtin" : "installed"
|
|
883
|
+
return { designDir: installedDir, source }
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (existsSync(draftDir)) return { designDir: draftDir, source: "draft" }
|
|
887
|
+
if (!installedDir) throw new Error(`Design '${name}' is not available as a workspace draft or installed design`)
|
|
888
|
+
return { designDir: installedDir, source: installedDir.startsWith(SEED_DIR + sep) ? "builtin" : "installed" }
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function readDesignCssFromDir(name: string, designDir: string): { css: string; warnings: string[] } {
|
|
892
|
+
const cssPath = join(designDir, "design.css")
|
|
893
|
+
if (existsSync(cssPath)) return { css: readFileSync(cssPath, "utf-8"), warnings: [] }
|
|
894
|
+
|
|
895
|
+
const mdPath = join(designDir, "DESIGN.md")
|
|
896
|
+
const markdown = existsSync(mdPath) ? readFileSync(mdPath, "utf-8") : ""
|
|
897
|
+
const cssBlocks = extractCssCodeBlocks(markdown)
|
|
898
|
+
const css = [
|
|
899
|
+
"/* Generated preview compatibility CSS from DESIGN.md. Add design.css to make this design CSS-native. */",
|
|
900
|
+
templateDeckCss(),
|
|
901
|
+
...cssBlocks,
|
|
902
|
+
].join("\n\n")
|
|
903
|
+
return {
|
|
904
|
+
css,
|
|
905
|
+
warnings: [`Design '${name}' has no design.css; generated preview CSS from DESIGN.md compatibility blocks.`],
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function copyMissingBuiltInPreviewAssets(previewHtml: string, targetAssetsDir: string): void {
|
|
910
|
+
const fallbackAssetsDir = join(SEED_DIR, PREVIEW_FALLBACK_ASSET_DESIGN, "assets")
|
|
911
|
+
const assetRefs = new Set<string>()
|
|
912
|
+
const re = /(?:src|href)=["'](?:\.\/)?assets\/([^"']+)["']/g
|
|
913
|
+
let match: RegExpExecArray | null
|
|
914
|
+
while ((match = re.exec(previewHtml)) !== null) assetRefs.add(match[1])
|
|
915
|
+
if (assetRefs.size === 0) return
|
|
916
|
+
|
|
917
|
+
mkdirSync(targetAssetsDir, { recursive: true })
|
|
918
|
+
for (const rel of assetRefs) {
|
|
919
|
+
const target = resolve(targetAssetsDir, rel)
|
|
920
|
+
if (existsSync(target)) continue
|
|
921
|
+
const source = resolve(fallbackAssetsDir, rel)
|
|
922
|
+
if (!existsSync(source) || !statSync(source).isFile()) continue
|
|
923
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
924
|
+
cpSync(source, target)
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
653
928
|
function designDraftExists(workspaceRoot: string, name: string): boolean {
|
|
654
929
|
const dir = designDraftDir(workspaceRoot, name)
|
|
655
930
|
return existsSync(dir) && statSync(dir).isDirectory() && existsSync(join(dir, "DESIGN.md"))
|
|
@@ -812,6 +1087,12 @@ export interface DesignInventory {
|
|
|
812
1087
|
sections: string[]
|
|
813
1088
|
layouts: DesignInventoryLayout[]
|
|
814
1089
|
components: DesignInventoryComponent[]
|
|
1090
|
+
pageTemplates: Array<{
|
|
1091
|
+
templateId: string
|
|
1092
|
+
slots: string[]
|
|
1093
|
+
requiredClasses: string[]
|
|
1094
|
+
contractNotes: string[]
|
|
1095
|
+
}>
|
|
815
1096
|
assets: DesignPackageAssetInfo[]
|
|
816
1097
|
hasMarkers: boolean
|
|
817
1098
|
}
|
|
@@ -953,6 +1234,12 @@ export function getDesignInventory(designName?: string): DesignInventory {
|
|
|
953
1234
|
nesting: inferComponentNesting(componentName),
|
|
954
1235
|
contract: inferComponentContract(componentName, content),
|
|
955
1236
|
})),
|
|
1237
|
+
pageTemplates: listPageTemplateVocabulary().map((template) => ({
|
|
1238
|
+
templateId: template.templateId,
|
|
1239
|
+
slots: template.slots.map((slot) => slot.name),
|
|
1240
|
+
requiredClasses: template.requiredClasses,
|
|
1241
|
+
contractNotes: template.contractNotes,
|
|
1242
|
+
})),
|
|
956
1243
|
assets: listDesignAssetsInDir(designDir),
|
|
957
1244
|
hasMarkers,
|
|
958
1245
|
}
|
|
@@ -1225,6 +1512,7 @@ const UNIVERSAL_CLASSES = new Set([
|
|
|
1225
1512
|
"overlay",
|
|
1226
1513
|
"alt",
|
|
1227
1514
|
"strong",
|
|
1515
|
+
...PAGE_TEMPLATE_CLASSES,
|
|
1228
1516
|
])
|
|
1229
1517
|
|
|
1230
1518
|
/**
|
|
@@ -1360,6 +1648,9 @@ export function extractDesignClasses(designName?: string): DesignClassVocabulary
|
|
|
1360
1648
|
extractFromSection(content)
|
|
1361
1649
|
}
|
|
1362
1650
|
|
|
1651
|
+
const cssPath = join(designDir, "design.css")
|
|
1652
|
+
if (existsSync(cssPath)) extractFromCss(readFileSync(cssPath, "utf-8"))
|
|
1653
|
+
|
|
1363
1654
|
return { classes, prefixExemptions: DEFAULT_PREFIX_EXEMPTIONS }
|
|
1364
1655
|
}
|
|
1365
1656
|
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, wri
|
|
|
2
2
|
import { dirname, join, relative } from "path"
|
|
3
3
|
import { createHash } from "crypto"
|
|
4
4
|
import type { DeckSpec, SlideSpec } from "../decks-state"
|
|
5
|
+
import { listPageTemplates } from "../page-templates"
|
|
5
6
|
import { parseVaultFrontmatter } from "../narrative-vault/frontmatter"
|
|
6
7
|
import { splitMarkdownSections } from "../narrative-vault/markdown"
|
|
7
8
|
import { stableVaultRelationId } from "../narrative-vault/relations"
|
|
@@ -92,6 +93,8 @@ export interface DeckPlanSlideProjection {
|
|
|
92
93
|
slideIndex?: number
|
|
93
94
|
title: string
|
|
94
95
|
chapter: string
|
|
96
|
+
template?: string
|
|
97
|
+
templateContent?: Record<string, any>
|
|
95
98
|
layout: string
|
|
96
99
|
components: string[]
|
|
97
100
|
componentPlan: DeckPlanSlideComponentPlan[]
|
|
@@ -575,6 +578,8 @@ function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string
|
|
|
575
578
|
slideIndex: numberField(parsed.frontmatter, "slideIndex"),
|
|
576
579
|
title: stringField(parsed.frontmatter, "title") || id,
|
|
577
580
|
chapter: stringField(parsed.frontmatter, "chapter"),
|
|
581
|
+
template: stringField(parsed.frontmatter, "template"),
|
|
582
|
+
templateContent: parseTemplateContent(split.sections["template-content"] ?? ""),
|
|
578
583
|
layout: stringField(parsed.frontmatter, "layout"),
|
|
579
584
|
components: arrayField(parsed.frontmatter, "components"),
|
|
580
585
|
componentPlan,
|
|
@@ -623,6 +628,8 @@ function readDeckPlanSlidesFromSingleFile(workspaceRoot: string, absolutePath: s
|
|
|
623
628
|
slideIndex,
|
|
624
629
|
title,
|
|
625
630
|
chapter: fields.chapter || "",
|
|
631
|
+
template: fields.template || "",
|
|
632
|
+
templateContent: parseTemplateContent(singleFileSubsection(block, "Template Content")),
|
|
626
633
|
layout: fields.layout || "",
|
|
627
634
|
components: parseCsv(fields.components || componentPlan.map((component) => component.name).join(", ")),
|
|
628
635
|
componentPlan,
|
|
@@ -671,6 +678,8 @@ function readDeckPlanSeparatorSlidesFromSingleFile(workspaceRoot: string, absolu
|
|
|
671
678
|
slideIndex,
|
|
672
679
|
title,
|
|
673
680
|
chapter: stringField(parsed.frontmatter, "chapter") || fields.chapter || "",
|
|
681
|
+
template: stringField(parsed.frontmatter, "template") || fields.template || "",
|
|
682
|
+
templateContent: parseTemplateContent(singleFileSubsection(block, "Template Content")),
|
|
674
683
|
layout: stringField(parsed.frontmatter, "layout") || fields.layout || "",
|
|
675
684
|
components: arrayField(parsed.frontmatter, "components").length > 0 ? arrayField(parsed.frontmatter, "components") : parseCsv(fields.components || componentPlan.map((component) => component.name).join(", ")),
|
|
676
685
|
componentPlan,
|
|
@@ -702,6 +711,27 @@ function parseSlideBlockFields(block: string): Record<string, string> {
|
|
|
702
711
|
return fields
|
|
703
712
|
}
|
|
704
713
|
|
|
714
|
+
function parseTemplateContent(section: string): Record<string, any> | undefined {
|
|
715
|
+
const trimmed = section.trim()
|
|
716
|
+
if (!trimmed) return undefined
|
|
717
|
+
const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```$/i.exec(trimmed)
|
|
718
|
+
const body = fenced ? fenced[1].trim() : trimmed
|
|
719
|
+
if (body.startsWith("{")) {
|
|
720
|
+
try {
|
|
721
|
+
return JSON.parse(body)
|
|
722
|
+
} catch {
|
|
723
|
+
return { raw: body }
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const result: Record<string, any> = {}
|
|
727
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
728
|
+
const match = /^-\s+([A-Za-z][A-Za-z0-9 _-]*):\s*(.*)$/.exec(rawLine.trim())
|
|
729
|
+
if (!match) continue
|
|
730
|
+
result[match[1].trim().replace(/\s+/g, "_").toLowerCase()] = cleanPlanValue(match[2])
|
|
731
|
+
}
|
|
732
|
+
return Object.keys(result).length > 0 ? result : { raw: body }
|
|
733
|
+
}
|
|
734
|
+
|
|
705
735
|
function singleFileSubsection(block: string, heading: string): string {
|
|
706
736
|
const re = new RegExp(`^[ \\t]*####\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "mi")
|
|
707
737
|
const match = re.exec(block)
|
|
@@ -906,9 +936,11 @@ function slideDiagnostics(slide: DeckPlanSlideProjection, knownNodeIds?: Set<str
|
|
|
906
936
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
907
937
|
if (!slide.structural && linksCount(slide.sourceLinks) === 0) diagnostics.push({ severity: "warning", code: "slide_source_link_missing", message: `Non-structural deck-plan slide ${slide.id} has no material, finding, asset, or URL source link.`, file: slide.path, nodeId: slide.id })
|
|
908
938
|
if (!slide.structural && linksCount(slide.sourceLinks) > 0) diagnostics.push(...slideSynthesisDiagnostics(slide))
|
|
909
|
-
if (!slide.
|
|
910
|
-
if (slide.
|
|
911
|
-
if (slide.
|
|
939
|
+
if (slide.template && !knownTemplateIds().has(slide.template)) diagnostics.push({ severity: "warning", code: "slide_template_unknown", message: `Deck-plan slide ${slide.id} uses unknown page template '${slide.template}'.`, file: slide.path, nodeId: slide.id })
|
|
940
|
+
if (slide.template && !slide.templateContent) diagnostics.push({ severity: "warning", code: "slide_template_content_missing", message: `Deck-plan slide ${slide.id} uses template '${slide.template}' but has no #### Template Content section.`, file: slide.path, nodeId: slide.id })
|
|
941
|
+
if (!slide.template && !slide.layout) diagnostics.push({ severity: "warning", code: "slide_layout_missing", message: `Deck-plan slide ${slide.id} is missing a layout.`, file: slide.path, nodeId: slide.id })
|
|
942
|
+
if (!slide.template && slide.components.length === 0) diagnostics.push({ severity: "warning", code: "slide_components_missing", message: `Deck-plan slide ${slide.id} has no component names in frontmatter.`, file: slide.path, nodeId: slide.id })
|
|
943
|
+
if (!slide.template && slide.componentPlan.length === 0) diagnostics.push({ severity: "warning", code: "slide_component_plan_missing", message: `Deck-plan slide ${slide.id} is missing structured ## Component Plan entries.`, file: slide.path, nodeId: slide.id })
|
|
912
944
|
for (const component of slide.componentPlan) {
|
|
913
945
|
for (const key of ["name", "slot", "position", "purpose", "content"] as const) {
|
|
914
946
|
if (!component[key] || (Array.isArray(component[key]) && component[key].length === 0)) diagnostics.push({ severity: "warning", code: "slide_component_plan_incomplete", message: `Deck-plan slide ${slide.id} has incomplete component plan entry for ${component.name || "unnamed component"}: missing ${key}.`, file: slide.path, nodeId: slide.id })
|
|
@@ -922,6 +954,10 @@ function slideDiagnostics(slide: DeckPlanSlideProjection, knownNodeIds?: Set<str
|
|
|
922
954
|
return diagnostics
|
|
923
955
|
}
|
|
924
956
|
|
|
957
|
+
function knownTemplateIds(): Set<string> {
|
|
958
|
+
return new Set(listPageTemplates().templates.map((template) => template.id))
|
|
959
|
+
}
|
|
960
|
+
|
|
925
961
|
function slideSynthesisDiagnostics(slide: DeckPlanSlideProjection): DeckPlanProjectionDiagnostic[] {
|
|
926
962
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
927
963
|
const contentPlan = singleFileSubsection(slide.markdown, "Content Plan")
|
|
@@ -958,6 +994,7 @@ export function deckPlanDesignDiagnostics(projection: DeckPlanProjection | undef
|
|
|
958
994
|
const components = new Set(inventory.components)
|
|
959
995
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
960
996
|
for (const slide of projection.slides) {
|
|
997
|
+
if (slide.template) continue
|
|
961
998
|
if (slide.layout && !layouts.has(slide.layout)) diagnostics.push({ severity: "warning", code: "slide_layout_unknown", message: `Deck-plan slide ${slide.id} uses layout '${slide.layout}' outside the active design inventory.`, file: slide.path, nodeId: slide.id })
|
|
962
999
|
for (const component of slide.componentPlan) diagnostics.push(...componentDesignDiagnostics(slide, component, inventory, components))
|
|
963
1000
|
}
|