@cyber-dash-tech/revela 0.18.15 → 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 +48 -45
- package/README.zh-CN.md +48 -45
- 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 +108 -1
- 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 +53 -9
- 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 +22 -5
- 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 +54 -9
- 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 +18 -21
- package/lib/commands/designs-preview.ts +3 -8
- package/lib/deck-html/foundation.ts +8 -8
- package/lib/design/designs.ts +385 -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 +9 -5
- package/lib/qa/artifact.ts +117 -7
- package/lib/qa/checks.ts +1 -1
- package/lib/qa/compliance.ts +5 -1
- package/lib/qa/component-contracts.ts +90 -0
- package/lib/runtime/index.ts +99 -3
- package/package.json +7 -15
- package/plugins/revela/.codex-plugin/plugin.json +4 -4
- package/plugins/revela/hooks/revela_guard.ts +35 -0
- package/plugins/revela/hooks/revela_post_write_notice.ts +39 -9
- package/plugins/revela/mcp/revela-server.ts +103 -7
- package/plugins/revela/skills/revela/SKILL.md +3 -3
- package/plugins/revela/skills/revela-design/SKILL.md +25 -14
- package/plugins/revela/skills/revela-helper/SKILL.md +3 -3
- package/plugins/revela/skills/revela-make-deck/SKILL.md +27 -12
- package/plugins/revela/skills/revela-research/SKILL.md +1 -0
- package/skill/SKILL.md +11 -2
- package/designs/lucent/preview.html +0 -612
- package/designs/monet/preview.html +0 -2293
- package/designs/starter/preview.html +0 -314
- package/designs/summit/preview.html +0 -2284
- package/plugins/revela/skills/revela-review/SKILL.md +0 -46
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[]
|
|
@@ -92,6 +100,7 @@ export interface ValidateDesignPackageResult {
|
|
|
92
100
|
components: string[]
|
|
93
101
|
assets: DesignPackageAssetInfo[]
|
|
94
102
|
errors: string[]
|
|
103
|
+
warnings: string[]
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
export interface DesignPackageAssetInput {
|
|
@@ -143,6 +152,37 @@ export interface DesignPreviewInfo {
|
|
|
143
152
|
hasPreview: boolean
|
|
144
153
|
}
|
|
145
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
|
+
|
|
146
186
|
// ---------------------------------------------------------------------------
|
|
147
187
|
// Seed
|
|
148
188
|
// ---------------------------------------------------------------------------
|
|
@@ -299,6 +339,115 @@ export function resolveDesignPreview(name?: string): DesignPreviewInfo {
|
|
|
299
339
|
}
|
|
300
340
|
}
|
|
301
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
|
+
|
|
302
451
|
/** Normalize and validate a design package name. */
|
|
303
452
|
export function normalizeDesignName(name: string): string {
|
|
304
453
|
const normalized = name.trim().toLowerCase()
|
|
@@ -312,10 +461,10 @@ export function normalizeDesignName(name: string): string {
|
|
|
312
461
|
export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesignPackageResult {
|
|
313
462
|
const name = normalizeDesignName(args.name)
|
|
314
463
|
const designMd = args.designMd?.trim()
|
|
464
|
+
const designCss = args.designCss?.trim()
|
|
315
465
|
const previewHtml = args.previewHtml?.trim()
|
|
316
466
|
|
|
317
467
|
if (!designMd) throw new Error("designMd is required")
|
|
318
|
-
if (!previewHtml) throw new Error("previewHtml is required")
|
|
319
468
|
|
|
320
469
|
const target = join(DESIGNS_DIR, name)
|
|
321
470
|
const existed = existsSync(target)
|
|
@@ -329,7 +478,8 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
|
|
|
329
478
|
}
|
|
330
479
|
mkdirSync(target, { recursive: true })
|
|
331
480
|
writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
|
|
332
|
-
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")
|
|
333
483
|
writeDesignAssets(target, args.assets)
|
|
334
484
|
|
|
335
485
|
const validation = validateDesignPackage(name)
|
|
@@ -352,10 +502,10 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
|
|
|
352
502
|
export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDesignPackageResult {
|
|
353
503
|
const name = normalizeDesignName(args.name)
|
|
354
504
|
const designMd = args.designMd?.trim()
|
|
505
|
+
const designCss = args.designCss?.trim()
|
|
355
506
|
const previewHtml = args.previewHtml?.trim()
|
|
356
507
|
|
|
357
508
|
if (!designMd) throw new Error("designMd is required")
|
|
358
|
-
if (!previewHtml) throw new Error("previewHtml is required")
|
|
359
509
|
|
|
360
510
|
const target = designDraftDir(args.workspaceRoot, name)
|
|
361
511
|
const existed = existsSync(target)
|
|
@@ -369,7 +519,8 @@ export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDes
|
|
|
369
519
|
}
|
|
370
520
|
mkdirSync(target, { recursive: true })
|
|
371
521
|
writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
|
|
372
|
-
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")
|
|
373
524
|
writeDesignAssets(target, args.assets)
|
|
374
525
|
|
|
375
526
|
const validation = validateDesignDraftPackage(args.workspaceRoot, name)
|
|
@@ -551,6 +702,7 @@ export function validateDesignPackage(nameInput: string): ValidateDesignPackageR
|
|
|
551
702
|
function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesignPackageResult {
|
|
552
703
|
let name = nameInput
|
|
553
704
|
const errors: string[] = []
|
|
705
|
+
const warnings: string[] = []
|
|
554
706
|
try {
|
|
555
707
|
name = normalizeDesignName(nameInput)
|
|
556
708
|
} catch (e) {
|
|
@@ -558,8 +710,10 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
558
710
|
}
|
|
559
711
|
|
|
560
712
|
const mdPath = join(dir, "DESIGN.md")
|
|
713
|
+
const cssPath = join(dir, "design.css")
|
|
561
714
|
const previewPath = join(dir, "preview.html")
|
|
562
715
|
const hasDesignMd = existsSync(mdPath)
|
|
716
|
+
const hasDesignCss = existsSync(cssPath)
|
|
563
717
|
const hasPreview = existsSync(previewPath)
|
|
564
718
|
let hasMarkers = false
|
|
565
719
|
let sections: string[] = []
|
|
@@ -569,7 +723,7 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
569
723
|
|
|
570
724
|
if (!existsSync(dir)) errors.push(`Design directory does not exist: ${dir}`)
|
|
571
725
|
if (!hasDesignMd) errors.push("DESIGN.md is missing")
|
|
572
|
-
if (!
|
|
726
|
+
if (!hasDesignCss) warnings.push("design.css is missing; DESIGN.md CSS fallback remains compatibility-only and should be migrated.")
|
|
573
727
|
if (existsSync(dir)) assets = listDesignAssetsInDir(dir)
|
|
574
728
|
|
|
575
729
|
if (hasDesignMd) {
|
|
@@ -593,11 +747,13 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
593
747
|
|
|
594
748
|
if (hasPreview) {
|
|
595
749
|
const preview = readFileSync(previewPath, "utf-8")
|
|
750
|
+
const designCss = hasDesignCss ? readFileSync(cssPath, "utf-8") : ""
|
|
596
751
|
if (!preview.includes('<section class="slide"')) errors.push("preview.html must include slide sections")
|
|
597
752
|
if (!preview.includes("slide-qa=")) errors.push("preview.html slides must include slide-qa attributes")
|
|
598
753
|
if (!preview.includes("slide-canvas")) errors.push("preview.html must include .slide-canvas")
|
|
599
|
-
if (!
|
|
600
|
-
|
|
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")
|
|
601
757
|
}
|
|
602
758
|
if (!hasSlideRole(preview, "cover")) errors.push('preview.html must include a slide section with data-slide-role="cover"')
|
|
603
759
|
if (!hasSlideRole(preview, "closing")) errors.push('preview.html must include a slide section with data-slide-role="closing"')
|
|
@@ -605,13 +761,28 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
605
761
|
if (missingComponents.length > 0) {
|
|
606
762
|
errors.push(`preview.html must showcase every @component; missing: ${missingComponents.join(", ")}`)
|
|
607
763
|
}
|
|
764
|
+
const missingLayoutPreviews = layouts.filter((layout) => !hasDataAttribute(preview, "data-preview-layout", layout))
|
|
765
|
+
if (missingLayoutPreviews.length > 0) {
|
|
766
|
+
warnings.push(`preview.html should mark layout fixtures with data-preview-layout; missing: ${missingLayoutPreviews.join(", ")}`)
|
|
767
|
+
}
|
|
608
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)
|
|
609
779
|
|
|
610
780
|
return {
|
|
611
781
|
ok: errors.length === 0,
|
|
612
782
|
name,
|
|
613
783
|
path: dir,
|
|
614
784
|
hasDesignMd,
|
|
785
|
+
hasDesignCss,
|
|
615
786
|
hasPreview,
|
|
616
787
|
hasMarkers,
|
|
617
788
|
sections,
|
|
@@ -619,13 +790,141 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
|
|
|
619
790
|
components,
|
|
620
791
|
assets,
|
|
621
792
|
errors,
|
|
793
|
+
warnings,
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function designContractTokenWarnings(text: string): string[] {
|
|
798
|
+
const warnings: string[] = []
|
|
799
|
+
const checks = [
|
|
800
|
+
{ label: "grid", pattern: /--grid-|grid columns|grid-column|column line|safe area|safe-area/i },
|
|
801
|
+
{ label: "spacing", pattern: /--space-|spacing scale|baseline|rhythm unit|gap scale/i },
|
|
802
|
+
{ label: "type scale", pattern: /--font-size-|type scale|typographic scale|line-height/i },
|
|
803
|
+
{ label: "surface", pattern: /--surface|surface token|border token|shadow token/i },
|
|
804
|
+
]
|
|
805
|
+
for (const check of checks) {
|
|
806
|
+
if (!check.pattern.test(text)) warnings.push(`DESIGN.md/design.css should document ${check.label} design tokens or an equivalent contract`)
|
|
622
807
|
}
|
|
808
|
+
return warnings
|
|
809
|
+
}
|
|
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
|
|
623
862
|
}
|
|
624
863
|
|
|
625
864
|
function designDraftDir(workspaceRoot: string, name: string): string {
|
|
626
865
|
return resolve(workspaceRoot, ".revela", "drafts", "designs", name)
|
|
627
866
|
}
|
|
628
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
|
+
|
|
629
928
|
function designDraftExists(workspaceRoot: string, name: string): boolean {
|
|
630
929
|
const dir = designDraftDir(workspaceRoot, name)
|
|
631
930
|
return existsSync(dir) && statSync(dir).isDirectory() && existsSync(join(dir, "DESIGN.md"))
|
|
@@ -764,6 +1063,23 @@ export interface DesignInventoryComponent {
|
|
|
764
1063
|
acceptsChildren: boolean
|
|
765
1064
|
allowedChildren?: string[]
|
|
766
1065
|
}
|
|
1066
|
+
contract?: DesignComponentContract
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export interface DesignComponentContractVariant {
|
|
1070
|
+
name: string
|
|
1071
|
+
requiredDescendantClasses: string[]
|
|
1072
|
+
repeatedItemClass?: string
|
|
1073
|
+
requiredItemClasses?: string[]
|
|
1074
|
+
requireAlternatingClasses?: string[]
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
export interface DesignComponentContract {
|
|
1078
|
+
component: string
|
|
1079
|
+
kind: "structure"
|
|
1080
|
+
requiredRootClasses: string[]
|
|
1081
|
+
variants: DesignComponentContractVariant[]
|
|
1082
|
+
guidance: string
|
|
767
1083
|
}
|
|
768
1084
|
|
|
769
1085
|
export interface DesignInventory {
|
|
@@ -771,6 +1087,12 @@ export interface DesignInventory {
|
|
|
771
1087
|
sections: string[]
|
|
772
1088
|
layouts: DesignInventoryLayout[]
|
|
773
1089
|
components: DesignInventoryComponent[]
|
|
1090
|
+
pageTemplates: Array<{
|
|
1091
|
+
templateId: string
|
|
1092
|
+
slots: string[]
|
|
1093
|
+
requiredClasses: string[]
|
|
1094
|
+
contractNotes: string[]
|
|
1095
|
+
}>
|
|
774
1096
|
assets: DesignPackageAssetInfo[]
|
|
775
1097
|
hasMarkers: boolean
|
|
776
1098
|
}
|
|
@@ -838,17 +1160,19 @@ export function generateComponentIndex(components: Record<string, string>): stri
|
|
|
838
1160
|
const desc = firstLine
|
|
839
1161
|
? firstLine.replace(/^#+\s*/, "").replace(/\(.*?\)/, "").trim()
|
|
840
1162
|
: ""
|
|
841
|
-
|
|
1163
|
+
const contract = inferComponentContract(name, body)
|
|
1164
|
+
const contractLabel = contract ? "✓" : "—"
|
|
1165
|
+
return `| \`${name}\` | ${contractLabel} | ${desc} |`
|
|
842
1166
|
})
|
|
843
1167
|
|
|
844
1168
|
return [
|
|
845
1169
|
"### Component Index",
|
|
846
1170
|
"",
|
|
847
|
-
"| Component | Description |",
|
|
848
|
-
"
|
|
1171
|
+
"| Component | Contract | Description |",
|
|
1172
|
+
"|---|---|---|",
|
|
849
1173
|
...rows,
|
|
850
1174
|
"",
|
|
851
|
-
"_Use `revela_design_read_component` with `component: \"<name>\"` to get full CSS/HTML
|
|
1175
|
+
"_Use `revela_design_read_component` with `component: \"<name>\"` to get full CSS/HTML and any structure contract for a component._",
|
|
852
1176
|
].join("\n")
|
|
853
1177
|
}
|
|
854
1178
|
|
|
@@ -908,12 +1232,55 @@ export function getDesignInventory(designName?: string): DesignInventory {
|
|
|
908
1232
|
name: componentName,
|
|
909
1233
|
description: designBlockDescription(content),
|
|
910
1234
|
nesting: inferComponentNesting(componentName),
|
|
1235
|
+
contract: inferComponentContract(componentName, content),
|
|
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,
|
|
911
1242
|
})),
|
|
912
1243
|
assets: listDesignAssetsInDir(designDir),
|
|
913
1244
|
hasMarkers,
|
|
914
1245
|
}
|
|
915
1246
|
}
|
|
916
1247
|
|
|
1248
|
+
export function extractDesignComponentContracts(designName?: string): DesignComponentContract[] {
|
|
1249
|
+
const name = normalizeDesignName(designName || activeDesign())
|
|
1250
|
+
const designDir = resolveDesignDir(name)
|
|
1251
|
+
if (!designDir) return []
|
|
1252
|
+
const mdPath = join(designDir, "DESIGN.md")
|
|
1253
|
+
const text = readFileSync(mdPath, "utf-8")
|
|
1254
|
+
const { body } = parseFrontmatter(text)
|
|
1255
|
+
const { components, hasMarkers } = parseDesignSections(body)
|
|
1256
|
+
if (!hasMarkers) return []
|
|
1257
|
+
return Object.entries(components)
|
|
1258
|
+
.map(([componentName, content]) => inferComponentContract(componentName, content))
|
|
1259
|
+
.filter((contract): contract is DesignComponentContract => Boolean(contract))
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function inferComponentContract(name: string, content: string): DesignComponentContract | undefined {
|
|
1263
|
+
const mentions = (value: string) => content.includes(value)
|
|
1264
|
+
|
|
1265
|
+
if (name === "roadmap-vertical" && ["tjv-axis", "tjv-item", "tjv-axis-dot", "tjv-stem", "tjv-tip-dot", "tjv-label"].every(mentions)) {
|
|
1266
|
+
return {
|
|
1267
|
+
component: name,
|
|
1268
|
+
kind: "structure",
|
|
1269
|
+
requiredRootClasses: ["roadmap-vertical"],
|
|
1270
|
+
variants: [{
|
|
1271
|
+
name: "timeline-journey-vertical",
|
|
1272
|
+
requiredDescendantClasses: ["tjv-axis"],
|
|
1273
|
+
repeatedItemClass: "tjv-item",
|
|
1274
|
+
requiredItemClasses: ["tjv-axis-dot", "tjv-stem", "tjv-tip-dot", "tjv-label"],
|
|
1275
|
+
requireAlternatingClasses: ["tjv-item--left", "tjv-item--right"],
|
|
1276
|
+
}],
|
|
1277
|
+
guidance: "Use structured milestone props/content to render axis-dot -> stem -> tip-dot -> label rows. Do not hand-roll a simplified axis plus labels.",
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return undefined
|
|
1282
|
+
}
|
|
1283
|
+
|
|
917
1284
|
function inferLayoutSlots(name: string, content: string): string[] {
|
|
918
1285
|
const slots = new Set<string>()
|
|
919
1286
|
const known = ["fullbleed", "left", "right", "top", "bottom", "main", "footer", "content", "overlay", "center"]
|
|
@@ -1145,6 +1512,7 @@ const UNIVERSAL_CLASSES = new Set([
|
|
|
1145
1512
|
"overlay",
|
|
1146
1513
|
"alt",
|
|
1147
1514
|
"strong",
|
|
1515
|
+
...PAGE_TEMPLATE_CLASSES,
|
|
1148
1516
|
])
|
|
1149
1517
|
|
|
1150
1518
|
/**
|
|
@@ -1280,6 +1648,9 @@ export function extractDesignClasses(designName?: string): DesignClassVocabulary
|
|
|
1280
1648
|
extractFromSection(content)
|
|
1281
1649
|
}
|
|
1282
1650
|
|
|
1651
|
+
const cssPath = join(designDir, "design.css")
|
|
1652
|
+
if (existsSync(cssPath)) extractFromCss(readFileSync(cssPath, "utf-8"))
|
|
1653
|
+
|
|
1283
1654
|
return { classes, prefixExemptions: DEFAULT_PREFIX_EXEMPTIONS }
|
|
1284
1655
|
}
|
|
1285
1656
|
|