@cyber-dash-tech/revela 0.19.7 → 0.19.9

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.
@@ -99,24 +99,24 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
99
99
  linear-gradient(135deg, #07111f, #101a2b 62%, #243a73);
100
100
  }
101
101
 
102
- .template-slide[data-design="starter"][data-template="cover"] .slide-canvas {
102
+ .template-slide[data-template="cover"] .slide-canvas {
103
103
  background:
104
104
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(7,17,31,0.42) 52%, rgba(7,17,31,0.24)),
105
105
  url("./assets/cover-background.jpg") center center / cover no-repeat;
106
106
  }
107
- .template-slide[data-design="starter"][data-template="agenda"] .slide-canvas {
107
+ .template-slide[data-template="agenda"] .slide-canvas {
108
108
  background:
109
109
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(7,17,31,0.58) 52%, rgba(7,17,31,0.32)),
110
110
  url("./assets/cover-background.jpg") center center / cover no-repeat;
111
111
  }
112
- .template-slide[data-design="starter"][data-template="section-divider"] .slide-canvas {
112
+ .template-slide[data-template="section-divider"] .slide-canvas {
113
113
  background:
114
114
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(16,26,43,0.62) 58%, rgba(36,58,115,0.36)),
115
115
  url("./assets/cover-background.jpg") center center / cover no-repeat;
116
116
  }
117
117
  .template-slide[data-template="closing"] .slide-canvas { background: linear-gradient(135deg, #07111f, #315eea 58%, #18a8d8); }
118
118
 
119
- .template-slide[data-design="starter"][data-template="closing"] .slide-canvas {
119
+ .template-slide[data-template="closing"] .slide-canvas {
120
120
  background:
121
121
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(49,94,234,0.42) 58%, rgba(24,168,216,0.24)),
122
122
  url("./assets/closing-background.jpg") center center / cover no-repeat;
@@ -162,10 +162,13 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
162
162
  .text-panel-formula-fallback { display: block; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82em; line-height: 1.35; color: inherit; }
163
163
  .text-panel-formula-caption { margin: 0; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
164
164
  .template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
165
- .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
165
+ .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid var(--line); }
166
+ .template-text-panel--color .template-chart-takeaway-item { border-top-color: rgba(255,255,255,0.24); }
166
167
  .template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
167
- .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: white; }
168
- .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: rgba(255,255,255,0.78); }
168
+ .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: var(--text-primary); }
169
+ .template-text-panel--color .template-chart-takeaway-item h3 { color: white; }
170
+ .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: var(--text-secondary); }
171
+ .template-text-panel--color .template-chart-takeaway-item p { color: rgba(255,255,255,0.78); }
169
172
  .template-bar { flex: 1; background: linear-gradient(180deg, var(--accent-primary), var(--accent-cyan)); min-height: 80px; }
170
173
  .template-table-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; }
171
174
  .template-table-layout .template-side-panel { grid-column: 1; grid-row: 1; }
@@ -333,14 +336,14 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
333
336
  .template-table,
334
337
  .template-insight-panel,
335
338
  .template-catalog-panel { box-shadow: none; }
336
- .template-slide[data-design="starter"][data-template="cover"] .slide-canvas,
337
- .template-slide[data-design="starter"][data-template="agenda"] .slide-canvas,
338
- .template-slide[data-design="starter"][data-template="section-divider"] .slide-canvas {
339
+ .template-slide[data-template="cover"] .slide-canvas,
340
+ .template-slide[data-template="agenda"] .slide-canvas,
341
+ .template-slide[data-template="section-divider"] .slide-canvas {
339
342
  background:
340
343
  linear-gradient(90deg, rgba(16,19,22,0.82), rgba(16,19,22,0.42) 52%, rgba(16,19,22,0.18)),
341
344
  url("./assets/cover-background.jpg") center center / cover no-repeat;
342
345
  }
343
- .template-slide[data-design="starter"][data-template="closing"] .slide-canvas {
346
+ .template-slide[data-template="closing"] .slide-canvas {
344
347
  background:
345
348
  linear-gradient(90deg, rgba(16,19,22,0.72), rgba(47,115,218,0.34) 58%, rgba(43,159,195,0.22)),
346
349
  url("./assets/closing-background.jpg") center center / cover no-repeat;
@@ -99,24 +99,24 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
99
99
  linear-gradient(135deg, #07111f, #101a2b 62%, #243a73);
100
100
  }
101
101
 
102
- .template-slide[data-design="summit"][data-template="cover"] .slide-canvas {
102
+ .template-slide[data-template="cover"] .slide-canvas {
103
103
  background:
104
104
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(7,17,31,0.42) 52%, rgba(7,17,31,0.24)),
105
105
  url("./assets/cover-background.jpg") center center / cover no-repeat;
106
106
  }
107
- .template-slide[data-design="summit"][data-template="agenda"] .slide-canvas {
107
+ .template-slide[data-template="agenda"] .slide-canvas {
108
108
  background:
109
109
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(7,17,31,0.58) 52%, rgba(7,17,31,0.32)),
110
110
  url("./assets/cover-background.jpg") center center / cover no-repeat;
111
111
  }
112
- .template-slide[data-design="summit"][data-template="section-divider"] .slide-canvas {
112
+ .template-slide[data-template="section-divider"] .slide-canvas {
113
113
  background:
114
114
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(16,26,43,0.62) 58%, rgba(36,58,115,0.36)),
115
115
  url("./assets/cover-background.jpg") center center / cover no-repeat;
116
116
  }
117
117
  .template-slide[data-template="closing"] .slide-canvas { background: linear-gradient(135deg, #07111f, #315eea 58%, #18a8d8); }
118
118
 
119
- .template-slide[data-design="summit"][data-template="closing"] .slide-canvas {
119
+ .template-slide[data-template="closing"] .slide-canvas {
120
120
  background:
121
121
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(49,94,234,0.42) 58%, rgba(24,168,216,0.24)),
122
122
  url("./assets/closing-background.jpg") center center / cover no-repeat;
@@ -162,10 +162,13 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
162
162
  .text-panel-formula-fallback { display: block; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82em; line-height: 1.35; color: inherit; }
163
163
  .text-panel-formula-caption { margin: 0; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
164
164
  .template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
165
- .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
165
+ .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid var(--line); }
166
+ .template-text-panel--color .template-chart-takeaway-item { border-top-color: rgba(255,255,255,0.24); }
166
167
  .template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
167
- .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: white; }
168
- .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: rgba(255,255,255,0.78); }
168
+ .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: var(--text-primary); }
169
+ .template-text-panel--color .template-chart-takeaway-item h3 { color: white; }
170
+ .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: var(--text-secondary); }
171
+ .template-text-panel--color .template-chart-takeaway-item p { color: rgba(255,255,255,0.78); }
169
172
  .template-bar { flex: 1; background: linear-gradient(180deg, var(--accent-primary), var(--accent-cyan)); min-height: 80px; }
170
173
  .template-table-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; }
171
174
  .template-table-layout .template-side-panel { grid-column: 1; grid-row: 1; }
@@ -345,14 +348,14 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
345
348
  .template-timeline-layout .template-text-panel--color {
346
349
  background: linear-gradient(135deg, #176c63 0%, #2a8f84 54%, #b0822e 125%);
347
350
  }
348
- .template-slide[data-design="summit"][data-template="cover"] .slide-canvas,
349
- .template-slide[data-design="summit"][data-template="agenda"] .slide-canvas,
350
- .template-slide[data-design="summit"][data-template="section-divider"] .slide-canvas {
351
+ .template-slide[data-template="cover"] .slide-canvas,
352
+ .template-slide[data-template="agenda"] .slide-canvas,
353
+ .template-slide[data-template="section-divider"] .slide-canvas {
351
354
  background:
352
355
  linear-gradient(90deg, rgba(10,16,16,0.84), rgba(10,16,16,0.46) 52%, rgba(10,16,16,0.18)),
353
356
  url("./assets/cover-background.jpg") center center / cover no-repeat;
354
357
  }
355
- .template-slide[data-design="summit"][data-template="closing"] .slide-canvas {
358
+ .template-slide[data-template="closing"] .slide-canvas {
356
359
  background:
357
360
  linear-gradient(90deg, rgba(10,16,16,0.78), rgba(23,108,99,0.4) 58%, rgba(176,130,46,0.22)),
358
361
  url("./assets/closing-background.jpg") center center / cover no-repeat;
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "fs"
2
- import { dirname, isAbsolute, normalize, resolve } from "path"
2
+ import { basename, dirname, isAbsolute, normalize, resolve } from "path"
3
3
  import { activeDesign, getDesignSection, materializeDesignCssSnapshot } from "../design/designs"
4
4
 
5
5
  export type DeckFoundationMode = "create" | "repair"
@@ -48,7 +48,12 @@ export function createDeckFoundation(input: CreateDeckFoundationInput): CreateDe
48
48
  const foundation = getDesignSection("foundation", design)
49
49
  const parts = parseFoundationParts(foundation)
50
50
  if (parts.scriptBlocks.length === 0) throw new Error(`Design '${design}' foundation does not include a SlidePresentation JavaScript code block.`)
51
- const snapshot = materializeDesignCssSnapshot({ workspaceRoot: input.workspaceRoot, outputPath, designName: design })
51
+ const snapshot = materializeDesignCssSnapshot({
52
+ workspaceRoot: input.workspaceRoot,
53
+ outputPath,
54
+ designName: design,
55
+ snapshotName: activeDesignSnapshotName(outputPath),
56
+ })
52
57
 
53
58
  const html = renderFoundationHtml({
54
59
  language: input.language || "en",
@@ -69,6 +74,7 @@ export function createDeckFoundation(input: CreateDeckFoundationInput): CreateDe
69
74
  "design:foundation",
70
75
  parts.fontLinks.length > 0 ? "foundation:font-links" : "foundation:font-links:none",
71
76
  snapshot.generatedFallback ? "design-css:fallback" : "design-css:snapshot",
77
+ `design-css:active-snapshot:${snapshot.snapshotName}`,
72
78
  snapshot.assetCount > 0 ? "design-assets:snapshot" : "design-assets:none",
73
79
  "foundation:SlidePresentation",
74
80
  ],
@@ -81,6 +87,14 @@ export function createDeckFoundation(input: CreateDeckFoundationInput): CreateDe
81
87
  }
82
88
  }
83
89
 
90
+ export function activeDesignSnapshotName(outputPath: string): string {
91
+ const stem = basename(normalizeOutputPath(outputPath), ".html")
92
+ .toLowerCase()
93
+ .replace(/[^a-z0-9-]+/g, "-")
94
+ .replace(/^-+|-+$/g, "")
95
+ return `${stem || "deck"}-active`
96
+ }
97
+
84
98
  export function normalizeOutputPath(outputPath: string): string {
85
99
  const trimmed = outputPath.trim()
86
100
  if (!trimmed) throw new Error("outputPath is required")
@@ -180,6 +180,7 @@ export interface MaterializeDesignPreviewResult {
180
180
  export interface DesignCssSnapshotResult {
181
181
  ok: true
182
182
  design: string
183
+ snapshotName: string
183
184
  sourcePath: string
184
185
  snapshotDir: string
185
186
  cssPath: string
@@ -425,11 +426,13 @@ export function materializeDesignCssSnapshot(input: {
425
426
  workspaceRoot: string
426
427
  outputPath: string
427
428
  designName?: string
429
+ snapshotName?: string
428
430
  }): DesignCssSnapshotResult {
429
431
  const designName = normalizeDesignName(input.designName || activeDesign())
432
+ const snapshotName = normalizeDesignSnapshotName(input.snapshotName || designName)
430
433
  const designDir = resolveDesignPackageDir(designName)
431
434
  const outputDir = dirname(normalize(input.outputPath))
432
- const snapshotRelDir = normalize(join(outputDir, "_revela-design", designName)).replace(/\\/g, "/")
435
+ const snapshotRelDir = normalize(join(outputDir, "_revela-design", snapshotName)).replace(/\\/g, "/")
433
436
  const snapshotDir = resolve(input.workspaceRoot, snapshotRelDir)
434
437
  const cssPath = join(snapshotDir, "design.css")
435
438
  const cssRead = readDesignCss(designName)
@@ -450,6 +453,7 @@ export function materializeDesignCssSnapshot(input: {
450
453
  return {
451
454
  ok: true,
452
455
  design: designName,
456
+ snapshotName,
453
457
  sourcePath: cssRead.path || join(designDir, "DESIGN.md"),
454
458
  snapshotDir,
455
459
  cssPath,
@@ -460,6 +464,14 @@ export function materializeDesignCssSnapshot(input: {
460
464
  }
461
465
  }
462
466
 
467
+ function normalizeDesignSnapshotName(name: string): string {
468
+ const normalized = name.trim().toLowerCase()
469
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(normalized)) {
470
+ throw new Error("Design snapshot name must be kebab-case using lowercase letters, numbers, and hyphens")
471
+ }
472
+ return normalized
473
+ }
474
+
463
475
  /** Normalize and validate a design package name. */
464
476
  export function normalizeDesignName(name: string): string {
465
477
  const normalized = name.trim().toLowerCase()
@@ -475,23 +475,23 @@ export function templateDeckCss(input: { designName?: string; designAssetBasePat
475
475
  const lucentCoverBackground = designName === "lucent" && assetBasePath ? cssUrl(`${assetBasePath}/cover-background.jpg`) : ""
476
476
  const lucentClosingBackground = designName === "lucent" && assetBasePath ? cssUrl(`${assetBasePath}/closing-background.jpg`) : ""
477
477
  const lucentCoverBackgroundCss = lucentCoverBackground ? `
478
- .template-slide[data-design="lucent"][data-template="cover"] .slide-canvas {
478
+ .template-slide[data-template="cover"] .slide-canvas {
479
479
  background:
480
480
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(7,17,31,0.42) 52%, rgba(7,17,31,0.24)),
481
481
  url("${lucentCoverBackground}") center center / cover no-repeat;
482
482
  }
483
- .template-slide[data-design="lucent"][data-template="agenda"] .slide-canvas {
483
+ .template-slide[data-template="agenda"] .slide-canvas {
484
484
  background:
485
485
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(7,17,31,0.58) 52%, rgba(7,17,31,0.32)),
486
486
  url("${lucentCoverBackground}") center center / cover no-repeat;
487
487
  }
488
- .template-slide[data-design="lucent"][data-template="section-divider"] .slide-canvas {
488
+ .template-slide[data-template="section-divider"] .slide-canvas {
489
489
  background:
490
490
  linear-gradient(90deg, rgba(7,17,31,0.86), rgba(16,26,43,0.62) 58%, rgba(36,58,115,0.36)),
491
491
  url("${lucentCoverBackground}") center center / cover no-repeat;
492
492
  }` : ""
493
493
  const lucentClosingBackgroundCss = lucentClosingBackground ? `
494
- .template-slide[data-design="lucent"][data-template="closing"] .slide-canvas {
494
+ .template-slide[data-template="closing"] .slide-canvas {
495
495
  background:
496
496
  linear-gradient(90deg, rgba(7,17,31,0.82), rgba(49,94,234,0.42) 58%, rgba(24,168,216,0.24)),
497
497
  url("${lucentClosingBackground}") center center / cover no-repeat;
@@ -591,10 +591,13 @@ ${lucentClosingBackgroundCss}
591
591
  .template-text-panel-formula-caption { margin: 0; font-size: 14px; line-height: 1.35; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
592
592
  .template-text-panel--color .template-text-panel-formula-caption { color: rgba(255,255,255,0.72); }
593
593
  .template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
594
- .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
594
+ .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid var(--line); }
595
+ .template-text-panel--color .template-chart-takeaway-item { border-top-color: rgba(255,255,255,0.24); }
595
596
  .template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
596
- .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: white; }
597
- .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: rgba(255,255,255,0.78); }
597
+ .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: var(--text-primary); }
598
+ .template-text-panel--color .template-chart-takeaway-item h3 { color: white; }
599
+ .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: var(--text-secondary); }
600
+ .template-text-panel--color .template-chart-takeaway-item p { color: rgba(255,255,255,0.78); }
598
601
  .template-bar { flex: 1; background: linear-gradient(180deg, var(--accent-primary), var(--accent-cyan)); min-height: 80px; }
599
602
  .template-table-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; }
600
603
  .template-table-layout .template-side-panel { grid-column: 1; grid-row: 1; }
@@ -577,25 +577,177 @@ async function exportSlidePptx(
577
577
  host.style.zIndex = "2147483647"
578
578
  host.style.background = "transparent"
579
579
 
580
- const exportTarget = target.cloneNode(true) as HTMLElement
580
+ const exportTarget = targetSlide.cloneNode(true) as HTMLElement
581
581
  exportTarget.style.width = "1920px"
582
+ exportTarget.style.minWidth = "1920px"
582
583
  exportTarget.style.height = "1080px"
584
+ exportTarget.style.minHeight = "1080px"
583
585
  exportTarget.style.position = "relative"
584
586
  exportTarget.style.left = "0"
585
587
  exportTarget.style.top = "0"
588
+ exportTarget.style.margin = "0"
589
+ exportTarget.style.display = "flex"
590
+ exportTarget.style.alignItems = "center"
591
+ exportTarget.style.justifyContent = "center"
592
+ exportTarget.style.overflow = "hidden"
586
593
  exportTarget.style.transform = "none"
587
594
  exportTarget.style.transformOrigin = "top left"
588
595
  exportTarget.style.transition = "none"
589
596
  exportTarget.style.animation = "none"
597
+ const exportCanvas = exportTarget.querySelector(".slide-canvas") as HTMLElement | null
598
+ if (!exportCanvas) {
599
+ host.remove()
600
+ throw new Error(`Missing cloned .slide-canvas for slide ${index + 1}`)
601
+ }
602
+ exportCanvas.style.width = "1920px"
603
+ exportCanvas.style.minWidth = "1920px"
604
+ exportCanvas.style.height = "1080px"
605
+ exportCanvas.style.minHeight = "1080px"
606
+ exportCanvas.style.position = "relative"
607
+ exportCanvas.style.left = "0"
608
+ exportCanvas.style.top = "0"
609
+ exportCanvas.style.flexShrink = "0"
610
+ exportCanvas.style.transform = "none"
611
+ exportCanvas.style.transformOrigin = "top left"
612
+ exportCanvas.style.transition = "none"
613
+ exportCanvas.style.animation = "none"
614
+
615
+ const splitCssList = (value: string): string[] => {
616
+ const parts: string[] = []
617
+ let depth = 0
618
+ let quote: string | null = null
619
+ let start = 0
620
+ for (let i = 0; i < value.length; i += 1) {
621
+ const char = value[i]
622
+ if (quote) {
623
+ if (char === quote && value[i - 1] !== "\\") quote = null
624
+ continue
625
+ }
626
+ if (char === "\"" || char === "'") {
627
+ quote = char
628
+ continue
629
+ }
630
+ if (char === "(") depth += 1
631
+ if (char === ")") depth = Math.max(0, depth - 1)
632
+ if (char === "," && depth === 0) {
633
+ parts.push(value.slice(start, i).trim())
634
+ start = i + 1
635
+ }
636
+ }
637
+ parts.push(value.slice(start).trim())
638
+ return parts.filter(Boolean)
639
+ }
640
+
641
+ const extractCssUrl = (value: string): string | null => {
642
+ const match = value.match(/url\((["']?)(.*?)\1\)/i)
643
+ return match?.[2] ?? null
644
+ }
645
+
646
+ const normalizeBackgroundLayersForPptx = (canvas: HTMLElement, sourceStyle: CSSStyleDeclaration) => {
647
+ const computed = sourceStyle
648
+ const imageLayers = splitCssList(computed.backgroundImage)
649
+ const hasUrlLayer = imageLayers.some((layer) => /url\(/i.test(layer))
650
+ const paddingLeft = Number.parseFloat(computed.paddingLeft) || 0
651
+ const paddingRight = Number.parseFloat(computed.paddingRight) || 0
652
+ const paddingTop = Number.parseFloat(computed.paddingTop) || 0
653
+ const paddingBottom = Number.parseFloat(computed.paddingBottom) || 0
654
+ const hasPadding = paddingLeft > 0 || paddingRight > 0 || paddingTop > 0 || paddingBottom > 0
655
+ if (!hasUrlLayer && !hasPadding) return
656
+
657
+ const sizeLayers = splitCssList(computed.backgroundSize)
658
+ const positionLayers = splitCssList(computed.backgroundPosition)
659
+ const repeatLayers = splitCssList(computed.backgroundRepeat)
660
+ const layerCount = imageLayers.length
661
+ const layers = hasUrlLayer ? document.createElement("div") : null
662
+ if (layers) {
663
+ layers.setAttribute("data-revela-pptx-background-layers", "true")
664
+ layers.style.position = "absolute"
665
+ layers.style.inset = "0"
666
+ layers.style.pointerEvents = "none"
667
+ layers.style.zIndex = "0"
668
+ layers.style.overflow = "hidden"
669
+ }
670
+
671
+ if (layers) imageLayers.forEach((layer, layerIndex) => {
672
+ if (layer === "none") return
673
+ const zIndex = String(layerCount - layerIndex)
674
+ const url = extractCssUrl(layer)
675
+ const size = sizeLayers[layerIndex] ?? sizeLayers[0] ?? "auto"
676
+ const position = positionLayers[layerIndex] ?? positionLayers[0] ?? "50% 50%"
677
+ const repeat = repeatLayers[layerIndex] ?? repeatLayers[0] ?? "repeat"
678
+ if (url) {
679
+ const img = document.createElement("img")
680
+ img.src = url
681
+ img.alt = ""
682
+ img.setAttribute("aria-hidden", "true")
683
+ img.style.position = "absolute"
684
+ img.style.inset = "0"
685
+ img.style.width = "100%"
686
+ img.style.height = "100%"
687
+ img.style.display = "block"
688
+ img.style.zIndex = zIndex
689
+ img.style.objectFit = size.includes("contain") ? "contain" : "cover"
690
+ img.style.objectPosition = position
691
+ if (!repeat.includes("no-repeat") && !size.includes("cover") && !size.includes("contain")) {
692
+ img.style.objectFit = "fill"
693
+ }
694
+ layers.appendChild(img)
695
+ return
696
+ }
697
+
698
+ const gradient = document.createElement("div")
699
+ gradient.style.position = "absolute"
700
+ gradient.style.inset = "0"
701
+ gradient.style.zIndex = zIndex
702
+ gradient.style.backgroundImage = layer
703
+ gradient.style.backgroundSize = size
704
+ gradient.style.backgroundPosition = position
705
+ gradient.style.backgroundRepeat = repeat
706
+ layers.appendChild(gradient)
707
+ })
708
+
709
+ if (layers && !layers.childElementCount) return
710
+
711
+ const content = document.createElement("div")
712
+ content.setAttribute("data-revela-pptx-content-layer", "true")
713
+ content.style.position = "absolute"
714
+ content.style.zIndex = String(layerCount + 1)
715
+ content.style.left = `${paddingLeft}px`
716
+ content.style.top = `${paddingTop}px`
717
+ content.style.width = `${Math.max(1, 1920 - paddingLeft - paddingRight)}px`
718
+ content.style.height = `${Math.max(1, 1080 - paddingTop - paddingBottom)}px`
719
+ content.style.minHeight = "0"
720
+ content.style.boxSizing = "border-box"
721
+ while (canvas.firstChild) {
722
+ content.appendChild(canvas.firstChild)
723
+ }
724
+
725
+ canvas.style.padding = "0"
726
+ if (layers) {
727
+ canvas.style.backgroundImage = "none"
728
+ canvas.style.backgroundColor = computed.backgroundColor
729
+ canvas.appendChild(layers)
730
+ }
731
+ canvas.appendChild(content)
732
+ }
733
+
734
+ normalizeBackgroundLayersForPptx(exportCanvas, window.getComputedStyle(target))
590
735
  host.appendChild(exportTarget)
591
736
  document.body.appendChild(host)
592
737
  await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
593
738
 
594
739
  const rect = exportTarget.getBoundingClientRect()
740
+ const canvasRect = exportCanvas.getBoundingClientRect()
595
741
  if (Math.abs(rect.width - 1920) > 2 || Math.abs(rect.height - 1080) > 2) {
596
742
  host.remove()
597
743
  throw new Error(
598
- `Slide ${index + 1} export canvas is ${Math.round(rect.width)}x${Math.round(rect.height)}, expected 1920x1080.`
744
+ `Slide ${index + 1} export slide is ${Math.round(rect.width)}x${Math.round(rect.height)}, expected 1920x1080.`
745
+ )
746
+ }
747
+ if (Math.abs(canvasRect.width - 1920) > 2 || Math.abs(canvasRect.height - 1080) > 2) {
748
+ host.remove()
749
+ throw new Error(
750
+ `Slide ${index + 1} export canvas is ${Math.round(canvasRect.width)}x${Math.round(canvasRect.height)}, expected 1920x1080.`
599
751
  )
600
752
  }
601
753
 
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "crypto"
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
3
- import { dirname, resolve } from "path"
2
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs"
3
+ import { dirname, isAbsolute, resolve, sep } from "path"
4
4
  import {
5
5
  activeDesign,
6
6
  activateDesign,
@@ -16,13 +16,14 @@ import {
16
16
  listDesignAssets,
17
17
  listDesigns,
18
18
  materializeDesignPreview,
19
+ materializeDesignCssSnapshot,
19
20
  packDesignPackage,
20
21
  seedBuiltinDesigns,
21
22
  validateDesignDraftPackage,
22
23
  validateDesignPackage,
23
24
  type DesignPackageAssetInput,
24
25
  } from "../design/designs"
25
- import { createDeckFoundation as createDeckFoundationShell } from "../deck-html/foundation"
26
+ import { activeDesignSnapshotName, createDeckFoundation as createDeckFoundationShell } from "../deck-html/foundation"
26
27
  import { activeDomain, activateDomain, createDomainDraftPackage, createDomainPackage, getDomainSkillMd, installDomainDraftPackage, listDomains, seedBuiltinDomains, validateDomainDraftPackage, validateDomainPackage } from "../domain/domains"
27
28
  import { compileNarrativeVault } from "../narrative-vault/compile"
28
29
  import { autoCompileNarrativeVault } from "../narrative-vault/auto-compile"
@@ -36,6 +37,8 @@ import { recordRenderedArtifact, workspaceRelative } from "../workspace-state/re
36
37
  import { existingWorkspaceMetaPath, workspaceMetaPath } from "../workspace-meta"
37
38
  import { checkMaterialIntake, extractMaterial, materialIntakeNoticeForCommand, prepareLocalMaterials, recordMaterialReview } from "../material-intake"
38
39
  import type { ReviewDeckOpenInput, ReviewDeckReadInput } from "./review"
40
+ export type { OpenDeckInput } from "./open-deck"
41
+ import { openDeck as openDeckDirect } from "./open-deck"
39
42
  import pkg from "../../package.json"
40
43
  export { bindResearchFindings, evaluateResearchFindings, researchSave, researchTargets } from "./research"
41
44
  export { storyRead } from "./story"
@@ -72,6 +75,12 @@ export interface RuntimeDesignInventoryInput {
72
75
  name?: string
73
76
  }
74
77
 
78
+ export interface RuntimeDeckDesignSwitchInput extends RuntimeWorkspaceInput {
79
+ file: string
80
+ name: string
81
+ openBrowser?: boolean
82
+ }
83
+
75
84
  export interface RuntimeDesignLayoutReadInput {
76
85
  name?: string
77
86
  layout: string | string[]
@@ -380,6 +389,16 @@ export async function reviewDeckOpen(input: ReviewDeckOpenInput) {
380
389
  return review.reviewDeckOpen(input)
381
390
  }
382
391
 
392
+ export async function openDeck(input: RuntimeFileInput & { openBrowser?: boolean; openUrl?: (url: string) => void }) {
393
+ const direct = await import("./open-deck")
394
+ return direct.openDeck(input)
395
+ }
396
+
397
+ export async function stopOpenDeckServers() {
398
+ const direct = await import("./open-deck")
399
+ return direct.stopOpenDeckServers()
400
+ }
401
+
383
402
  export function designList() {
384
403
  return {
385
404
  ok: true,
@@ -586,6 +605,50 @@ export function designActivate(input: RuntimeNameInput) {
586
605
  }
587
606
  }
588
607
 
608
+ export function switchDeckDesign(input: RuntimeDeckDesignSwitchInput) {
609
+ seedBuiltinDesigns()
610
+ const workspaceRoot = root(input.workspaceRoot)
611
+ const file = normalizeDeckFile(workspaceRoot, requiredString(input.file, "file"))
612
+ const design = requiredName({ name: input.name }, "design")
613
+ activateDesign(design)
614
+
615
+ const snapshot = materializeDesignCssSnapshot({
616
+ workspaceRoot,
617
+ outputPath: file,
618
+ designName: design,
619
+ snapshotName: activeDesignSnapshotName(file),
620
+ })
621
+
622
+ const htmlPath = resolve(workspaceRoot, file)
623
+ const html = readFileSync(htmlPath, "utf-8")
624
+ const migratedHtml = migrateDeckDesignLink(html, snapshot.href)
625
+ const migratedLink = migratedHtml !== html
626
+ if (migratedLink) writeFileSync(htmlPath, migratedHtml, "utf-8")
627
+
628
+ const opened = input.openBrowser === false
629
+ ? undefined
630
+ : openDeckDirect({ workspaceRoot, file, openBrowser: input.openBrowser })
631
+
632
+ return {
633
+ ok: true,
634
+ file,
635
+ activeDesign: activeDesign(),
636
+ design,
637
+ snapshotHref: snapshot.href,
638
+ snapshotDir: workspaceRelative(workspaceRoot, snapshot.snapshotDir),
639
+ assetCount: snapshot.assetCount,
640
+ migratedLink,
641
+ opened: opened ? {
642
+ ok: opened.ok,
643
+ url: opened.url,
644
+ openedBrowser: opened.openedBrowser,
645
+ mode: opened.mode,
646
+ readOnly: opened.readOnly,
647
+ } : undefined,
648
+ warnings: snapshot.warnings,
649
+ }
650
+ }
651
+
589
652
  export function domainList() {
590
653
  seedBuiltinDomains()
591
654
  return {
@@ -659,6 +722,35 @@ function root(workspaceRoot: string | undefined): string {
659
722
  return resolve(workspaceRoot || process.cwd())
660
723
  }
661
724
 
725
+ function normalizeDeckFile(workspaceRoot: string, fileInput: string): string {
726
+ const requested = fileInput.trim()
727
+ const absolute = isAbsolute(requested) ? resolve(requested) : resolve(workspaceRoot, requested)
728
+ if (!isInside(workspaceRoot, absolute)) throw new Error(`Deck HTML file is outside the workspace: ${requested}`)
729
+ if (!existsSync(absolute) || !statSync(absolute).isFile()) throw new Error(`Deck HTML file not found: ${requested}`)
730
+ const file = workspaceRelative(workspaceRoot, absolute)
731
+ if (!file.startsWith("decks/") || !file.endsWith(".html")) throw new Error(`Deck HTML file must be under decks/*.html: ${file}`)
732
+ return file
733
+ }
734
+
735
+ function migrateDeckDesignLink(html: string, activeHref: string): string {
736
+ const escapedHref = activeHref.replace(/"/g, "&quot;")
737
+ if (html.includes(`href="${escapedHref}"`) || html.includes(`href='${escapedHref}'`)) return html
738
+ const linkRe = /<link\b(?=[^>]*\brel=(["'])stylesheet\1)(?=[^>]*\bhref=(["'])\.\/_revela-design\/[^"']+\/design\.css\2)[^>]*>/i
739
+ if (linkRe.test(html)) {
740
+ return html.replace(linkRe, `<link rel="stylesheet" href="${escapedHref}" data-revela-design-link="active">`)
741
+ }
742
+ if (/<\/head>/i.test(html)) {
743
+ return html.replace(/<\/head>/i, ` <link rel="stylesheet" href="${escapedHref}" data-revela-design-link="active">\n</head>`)
744
+ }
745
+ return html
746
+ }
747
+
748
+ function isInside(rootPath: string, candidate: string): boolean {
749
+ const normalizedRoot = resolve(rootPath)
750
+ const normalizedCandidate = resolve(candidate)
751
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : normalizedRoot + sep)
752
+ }
753
+
662
754
  function safe<T>(fn: () => T): T | undefined {
663
755
  try {
664
756
  return fn()