@cyber-dash-tech/revela 0.18.16 → 0.19.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/README.md +45 -31
- package/README.zh-CN.md +45 -31
- 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 +317 -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/hooks/revela_post_write_notice.ts +4 -4
- package/plugins/revela/mcp/revela-server.ts +104 -6
- package/plugins/revela/skills/revela/SKILL.md +1 -1
- package/plugins/revela/skills/revela-design/SKILL.md +22 -16
- package/plugins/revela/skills/revela-helper/SKILL.md +1 -1
- package/plugins/revela/skills/revela-make-deck/SKILL.md +25 -16
- 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,43 @@ 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
|
+
browserHandoff: {
|
|
170
|
+
serveRoot: string
|
|
171
|
+
path: string
|
|
172
|
+
urlTemplate: string
|
|
173
|
+
instructions: string
|
|
174
|
+
}
|
|
175
|
+
designCssPath: string
|
|
176
|
+
files: string[]
|
|
177
|
+
warnings: string[]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface DesignCssSnapshotResult {
|
|
181
|
+
ok: true
|
|
182
|
+
design: string
|
|
183
|
+
sourcePath: string
|
|
184
|
+
snapshotDir: string
|
|
185
|
+
cssPath: string
|
|
186
|
+
href: string
|
|
187
|
+
assetCount: number
|
|
188
|
+
generatedFallback: boolean
|
|
189
|
+
warnings: string[]
|
|
190
|
+
}
|
|
191
|
+
|
|
147
192
|
// ---------------------------------------------------------------------------
|
|
148
193
|
// Seed
|
|
149
194
|
// ---------------------------------------------------------------------------
|
|
@@ -300,6 +345,121 @@ export function resolveDesignPreview(name?: string): DesignPreviewInfo {
|
|
|
300
345
|
}
|
|
301
346
|
}
|
|
302
347
|
|
|
348
|
+
export function materializeDesignPreview(args: MaterializeDesignPreviewArgs): MaterializeDesignPreviewResult {
|
|
349
|
+
const name = normalizeDesignName(args.name)
|
|
350
|
+
const { designDir, source } = resolveDesignPreviewSourceDir(args.workspaceRoot, name, args.source)
|
|
351
|
+
const previewDir = resolve(args.workspaceRoot, ".revela", "previews", "designs", name)
|
|
352
|
+
const previewPath = join(previewDir, "preview.html")
|
|
353
|
+
const designCssPath = join(previewDir, "design.css")
|
|
354
|
+
const warnings: string[] = []
|
|
355
|
+
|
|
356
|
+
if (!existsSync(BUILT_IN_PREVIEW_PATH)) {
|
|
357
|
+
throw new Error(`Built-in design preview fixture is missing: ${BUILT_IN_PREVIEW_PATH}`)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
rmSync(previewDir, { recursive: true, force: true })
|
|
361
|
+
mkdirSync(previewDir, { recursive: true })
|
|
362
|
+
|
|
363
|
+
const previewHtml = readFileSync(BUILT_IN_PREVIEW_PATH, "utf-8").replace(/data-design="built-in-preview"/g, `data-design="${name}"`)
|
|
364
|
+
writeFileSync(previewPath, previewHtml, "utf-8")
|
|
365
|
+
|
|
366
|
+
const cssRead = readDesignCssFromDir(name, designDir)
|
|
367
|
+
warnings.push(...cssRead.warnings)
|
|
368
|
+
writeFileSync(designCssPath, `${cssRead.css.trim()}\n`, "utf-8")
|
|
369
|
+
|
|
370
|
+
const sourceAssets = join(designDir, "assets")
|
|
371
|
+
const targetAssets = join(previewDir, "assets")
|
|
372
|
+
if (existsSync(sourceAssets)) cpSync(sourceAssets, targetAssets, { recursive: true })
|
|
373
|
+
copyMissingBuiltInPreviewAssets(previewHtml, targetAssets)
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
name,
|
|
378
|
+
source,
|
|
379
|
+
designDir,
|
|
380
|
+
previewDir,
|
|
381
|
+
previewPath,
|
|
382
|
+
previewUrl: pathToFileURL(previewPath).href,
|
|
383
|
+
browserHandoff: {
|
|
384
|
+
serveRoot: previewDir,
|
|
385
|
+
path: "preview.html",
|
|
386
|
+
urlTemplate: "http://127.0.0.1:<port>/preview.html",
|
|
387
|
+
instructions: "Start a read-only local static server from serveRoot, then reply with the localhost URL so the user can click it open in Codex Browser. Do not open the file:// preview directly.",
|
|
388
|
+
},
|
|
389
|
+
designCssPath,
|
|
390
|
+
files: listDesignPackageFiles(previewDir),
|
|
391
|
+
warnings,
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function resolveDesignPackageDir(name?: string): string {
|
|
396
|
+
const designName = normalizeDesignName(name || activeDesign())
|
|
397
|
+
const designDir = resolveDesignDir(designName)
|
|
398
|
+
if (!designDir) throw new Error(`Design '${designName}' is not installed`)
|
|
399
|
+
return designDir
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function readDesignCss(name?: string): { css: string; path?: string; generatedFallback: boolean; warnings: string[] } {
|
|
403
|
+
const designName = normalizeDesignName(name || activeDesign())
|
|
404
|
+
const designDir = resolveDesignPackageDir(designName)
|
|
405
|
+
const cssPath = join(designDir, "design.css")
|
|
406
|
+
if (existsSync(cssPath)) {
|
|
407
|
+
return { css: readFileSync(cssPath, "utf-8"), path: cssPath, generatedFallback: false, warnings: [] }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const foundation = getDesignSection("foundation", designName)
|
|
411
|
+
const cssBlocks = extractCssCodeBlocks(foundation)
|
|
412
|
+
if (cssBlocks.length === 0) throw new Error(`Design '${designName}' has no design.css and no CSS fallback in @design:foundation.`)
|
|
413
|
+
return {
|
|
414
|
+
css: [
|
|
415
|
+
"/* Generated compatibility CSS from DESIGN.md. Add design.css to make this design fully CSS-package native. */",
|
|
416
|
+
...cssBlocks,
|
|
417
|
+
templateDeckCss({ designName, designAssetBasePath: "./assets" }),
|
|
418
|
+
].join("\n\n"),
|
|
419
|
+
generatedFallback: true,
|
|
420
|
+
warnings: [`Design '${designName}' has no design.css; generated a deck-local compatibility CSS snapshot from DESIGN.md.`],
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function materializeDesignCssSnapshot(input: {
|
|
425
|
+
workspaceRoot: string
|
|
426
|
+
outputPath: string
|
|
427
|
+
designName?: string
|
|
428
|
+
}): DesignCssSnapshotResult {
|
|
429
|
+
const designName = normalizeDesignName(input.designName || activeDesign())
|
|
430
|
+
const designDir = resolveDesignPackageDir(designName)
|
|
431
|
+
const outputDir = dirname(normalize(input.outputPath))
|
|
432
|
+
const snapshotRelDir = normalize(join(outputDir, "_revela-design", designName)).replace(/\\/g, "/")
|
|
433
|
+
const snapshotDir = resolve(input.workspaceRoot, snapshotRelDir)
|
|
434
|
+
const cssPath = join(snapshotDir, "design.css")
|
|
435
|
+
const cssRead = readDesignCss(designName)
|
|
436
|
+
|
|
437
|
+
mkdirSync(snapshotDir, { recursive: true })
|
|
438
|
+
writeFileSync(cssPath, cssRead.css.endsWith("\n") ? cssRead.css : `${cssRead.css}\n`, "utf-8")
|
|
439
|
+
|
|
440
|
+
const assetsDir = join(designDir, "assets")
|
|
441
|
+
let assetCount = 0
|
|
442
|
+
if (existsSync(assetsDir) && statSync(assetsDir).isDirectory()) {
|
|
443
|
+
const targetAssets = join(snapshotDir, "assets")
|
|
444
|
+
rmSync(targetAssets, { recursive: true, force: true })
|
|
445
|
+
cpSync(assetsDir, targetAssets, { recursive: true })
|
|
446
|
+
assetCount = listDesignPackageFiles(targetAssets).length
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const href = normalize(relative(outputDir || ".", normalize(join(snapshotRelDir, "design.css")))).replace(/\\/g, "/")
|
|
450
|
+
return {
|
|
451
|
+
ok: true,
|
|
452
|
+
design: designName,
|
|
453
|
+
sourcePath: cssRead.path || join(designDir, "DESIGN.md"),
|
|
454
|
+
snapshotDir,
|
|
455
|
+
cssPath,
|
|
456
|
+
href: href.startsWith(".") ? href : `./${href}`,
|
|
457
|
+
assetCount,
|
|
458
|
+
generatedFallback: cssRead.generatedFallback,
|
|
459
|
+
warnings: cssRead.warnings,
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
303
463
|
/** Normalize and validate a design package name. */
|
|
304
464
|
export function normalizeDesignName(name: string): string {
|
|
305
465
|
const normalized = name.trim().toLowerCase()
|
|
@@ -313,10 +473,10 @@ export function normalizeDesignName(name: string): string {
|
|
|
313
473
|
export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesignPackageResult {
|
|
314
474
|
const name = normalizeDesignName(args.name)
|
|
315
475
|
const designMd = args.designMd?.trim()
|
|
476
|
+
const designCss = args.designCss?.trim()
|
|
316
477
|
const previewHtml = args.previewHtml?.trim()
|
|
317
478
|
|
|
318
479
|
if (!designMd) throw new Error("designMd is required")
|
|
319
|
-
if (!previewHtml) throw new Error("previewHtml is required")
|
|
320
480
|
|
|
321
481
|
const target = join(DESIGNS_DIR, name)
|
|
322
482
|
const existed = existsSync(target)
|
|
@@ -330,7 +490,8 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
|
|
|
330
490
|
}
|
|
331
491
|
mkdirSync(target, { recursive: true })
|
|
332
492
|
writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
|
|
333
|
-
writeFileSync(join(target, "
|
|
493
|
+
if (designCss) writeFileSync(join(target, "design.css"), `${designCss}\n`, "utf-8")
|
|
494
|
+
if (previewHtml) writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
|
|
334
495
|
writeDesignAssets(target, args.assets)
|
|
335
496
|
|
|
336
497
|
const validation = validateDesignPackage(name)
|
|
@@ -353,10 +514,10 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
|
|
|
353
514
|
export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDesignPackageResult {
|
|
354
515
|
const name = normalizeDesignName(args.name)
|
|
355
516
|
const designMd = args.designMd?.trim()
|
|
517
|
+
const designCss = args.designCss?.trim()
|
|
356
518
|
const previewHtml = args.previewHtml?.trim()
|
|
357
519
|
|
|
358
520
|
if (!designMd) throw new Error("designMd is required")
|
|
359
|
-
if (!previewHtml) throw new Error("previewHtml is required")
|
|
360
521
|
|
|
361
522
|
const target = designDraftDir(args.workspaceRoot, name)
|
|
362
523
|
const existed = existsSync(target)
|
|
@@ -370,7 +531,8 @@ export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDes
|
|
|
370
531
|
}
|
|
371
532
|
mkdirSync(target, { recursive: true })
|
|
372
533
|
writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
|
|
373
|
-
writeFileSync(join(target, "
|
|
534
|
+
if (designCss) writeFileSync(join(target, "design.css"), `${designCss}\n`, "utf-8")
|
|
535
|
+
if (previewHtml) writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
|
|
374
536
|
writeDesignAssets(target, args.assets)
|
|
375
537
|
|
|
376
538
|
const validation = validateDesignDraftPackage(args.workspaceRoot, name)
|
|
@@ -560,8 +722,10 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
560
722
|
}
|
|
561
723
|
|
|
562
724
|
const mdPath = join(dir, "DESIGN.md")
|
|
725
|
+
const cssPath = join(dir, "design.css")
|
|
563
726
|
const previewPath = join(dir, "preview.html")
|
|
564
727
|
const hasDesignMd = existsSync(mdPath)
|
|
728
|
+
const hasDesignCss = existsSync(cssPath)
|
|
565
729
|
const hasPreview = existsSync(previewPath)
|
|
566
730
|
let hasMarkers = false
|
|
567
731
|
let sections: string[] = []
|
|
@@ -571,7 +735,7 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
571
735
|
|
|
572
736
|
if (!existsSync(dir)) errors.push(`Design directory does not exist: ${dir}`)
|
|
573
737
|
if (!hasDesignMd) errors.push("DESIGN.md is missing")
|
|
574
|
-
if (!
|
|
738
|
+
if (!hasDesignCss) warnings.push("design.css is missing; DESIGN.md CSS fallback remains compatibility-only and should be migrated.")
|
|
575
739
|
if (existsSync(dir)) assets = listDesignAssetsInDir(dir)
|
|
576
740
|
|
|
577
741
|
if (hasDesignMd) {
|
|
@@ -595,11 +759,13 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
595
759
|
|
|
596
760
|
if (hasPreview) {
|
|
597
761
|
const preview = readFileSync(previewPath, "utf-8")
|
|
762
|
+
const designCss = hasDesignCss ? readFileSync(cssPath, "utf-8") : ""
|
|
598
763
|
if (!preview.includes('<section class="slide"')) errors.push("preview.html must include slide sections")
|
|
599
764
|
if (!preview.includes("slide-qa=")) errors.push("preview.html slides must include slide-qa attributes")
|
|
600
765
|
if (!preview.includes("slide-canvas")) errors.push("preview.html must include .slide-canvas")
|
|
601
|
-
if (!
|
|
602
|
-
|
|
766
|
+
if (hasDesignCss && !previewReferencesDesignCss(preview)) errors.push("preview.html must reference design.css")
|
|
767
|
+
if (!hasFixedSizeCssRule(`${preview}\n${designCss}`, "slide-canvas")) {
|
|
768
|
+
errors.push("preview.html or design.css must define .slide-canvas CSS with width: 1920px and height: 1080px")
|
|
603
769
|
}
|
|
604
770
|
if (!hasSlideRole(preview, "cover")) errors.push('preview.html must include a slide section with data-slide-role="cover"')
|
|
605
771
|
if (!hasSlideRole(preview, "closing")) errors.push('preview.html must include a slide section with data-slide-role="closing"')
|
|
@@ -611,16 +777,24 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
611
777
|
if (missingLayoutPreviews.length > 0) {
|
|
612
778
|
warnings.push(`preview.html should mark layout fixtures with data-preview-layout; missing: ${missingLayoutPreviews.join(", ")}`)
|
|
613
779
|
}
|
|
614
|
-
const designText = hasDesignMd ? readFileSync(mdPath, "utf-8") : ""
|
|
615
|
-
const tokenWarnings = designContractTokenWarnings(`${designText}\n${preview}`)
|
|
616
|
-
warnings.push(...tokenWarnings)
|
|
617
780
|
}
|
|
781
|
+
if (hasDesignCss) {
|
|
782
|
+
const designCss = readFileSync(cssPath, "utf-8")
|
|
783
|
+
const missingCoreClasses = requiredDesignCssClasses().filter((className) => !cssTextHasClassSelector(designCss, className))
|
|
784
|
+
if (missingCoreClasses.length > 0) errors.push(`design.css must style core template classes; missing: ${missingCoreClasses.map((item) => `.${item}`).join(", ")}`)
|
|
785
|
+
errors.push(...missingDesignCssAssetErrors(designCss, dir))
|
|
786
|
+
}
|
|
787
|
+
const designText = hasDesignMd ? readFileSync(mdPath, "utf-8") : ""
|
|
788
|
+
const designCss = hasDesignCss ? readFileSync(cssPath, "utf-8") : ""
|
|
789
|
+
const tokenWarnings = designContractTokenWarnings(`${designText}\n${designCss}`)
|
|
790
|
+
warnings.push(...tokenWarnings)
|
|
618
791
|
|
|
619
792
|
return {
|
|
620
793
|
ok: errors.length === 0,
|
|
621
794
|
name,
|
|
622
795
|
path: dir,
|
|
623
796
|
hasDesignMd,
|
|
797
|
+
hasDesignCss,
|
|
624
798
|
hasPreview,
|
|
625
799
|
hasMarkers,
|
|
626
800
|
sections,
|
|
@@ -641,15 +815,128 @@ function designContractTokenWarnings(text: string): string[] {
|
|
|
641
815
|
{ label: "surface", pattern: /--surface|surface token|border token|shadow token/i },
|
|
642
816
|
]
|
|
643
817
|
for (const check of checks) {
|
|
644
|
-
if (!check.pattern.test(text)) warnings.push(`DESIGN.md/
|
|
818
|
+
if (!check.pattern.test(text)) warnings.push(`DESIGN.md/design.css should document ${check.label} design tokens or an equivalent contract`)
|
|
645
819
|
}
|
|
646
820
|
return warnings
|
|
647
821
|
}
|
|
648
822
|
|
|
823
|
+
function previewReferencesDesignCss(preview: string): boolean {
|
|
824
|
+
return /<link\b[^>]*href=["'][^"']*design\.css["'][^>]*>/i.test(preview)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function requiredDesignCssClasses(): string[] {
|
|
828
|
+
return [
|
|
829
|
+
"slide-canvas",
|
|
830
|
+
"template-slide",
|
|
831
|
+
"template-frame",
|
|
832
|
+
"template-title",
|
|
833
|
+
"template-card",
|
|
834
|
+
"template-visual-slot-panel",
|
|
835
|
+
]
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function cssTextHasClassSelector(css: string, className: string): boolean {
|
|
839
|
+
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
840
|
+
const ruleRe = /([^{}]+)\{[^{}]*\}/g
|
|
841
|
+
let match: RegExpExecArray | null
|
|
842
|
+
while ((match = ruleRe.exec(withoutComments)) !== null) {
|
|
843
|
+
if (cssRuleHasClassSelector(match[1] ?? "", className)) return true
|
|
844
|
+
}
|
|
845
|
+
return false
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function missingDesignCssAssetErrors(css: string, designDir: string): string[] {
|
|
849
|
+
const errors: string[] = []
|
|
850
|
+
const seen = new Set<string>()
|
|
851
|
+
const urlRe = /url\(\s*["']?([^"')]+)["']?\s*\)/gi
|
|
852
|
+
let match: RegExpExecArray | null
|
|
853
|
+
while ((match = urlRe.exec(css)) !== null) {
|
|
854
|
+
const raw = match[1].trim()
|
|
855
|
+
if (!raw || raw.startsWith("data:") || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith("#")) continue
|
|
856
|
+
const normalized = raw.replace(/^\.\/+/, "")
|
|
857
|
+
if (!normalized.startsWith("assets/")) continue
|
|
858
|
+
if (seen.has(normalized)) continue
|
|
859
|
+
seen.add(normalized)
|
|
860
|
+
if (!existsSync(join(designDir, ...normalized.split("/")))) errors.push(`design.css references missing asset: ${normalized}`)
|
|
861
|
+
}
|
|
862
|
+
return errors
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function extractCssCodeBlocks(markdown: string): string[] {
|
|
866
|
+
const blocks: string[] = []
|
|
867
|
+
const fenceRe = /```css\n([\s\S]*?)```/g
|
|
868
|
+
let match: RegExpExecArray | null
|
|
869
|
+
while ((match = fenceRe.exec(markdown)) !== null) {
|
|
870
|
+
const body = match[1].trim()
|
|
871
|
+
if (body) blocks.push(body)
|
|
872
|
+
}
|
|
873
|
+
return blocks
|
|
874
|
+
}
|
|
875
|
+
|
|
649
876
|
function designDraftDir(workspaceRoot: string, name: string): string {
|
|
650
877
|
return resolve(workspaceRoot, ".revela", "drafts", "designs", name)
|
|
651
878
|
}
|
|
652
879
|
|
|
880
|
+
function resolveDesignPreviewSourceDir(
|
|
881
|
+
workspaceRoot: string,
|
|
882
|
+
name: string,
|
|
883
|
+
requested?: "draft" | "installed" | "builtin",
|
|
884
|
+
): { designDir: string; source: "draft" | "installed" | "builtin" } {
|
|
885
|
+
const draftDir = designDraftDir(workspaceRoot, name)
|
|
886
|
+
if (requested === "draft") {
|
|
887
|
+
if (!existsSync(draftDir)) throw new Error(`Design draft '${name}' does not exist: ${draftDir}`)
|
|
888
|
+
return { designDir: draftDir, source: "draft" }
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const installedDir = resolveDesignDir(name)
|
|
892
|
+
if (requested === "installed" || requested === "builtin") {
|
|
893
|
+
if (!installedDir) throw new Error(`Design '${name}' is not installed`)
|
|
894
|
+
const source = requested === "builtin" ? "builtin" : installedDir.startsWith(SEED_DIR + sep) ? "builtin" : "installed"
|
|
895
|
+
return { designDir: installedDir, source }
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (existsSync(draftDir)) return { designDir: draftDir, source: "draft" }
|
|
899
|
+
if (!installedDir) throw new Error(`Design '${name}' is not available as a workspace draft or installed design`)
|
|
900
|
+
return { designDir: installedDir, source: installedDir.startsWith(SEED_DIR + sep) ? "builtin" : "installed" }
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function readDesignCssFromDir(name: string, designDir: string): { css: string; warnings: string[] } {
|
|
904
|
+
const cssPath = join(designDir, "design.css")
|
|
905
|
+
if (existsSync(cssPath)) return { css: readFileSync(cssPath, "utf-8"), warnings: [] }
|
|
906
|
+
|
|
907
|
+
const mdPath = join(designDir, "DESIGN.md")
|
|
908
|
+
const markdown = existsSync(mdPath) ? readFileSync(mdPath, "utf-8") : ""
|
|
909
|
+
const cssBlocks = extractCssCodeBlocks(markdown)
|
|
910
|
+
const css = [
|
|
911
|
+
"/* Generated preview compatibility CSS from DESIGN.md. Add design.css to make this design CSS-native. */",
|
|
912
|
+
templateDeckCss(),
|
|
913
|
+
...cssBlocks,
|
|
914
|
+
].join("\n\n")
|
|
915
|
+
return {
|
|
916
|
+
css,
|
|
917
|
+
warnings: [`Design '${name}' has no design.css; generated preview CSS from DESIGN.md compatibility blocks.`],
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function copyMissingBuiltInPreviewAssets(previewHtml: string, targetAssetsDir: string): void {
|
|
922
|
+
const fallbackAssetsDir = join(SEED_DIR, PREVIEW_FALLBACK_ASSET_DESIGN, "assets")
|
|
923
|
+
const assetRefs = new Set<string>()
|
|
924
|
+
const re = /(?:src|href)=["'](?:\.\/)?assets\/([^"']+)["']/g
|
|
925
|
+
let match: RegExpExecArray | null
|
|
926
|
+
while ((match = re.exec(previewHtml)) !== null) assetRefs.add(match[1])
|
|
927
|
+
if (assetRefs.size === 0) return
|
|
928
|
+
|
|
929
|
+
mkdirSync(targetAssetsDir, { recursive: true })
|
|
930
|
+
for (const rel of assetRefs) {
|
|
931
|
+
const target = resolve(targetAssetsDir, rel)
|
|
932
|
+
if (existsSync(target)) continue
|
|
933
|
+
const source = resolve(fallbackAssetsDir, rel)
|
|
934
|
+
if (!existsSync(source) || !statSync(source).isFile()) continue
|
|
935
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
936
|
+
cpSync(source, target)
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
653
940
|
function designDraftExists(workspaceRoot: string, name: string): boolean {
|
|
654
941
|
const dir = designDraftDir(workspaceRoot, name)
|
|
655
942
|
return existsSync(dir) && statSync(dir).isDirectory() && existsSync(join(dir, "DESIGN.md"))
|
|
@@ -812,6 +1099,12 @@ export interface DesignInventory {
|
|
|
812
1099
|
sections: string[]
|
|
813
1100
|
layouts: DesignInventoryLayout[]
|
|
814
1101
|
components: DesignInventoryComponent[]
|
|
1102
|
+
pageTemplates: Array<{
|
|
1103
|
+
templateId: string
|
|
1104
|
+
slots: string[]
|
|
1105
|
+
requiredClasses: string[]
|
|
1106
|
+
contractNotes: string[]
|
|
1107
|
+
}>
|
|
815
1108
|
assets: DesignPackageAssetInfo[]
|
|
816
1109
|
hasMarkers: boolean
|
|
817
1110
|
}
|
|
@@ -953,6 +1246,12 @@ export function getDesignInventory(designName?: string): DesignInventory {
|
|
|
953
1246
|
nesting: inferComponentNesting(componentName),
|
|
954
1247
|
contract: inferComponentContract(componentName, content),
|
|
955
1248
|
})),
|
|
1249
|
+
pageTemplates: listPageTemplateVocabulary().map((template) => ({
|
|
1250
|
+
templateId: template.templateId,
|
|
1251
|
+
slots: template.slots.map((slot) => slot.name),
|
|
1252
|
+
requiredClasses: template.requiredClasses,
|
|
1253
|
+
contractNotes: template.contractNotes,
|
|
1254
|
+
})),
|
|
956
1255
|
assets: listDesignAssetsInDir(designDir),
|
|
957
1256
|
hasMarkers,
|
|
958
1257
|
}
|
|
@@ -1225,6 +1524,7 @@ const UNIVERSAL_CLASSES = new Set([
|
|
|
1225
1524
|
"overlay",
|
|
1226
1525
|
"alt",
|
|
1227
1526
|
"strong",
|
|
1527
|
+
...PAGE_TEMPLATE_CLASSES,
|
|
1228
1528
|
])
|
|
1229
1529
|
|
|
1230
1530
|
/**
|
|
@@ -1360,6 +1660,9 @@ export function extractDesignClasses(designName?: string): DesignClassVocabulary
|
|
|
1360
1660
|
extractFromSection(content)
|
|
1361
1661
|
}
|
|
1362
1662
|
|
|
1663
|
+
const cssPath = join(designDir, "design.css")
|
|
1664
|
+
if (existsSync(cssPath)) extractFromCss(readFileSync(cssPath, "utf-8"))
|
|
1665
|
+
|
|
1363
1666
|
return { classes, prefixExemptions: DEFAULT_PREFIX_EXEMPTIONS }
|
|
1364
1667
|
}
|
|
1365
1668
|
|
|
@@ -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
|
}
|