@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
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs"
|
|
2
|
+
import { isAbsolute, normalize, resolve } from "path"
|
|
3
|
+
import { getPageTemplateVocabulary } from "./vocabulary"
|
|
4
|
+
|
|
5
|
+
export type PageTemplateStatus = "metadata-only" | "renderable"
|
|
6
|
+
|
|
7
|
+
export interface PageTemplateField {
|
|
8
|
+
name: string
|
|
9
|
+
type: "string" | "string[]" | "items[]" | "metrics[]" | "milestones[]" | "rows[]" | "steps[]"
|
|
10
|
+
required?: boolean
|
|
11
|
+
description: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PageTemplateDefinition {
|
|
15
|
+
id: string
|
|
16
|
+
title: string
|
|
17
|
+
purpose: string
|
|
18
|
+
status: PageTemplateStatus
|
|
19
|
+
fields: PageTemplateField[]
|
|
20
|
+
contentRules: string[]
|
|
21
|
+
qaRules: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RenderTemplateSlideInput {
|
|
25
|
+
templateId: string
|
|
26
|
+
slideIndex: number
|
|
27
|
+
content: Record<string, any>
|
|
28
|
+
designName?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RenderTemplateScaffoldInput {
|
|
32
|
+
templateId: string
|
|
33
|
+
slideIndex: number
|
|
34
|
+
seed?: Record<string, any>
|
|
35
|
+
designName?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RenderTemplateSlideResult {
|
|
39
|
+
ok: true
|
|
40
|
+
templateId: string
|
|
41
|
+
slideIndex: number
|
|
42
|
+
designName: string
|
|
43
|
+
html: string
|
|
44
|
+
warnings: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RenderTemplateScaffoldResult extends RenderTemplateSlideResult {
|
|
48
|
+
scaffold: true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AddTemplateSlideInput extends RenderTemplateSlideInput {
|
|
52
|
+
workspaceRoot: string
|
|
53
|
+
outputPath: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AddTemplateScaffoldInput extends RenderTemplateScaffoldInput {
|
|
57
|
+
workspaceRoot: string
|
|
58
|
+
outputPath: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AddTemplateSlideResult extends RenderTemplateSlideResult {
|
|
62
|
+
outputPath: string
|
|
63
|
+
inserted: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AddTemplateScaffoldResult extends RenderTemplateScaffoldResult {
|
|
67
|
+
outputPath: string
|
|
68
|
+
inserted: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PageTemplateContractIssue {
|
|
72
|
+
severity: "error" | "warning"
|
|
73
|
+
templateId: string
|
|
74
|
+
slideIndex?: number
|
|
75
|
+
message: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PageTemplateContractReport {
|
|
79
|
+
ok: boolean
|
|
80
|
+
issues: PageTemplateContractIssue[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface BoundedTemplateEditInput {
|
|
84
|
+
beforeHtml: string
|
|
85
|
+
afterHtml: string
|
|
86
|
+
slideIndex: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface BuiltInPreviewFixture {
|
|
90
|
+
templateId: string
|
|
91
|
+
content: Record<string, any>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const templates: PageTemplateDefinition[] = [
|
|
95
|
+
define("cover", "Cover", "Open the deck with one clear artifact title and context.", [
|
|
96
|
+
field("eyebrow", "string", "Small context label."),
|
|
97
|
+
field("title", "string", "Deck title.", true),
|
|
98
|
+
], ["Use one dominant title.", "Keep source/evidence details out of the cover."], ["Has one h1.", "Uses hero structure."]),
|
|
99
|
+
define("section-divider", "Section Divider", "Mark a chapter transition with a short label and thesis.", [
|
|
100
|
+
field("eyebrow", "string", "Chapter label."),
|
|
101
|
+
field("title", "string", "Section title.", true),
|
|
102
|
+
], ["Use between chapters only."], ["Has one h1.", "Counts as structural."]),
|
|
103
|
+
define("closing", "Closing", "End with the final decision, ask, or next action.", [
|
|
104
|
+
field("title", "string", "Closing line.", true),
|
|
105
|
+
], ["Keep the close concise."], ["Has one h1.", "Uses closing/hero structure."]),
|
|
106
|
+
define("agenda", "Agenda / TOC", "Orient the audience to the deck flow.", [
|
|
107
|
+
field("title", "string", "Agenda title.", true),
|
|
108
|
+
field("items", "items[]", "Agenda items.", true),
|
|
109
|
+
], ["Use 3-6 items."], ["Numbers are in DOM order."]),
|
|
110
|
+
define("executive-summary", "Executive Summary", "Compress the decision logic into a few takeaways.", [
|
|
111
|
+
field("title", "string", "Slide title.", true),
|
|
112
|
+
field("items", "items[]", "Summary takeaways.", true),
|
|
113
|
+
], ["Use 3-4 takeaways.", "Each takeaway needs a short label and support line."], ["Contains summary cards."]),
|
|
114
|
+
define("problem-context", "Problem / Context", "Frame why the topic matters now.", [
|
|
115
|
+
field("title", "string", "Slide title.", true),
|
|
116
|
+
field("body", "string", "Context paragraph.", true),
|
|
117
|
+
field("items", "items[]", "Context bullets."),
|
|
118
|
+
], ["Separate situation from implication."], ["Main message remains outside cards."]),
|
|
119
|
+
define("key-message-evidence", "Key Message + Evidence", "State a claim and show the supporting evidence items.", [
|
|
120
|
+
field("title", "string", "Claim title.", true),
|
|
121
|
+
field("body", "string", "Claim explanation.", true),
|
|
122
|
+
field("items", "items[]", "Evidence items.", true),
|
|
123
|
+
], ["Evidence cards must not invent unsupported facts."], ["Has claim and evidence region."]),
|
|
124
|
+
define("claim-supporting-visual", "Claim + Supporting Visual", "Pair a claim with one visual or diagram placeholder.", [
|
|
125
|
+
field("title", "string", "Claim title.", true),
|
|
126
|
+
field("body", "string", "Claim explanation.", true),
|
|
127
|
+
field("visualTitle", "string", "Visual label."),
|
|
128
|
+
field("items", "items[]", "Visual callouts."),
|
|
129
|
+
], ["Use for one visual argument, not many unrelated facts."], ["Visual region is present."]),
|
|
130
|
+
define("metric-highlight", "Metric Highlight", "Let one or more metrics carry the page.", [
|
|
131
|
+
field("title", "string", "Slide title.", true),
|
|
132
|
+
field("metrics", "metrics[]", "Metric cards.", true),
|
|
133
|
+
field("body", "string", "Interpretation."),
|
|
134
|
+
field("insightTitle", "string", "Insight panel title."),
|
|
135
|
+
field("insightBody", "string", "Metric interpretation, reading note, or caveat."),
|
|
136
|
+
field("insightIcon", "string", "Lucide icon name for the insight title."),
|
|
137
|
+
field("insightPosition", "string", "top or bottom insight panel placement."),
|
|
138
|
+
], ["Every number needs an interpretation line."], ["Metric values are not hidden in body copy."]),
|
|
139
|
+
define("chart-takeaways", "Chart + Takeaways", "Reserve space for a chart and explain what to read from it.", [
|
|
140
|
+
field("title", "string", "Slide title.", true),
|
|
141
|
+
field("chartTitle", "string", "Chart title."),
|
|
142
|
+
field("takeawaysTitle", "string", "Title for the interpretation text panel."),
|
|
143
|
+
field("items", "items[]", "Takeaways.", true),
|
|
144
|
+
], ["Chart area must be explicit and bounded."], ["Chart panel and takeaways both exist."]),
|
|
145
|
+
define("table-comparison", "Table / Comparison", "Compare options, segments, or facts in a structured table.", [
|
|
146
|
+
field("title", "string", "Slide title.", true),
|
|
147
|
+
field("columns", "string[]", "Column labels.", true),
|
|
148
|
+
field("rows", "rows[]", "Table rows.", true),
|
|
149
|
+
field("insightTitle", "string", "Insight panel title."),
|
|
150
|
+
field("insightBody", "string", "Interpretation, reading note, or caveat below the table."),
|
|
151
|
+
field("insightIcon", "string", "Lucide icon name for the insight title."),
|
|
152
|
+
], ["Keep rows scannable.", "Do not use a table for pure prose."], ["Table has headers and body rows."]),
|
|
153
|
+
define("timeline-roadmap", "Timeline / Roadmap", "Show dated phases, milestones, or journey steps.", [
|
|
154
|
+
field("title", "string", "Slide title.", true),
|
|
155
|
+
field("orientation", "string", "horizontal or vertical."),
|
|
156
|
+
field("milestones", "milestones[]", "Timeline milestones.", true),
|
|
157
|
+
field("insightTitle", "string", "Side panel title."),
|
|
158
|
+
field("insightBody", "string", "Timeline interpretation, so-what, or caveat."),
|
|
159
|
+
field("insightSide", "string", "left or right side panel placement."),
|
|
160
|
+
], ["Use 3-6 milestones.", "Horizontal timelines use card copy above the axis and year labels below it.", "Each dot belongs to the same DOM item as its copy."], ["Timeline root exists.", "Every milestone has dot and copy.", "Dot and copy are sibling anchors inside one timeline item."]),
|
|
161
|
+
define("process-steps", "Process / Steps", "Show a short ordered process or execution sequence.", [
|
|
162
|
+
field("title", "string", "Slide title.", true),
|
|
163
|
+
field("steps", "steps[]", "Ordered steps.", true),
|
|
164
|
+
], ["Use 3-5 steps.", "Each step starts with an action."], ["Steps are numbered in DOM order."]),
|
|
165
|
+
define("recommendation-decision", "Recommendation / Decision / Ask", "Make the requested decision and explain rationale and next steps.", [
|
|
166
|
+
field("title", "string", "Slide title.", true),
|
|
167
|
+
field("recommendation", "string", "Recommended action.", true),
|
|
168
|
+
field("image", "string", "Optional image card path for the recommendation panel."),
|
|
169
|
+
field("imageAlt", "string", "Optional image alt text."),
|
|
170
|
+
field("imageCaption", "string", "Optional image caption."),
|
|
171
|
+
field("items", "items[]", "Rationale points."),
|
|
172
|
+
field("steps", "steps[]", "Next steps."),
|
|
173
|
+
], ["State the ask plainly.", "Separate rationale from next steps."], ["Recommendation panel exists.", "Next steps are ordered."]),
|
|
174
|
+
define("risks-tradeoffs", "Risks / Caveats / Tradeoffs", "Keep limitations and tradeoffs visible.", [
|
|
175
|
+
field("title", "string", "Slide title.", true),
|
|
176
|
+
field("items", "items[]", "Risks or caveats.", true),
|
|
177
|
+
], ["Name uncertainty instead of hiding it."], ["Contains risk/tradeoff cards."]),
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
export function listPageTemplates(): { ok: true; templates: PageTemplateDefinition[] } {
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
templates: templates.map((template) => ({
|
|
184
|
+
...template,
|
|
185
|
+
vocabulary: getPageTemplateVocabulary(template.id),
|
|
186
|
+
} as PageTemplateDefinition & { vocabulary: ReturnType<typeof getPageTemplateVocabulary> })),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function renderTemplateSlide(input: RenderTemplateSlideInput): RenderTemplateSlideResult {
|
|
191
|
+
const template = getPageTemplate(input.templateId)
|
|
192
|
+
const slideIndex = positiveIndex(input.slideIndex)
|
|
193
|
+
const content = input.content ?? {}
|
|
194
|
+
const designName = input.designName || "lucent"
|
|
195
|
+
const warnings = validateRequiredFields(template, content)
|
|
196
|
+
const html = renderSlideShell({
|
|
197
|
+
template,
|
|
198
|
+
slideIndex,
|
|
199
|
+
designName,
|
|
200
|
+
title: stringValue(content.title) || template.title,
|
|
201
|
+
body: renderBody(template.id, content),
|
|
202
|
+
})
|
|
203
|
+
return { ok: true, templateId: template.id, slideIndex, designName, html, warnings }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function builtInPreviewFixtures(): BuiltInPreviewFixture[] {
|
|
207
|
+
return [
|
|
208
|
+
fixture("cover", { title: "cover" }),
|
|
209
|
+
fixture("section-divider", { title: "section-divider" }),
|
|
210
|
+
fixture("closing", { title: "closing" }),
|
|
211
|
+
fixture("agenda", {
|
|
212
|
+
title: "agenda",
|
|
213
|
+
items: [
|
|
214
|
+
{ label: "Frame the decision" },
|
|
215
|
+
{ label: "Show the evidence" },
|
|
216
|
+
{ label: "Compare options" },
|
|
217
|
+
{ label: "Close with action" },
|
|
218
|
+
],
|
|
219
|
+
}),
|
|
220
|
+
fixture("executive-summary", {
|
|
221
|
+
title: "executive-summary",
|
|
222
|
+
items: [
|
|
223
|
+
{ label: "Decision is ready", description: "The facts support moving from discussion to selection without adding another analysis cycle." },
|
|
224
|
+
{ label: "Risk is bounded", description: "Known caveats are visible, named, and can be managed through rollout gates." },
|
|
225
|
+
{ label: "Next step is narrow", description: "A pilot decision creates more learning without overcommitting capital or team capacity." },
|
|
226
|
+
],
|
|
227
|
+
}),
|
|
228
|
+
fixture("problem-context", {
|
|
229
|
+
title: "problem-context",
|
|
230
|
+
body: "Use this template when the audience needs the situation, tension, and implication before seeing recommendations.",
|
|
231
|
+
items: [
|
|
232
|
+
{ label: "Situation", description: "A shift has changed the operating baseline." },
|
|
233
|
+
{ label: "Tension", description: "Current process cannot absorb the new variance cleanly." },
|
|
234
|
+
{ label: "Implication", description: "Delay increases rework and weakens decision confidence." },
|
|
235
|
+
],
|
|
236
|
+
}),
|
|
237
|
+
fixture("key-message-evidence", {
|
|
238
|
+
title: "key-message-evidence",
|
|
239
|
+
body: "This key message stays large and readable, while the supporting evidence is separated into traceable slots for source-backed proof.",
|
|
240
|
+
items: [
|
|
241
|
+
{ label: "Evidence 1", description: "The generated HTML separates the key-message panel from the evidence grid, so the claim cannot collapse into generic card content." },
|
|
242
|
+
{ label: "Evidence 2", description: "Each evidence slot has a stable title and explanation area, giving the agent a predictable place for proof, caveat, or source-backed detail." },
|
|
243
|
+
{ label: "Evidence 3", description: "QA can inspect the DOM contract before visual styling, which keeps template structure from depending on a design skin." },
|
|
244
|
+
],
|
|
245
|
+
}),
|
|
246
|
+
fixture("claim-supporting-visual", {
|
|
247
|
+
title: "claim-supporting-visual",
|
|
248
|
+
claim: "A single visual should carry one argument.",
|
|
249
|
+
body: "The template reserves a stable visual region while keeping explanatory copy close enough to guide interpretation.",
|
|
250
|
+
items: [
|
|
251
|
+
{ label: "Anchor", description: "State what the reader should inspect first." },
|
|
252
|
+
{ label: "Callout", description: "Use short labels instead of a second paragraph." },
|
|
253
|
+
],
|
|
254
|
+
}),
|
|
255
|
+
fixture("metric-highlight", {
|
|
256
|
+
title: "metric-highlight",
|
|
257
|
+
metrics: [
|
|
258
|
+
{ value: "67%", label: "Adoption signal", description: "Primary number plus interpretation." },
|
|
259
|
+
{ value: "3x", label: "Review speed", description: "Comparison is stated beside the metric." },
|
|
260
|
+
{ value: "14d", label: "Pilot window", description: "Time bound keeps the ask concrete." },
|
|
261
|
+
],
|
|
262
|
+
insightTitle: "Read the signal",
|
|
263
|
+
insightIcon: "scan-search",
|
|
264
|
+
insightBody: "Treat the metric row as the evidence surface, then use this panel to state the decision implication, caveat, or next reading step.",
|
|
265
|
+
}),
|
|
266
|
+
fixture("chart-takeaways", {
|
|
267
|
+
title: "chart-takeaways",
|
|
268
|
+
takeawaysTitle: "What to read",
|
|
269
|
+
items: [
|
|
270
|
+
{ label: "Trend", description: "Call out the movement or comparison the chart is meant to prove, including the direction and the comparison baseline." },
|
|
271
|
+
{ label: "Driver", description: "Name the likely reason without overclaiming; separate observed movement from the interpretation or hypothesis." },
|
|
272
|
+
{ label: "Decision use", description: "Explain how the chart changes the recommendation, what threshold matters, and what follow-up evidence would reduce risk." },
|
|
273
|
+
],
|
|
274
|
+
}),
|
|
275
|
+
fixture("table-comparison", {
|
|
276
|
+
title: "table-comparison",
|
|
277
|
+
columns: ["Layer", "Owns", "Agent task"],
|
|
278
|
+
rows: [
|
|
279
|
+
["Template", "Structure and DOM contract", "Select the page pattern"],
|
|
280
|
+
["Content", "Claim, evidence, caveat", "Fill the meaning"],
|
|
281
|
+
["Design", "Color, type, surfaces", "Skin stable classes"],
|
|
282
|
+
],
|
|
283
|
+
insightTitle: "Insight",
|
|
284
|
+
insightIcon: "lightbulb",
|
|
285
|
+
insightBody: "The template layer owns structure, while the design layer owns visual treatment. This keeps agent edits bounded without freezing the final look.",
|
|
286
|
+
}),
|
|
287
|
+
fixture("timeline-roadmap", {
|
|
288
|
+
title: "timeline-roadmap",
|
|
289
|
+
orientation: "horizontal",
|
|
290
|
+
milestones: [
|
|
291
|
+
{ date: "2022", label: "Signal", description: "Map the baseline." },
|
|
292
|
+
{ date: "2023", label: "Proof", description: "Validate the evidence threshold." },
|
|
293
|
+
{ date: "2024", label: "Inflection", description: "Use the pivotal moment to frame the shift." },
|
|
294
|
+
{ date: "2025", label: "Scale", description: "Use a taller card for the highlighted milestone.", highlight: true },
|
|
295
|
+
{ date: "2026", label: "Decision", description: "Commit to the next path." },
|
|
296
|
+
],
|
|
297
|
+
}),
|
|
298
|
+
fixture("timeline-roadmap", {
|
|
299
|
+
title: "timeline-roadmap-vertical",
|
|
300
|
+
orientation: "vertical",
|
|
301
|
+
insightTitle: "Reading the journey",
|
|
302
|
+
insightBody: "The timeline should show sequence and decision rhythm, while the side panel explains why the milestones matter.",
|
|
303
|
+
milestones: [
|
|
304
|
+
{ date: "Mar 2019", label: "Launch", description: "Baseline mapping." },
|
|
305
|
+
{ date: "Nov 2019", label: "Audit", description: "Evidence sprint." },
|
|
306
|
+
{ date: "May 2020", label: "Scale", description: "Operating cadence." },
|
|
307
|
+
{ date: "Feb 2021", label: "Review", description: "QA before export." },
|
|
308
|
+
],
|
|
309
|
+
}),
|
|
310
|
+
fixture("process-steps", {
|
|
311
|
+
title: "process-steps",
|
|
312
|
+
steps: [
|
|
313
|
+
{ label: "Choose", description: "Select the page template that matches the communication job." },
|
|
314
|
+
{ label: "Fill", description: "Provide only the content fields the template needs." },
|
|
315
|
+
{ label: "Style", description: "Let the active design control type, color, and surfaces." },
|
|
316
|
+
{ label: "QA", description: "Run contract and visual checks before export." },
|
|
317
|
+
],
|
|
318
|
+
}),
|
|
319
|
+
fixture("recommendation-decision", {
|
|
320
|
+
title: "recommendation-decision",
|
|
321
|
+
recommendation: "Adopt page templates as the structural layer, with designs remaining user-customizable.",
|
|
322
|
+
image: "./assets/card-lens.jpg",
|
|
323
|
+
imageAlt: "Lucent design asset",
|
|
324
|
+
imageCaption: "Design asset example",
|
|
325
|
+
items: [
|
|
326
|
+
{ label: "Rationale", description: "This keeps generation reliable while leaving style expressive and replaceable." },
|
|
327
|
+
],
|
|
328
|
+
steps: [
|
|
329
|
+
{ label: "Pilot", description: "Use the built-in preview to tune every template." },
|
|
330
|
+
{ label: "Validate", description: "Promote only contracts that pass QA and browser review." },
|
|
331
|
+
{ label: "Ship", description: "Document the add-slide workflow for agents." },
|
|
332
|
+
],
|
|
333
|
+
}),
|
|
334
|
+
fixture("risks-tradeoffs", {
|
|
335
|
+
title: "risks-tradeoffs",
|
|
336
|
+
items: [
|
|
337
|
+
{ label: "Constraint", description: "Templates can over-constrain if they become decorative presets instead of communication jobs.", image: "./assets/report-visual.jpg", imageAlt: "Report visual", imageCaption: "Design asset example" },
|
|
338
|
+
{ label: "Mitigation", description: "Keep bounded HTML edit flow after scaffold insertion so agents can improve the page directly." },
|
|
339
|
+
{ label: "Tradeoff", description: "More structure improves QA, but only if template contracts stay small and semantic." },
|
|
340
|
+
],
|
|
341
|
+
}),
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function renderBuiltInPreviewHtml(): string {
|
|
346
|
+
const fixtures = builtInPreviewFixtures()
|
|
347
|
+
const slides = fixtures
|
|
348
|
+
.map((item, index) => renderTemplateSlide({
|
|
349
|
+
templateId: item.templateId,
|
|
350
|
+
slideIndex: index + 1,
|
|
351
|
+
designName: "built-in-preview",
|
|
352
|
+
content: {
|
|
353
|
+
eyebrow: previewEyebrow(index + 1, fixtures.length),
|
|
354
|
+
...item.content,
|
|
355
|
+
},
|
|
356
|
+
}).html)
|
|
357
|
+
.join("\n")
|
|
358
|
+
const html = `<!DOCTYPE html>
|
|
359
|
+
<html lang="en">
|
|
360
|
+
<head>
|
|
361
|
+
<meta charset="UTF-8">
|
|
362
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
363
|
+
<title>Revela Page Templates</title>
|
|
364
|
+
<link rel="stylesheet" href="./design.css">
|
|
365
|
+
</head>
|
|
366
|
+
<body>
|
|
367
|
+
<!-- revela-slides:start -->
|
|
368
|
+
${slides}
|
|
369
|
+
<!-- revela-slides:end -->
|
|
370
|
+
${slidePresentationRuntime()}
|
|
371
|
+
</body>
|
|
372
|
+
</html>
|
|
373
|
+
`
|
|
374
|
+
return ensureInlineLucideRuntime(html)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function addTemplateSlide(input: AddTemplateSlideInput): AddTemplateSlideResult {
|
|
378
|
+
const outputPath = normalizeOutputPath(input.outputPath)
|
|
379
|
+
const targetPath = resolve(input.workspaceRoot, outputPath)
|
|
380
|
+
if (!existsSync(targetPath)) throw new Error(`Deck HTML does not exist: ${outputPath}. Create the deck foundation before adding template slides.`)
|
|
381
|
+
const rendered = renderTemplateSlide(input)
|
|
382
|
+
const html = readFileSync(targetPath, "utf-8")
|
|
383
|
+
const markers = deckFoundationMarkers()
|
|
384
|
+
if (!html.includes(markers.start) || !html.includes(markers.end)) throw new Error(`Deck HTML is missing Revela slide markers: ${outputPath}`)
|
|
385
|
+
const withSlide = html.replace(markers.end, `${rendered.html}\n ${markers.end}`)
|
|
386
|
+
const next = rendered.html.includes("data-lucide=") ? ensureInlineLucideRuntime(withSlide) : withSlide
|
|
387
|
+
writeFileSync(targetPath, next, "utf-8")
|
|
388
|
+
return { ...rendered, outputPath, inserted: true }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function renderTemplateScaffold(input: RenderTemplateScaffoldInput): RenderTemplateScaffoldResult {
|
|
392
|
+
const seed = scaffoldSeed(input.templateId, input.seed ?? {})
|
|
393
|
+
const rendered = renderTemplateSlide({
|
|
394
|
+
templateId: input.templateId,
|
|
395
|
+
slideIndex: input.slideIndex,
|
|
396
|
+
content: seed,
|
|
397
|
+
designName: input.designName,
|
|
398
|
+
})
|
|
399
|
+
return { ...rendered, scaffold: true }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function addTemplateScaffold(input: AddTemplateScaffoldInput): AddTemplateScaffoldResult {
|
|
403
|
+
const outputPath = normalizeOutputPath(input.outputPath)
|
|
404
|
+
const targetPath = resolve(input.workspaceRoot, outputPath)
|
|
405
|
+
if (!existsSync(targetPath)) throw new Error(`Deck HTML does not exist: ${outputPath}. Create the deck foundation before adding template slides.`)
|
|
406
|
+
const rendered = renderTemplateScaffold(input)
|
|
407
|
+
const html = readFileSync(targetPath, "utf-8")
|
|
408
|
+
const markers = deckFoundationMarkers()
|
|
409
|
+
if (!html.includes(markers.start) || !html.includes(markers.end)) throw new Error(`Deck HTML is missing Revela slide markers: ${outputPath}`)
|
|
410
|
+
const withSlide = html.replace(markers.end, `${rendered.html}\n ${markers.end}`)
|
|
411
|
+
const next = rendered.html.includes("data-lucide=") ? ensureInlineLucideRuntime(withSlide) : withSlide
|
|
412
|
+
writeFileSync(targetPath, next, "utf-8")
|
|
413
|
+
return { ...rendered, outputPath, inserted: true }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function deckFoundationMarkers(): { start: string; end: string } {
|
|
417
|
+
return { start: "<!-- revela-slides:start -->", end: "<!-- revela-slides:end -->" }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function fixture(templateId: string, content: Record<string, any>): BuiltInPreviewFixture {
|
|
421
|
+
return { templateId, content }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function previewEyebrow(slideIndex: number, total: number): string {
|
|
425
|
+
const index = String(slideIndex).padStart(2, "0")
|
|
426
|
+
const count = String(total).padStart(2, "0")
|
|
427
|
+
return `Template ${index} / ${count}`
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function normalizeOutputPath(outputPath: string): string {
|
|
431
|
+
const trimmed = String(outputPath || "").trim()
|
|
432
|
+
if (!trimmed) throw new Error("outputPath is required")
|
|
433
|
+
if (!trimmed.endsWith(".html")) throw new Error("Deck outputPath must end in .html")
|
|
434
|
+
if (isAbsolute(trimmed)) throw new Error("Deck outputPath must be workspace-relative")
|
|
435
|
+
const segments = trimmed.split(/[\\/]+/)
|
|
436
|
+
if (segments.includes("..")) throw new Error("Deck outputPath must not contain parent-directory traversal")
|
|
437
|
+
return normalize(trimmed).replace(/\\/g, "/")
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function templateDeckCss(input: { designName?: string; designAssetBasePath?: string } = {}): string {
|
|
441
|
+
const designName = input.designName || "lucent"
|
|
442
|
+
const assetBasePath = input.designAssetBasePath
|
|
443
|
+
const lucentCoverBackground = designName === "lucent" && assetBasePath ? cssUrl(`${assetBasePath}/cover-background.jpg`) : ""
|
|
444
|
+
const lucentClosingBackground = designName === "lucent" && assetBasePath ? cssUrl(`${assetBasePath}/closing-background.jpg`) : ""
|
|
445
|
+
const lucentCoverBackgroundCss = lucentCoverBackground ? `
|
|
446
|
+
.template-slide[data-design="lucent"][data-template="cover"] .slide-canvas {
|
|
447
|
+
background:
|
|
448
|
+
linear-gradient(90deg, rgba(7,17,31,0.82), rgba(7,17,31,0.42) 52%, rgba(7,17,31,0.24)),
|
|
449
|
+
url("${lucentCoverBackground}") center center / cover no-repeat;
|
|
450
|
+
}
|
|
451
|
+
.template-slide[data-design="lucent"][data-template="agenda"] .slide-canvas {
|
|
452
|
+
background:
|
|
453
|
+
linear-gradient(90deg, rgba(7,17,31,0.86), rgba(7,17,31,0.58) 52%, rgba(7,17,31,0.32)),
|
|
454
|
+
url("${lucentCoverBackground}") center center / cover no-repeat;
|
|
455
|
+
}
|
|
456
|
+
.template-slide[data-design="lucent"][data-template="section-divider"] .slide-canvas {
|
|
457
|
+
background:
|
|
458
|
+
linear-gradient(90deg, rgba(7,17,31,0.86), rgba(16,26,43,0.62) 58%, rgba(36,58,115,0.36)),
|
|
459
|
+
url("${lucentCoverBackground}") center center / cover no-repeat;
|
|
460
|
+
}` : ""
|
|
461
|
+
const lucentClosingBackgroundCss = lucentClosingBackground ? `
|
|
462
|
+
.template-slide[data-design="lucent"][data-template="closing"] .slide-canvas {
|
|
463
|
+
background:
|
|
464
|
+
linear-gradient(90deg, rgba(7,17,31,0.82), rgba(49,94,234,0.42) 58%, rgba(24,168,216,0.24)),
|
|
465
|
+
url("${lucentClosingBackground}") center center / cover no-repeat;
|
|
466
|
+
}` : ""
|
|
467
|
+
return `
|
|
468
|
+
* { box-sizing: border-box; }
|
|
469
|
+
html { scroll-snap-type: y mandatory; overflow-y: scroll; height: 100%; }
|
|
470
|
+
body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primary, #101a2b); font-family: var(--font-body, Arial, sans-serif); -webkit-font-smoothing: antialiased; }
|
|
471
|
+
.slide { min-height: 100dvh; scroll-snap-align: start; display: flex; align-items: center; justify-content: center; overflow: hidden; background: var(--bg-frame, #07111f); }
|
|
472
|
+
.slide-canvas { width: 1920px; height: 1080px; flex-shrink: 0; transform-origin: center center; position: relative; overflow: hidden; }
|
|
473
|
+
.template-slide .slide-canvas {
|
|
474
|
+
background:
|
|
475
|
+
radial-gradient(circle at 82% 16%, rgba(49, 94, 234, 0.11), transparent 29%),
|
|
476
|
+
linear-gradient(135deg, var(--bg-page), var(--bg-page-alt));
|
|
477
|
+
color: var(--text-primary);
|
|
478
|
+
padding: 72px;
|
|
479
|
+
box-sizing: border-box;
|
|
480
|
+
}
|
|
481
|
+
.template-frame { width: 100%; height: 100%; display: flex; flex-direction: column; gap: 34px; }
|
|
482
|
+
.template-frame--catalog { gap: 26px; }
|
|
483
|
+
.template-eyebrow { margin: 0 0 14px; font-size: 16px; text-transform: uppercase; letter-spacing: 0.18em; color: var(--text-muted); font-weight: 700; }
|
|
484
|
+
.template-frame header { flex: 0 0 auto; padding-bottom: 8px; overflow: visible; }
|
|
485
|
+
.template-title { margin: 0; max-width: 1320px; font-family: var(--font-display); font-size: 62px; line-height: 1.22; color: var(--text-primary); padding-bottom: 6px; overflow: visible; }
|
|
486
|
+
.template-body { flex: 1; min-height: 0; }
|
|
487
|
+
.template-grid { display: grid; gap: 24px; height: 100%; }
|
|
488
|
+
.template-grid.cols-2 { grid-template-columns: 0.95fr 1.05fr; }
|
|
489
|
+
.template-grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
490
|
+
.template-grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
|
|
491
|
+
.template-chart-layout { grid-template-columns: 2fr 1fr; }
|
|
492
|
+
.template-card { background: rgba(255,255,255,0.82); border: 1px solid var(--line); border-radius: var(--surface-radius); padding: 28px; box-shadow: 0 18px 44px var(--shadow-soft); }
|
|
493
|
+
.template-card h2, .template-card h3 { margin: 0 0 12px; font-size: 28px; line-height: 1.32; padding-bottom: 4px; overflow: visible; }
|
|
494
|
+
.template-card p { margin: 10px 0; font-size: 21px; line-height: 1.42; color: var(--text-secondary); }
|
|
495
|
+
.template-key-message-panel { display: flex; flex-direction: column; justify-content: flex-start; gap: 24px; padding: 0; background: transparent; border-radius: 0; box-shadow: none; }
|
|
496
|
+
.template-key-message-kicker { margin: 0; max-width: 720px; font-size: 32px; line-height: 1.24; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-primary); font-weight: 800; padding-bottom: 6px; overflow: visible; }
|
|
497
|
+
.template-key-message-panel p { margin: 0; max-width: 760px; font-size: 25px; line-height: 1.5; color: var(--text-secondary); }
|
|
498
|
+
.template-evidence-grid { display: grid; gap: 24px; min-height: 0; }
|
|
499
|
+
.template-evidence-card { min-height: 0; }
|
|
500
|
+
.template-claim-text-panel { min-height: 0; display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 18px; padding: 0; background: transparent; border: 0; border-radius: 0; box-shadow: none; }
|
|
501
|
+
.template-claim-text-title { margin: 0; max-width: 760px; font-size: 31px; line-height: 1.26; color: var(--text-primary); padding-bottom: 4px; overflow: visible; }
|
|
502
|
+
.template-claim-text-body { margin: 0; max-width: 760px; font-size: 22px; line-height: 1.48; color: var(--text-secondary); }
|
|
503
|
+
.template-claim-text-panel .template-list { margin-top: 4px; }
|
|
504
|
+
.template-list { display: grid; gap: 18px; margin: 0; padding: 0; list-style: none; }
|
|
505
|
+
.template-list li { position: relative; padding-left: 24px; font-size: 24px; line-height: 1.38; color: var(--text-secondary); }
|
|
506
|
+
.template-list li::before { content: ""; position: absolute; left: 0; top: 14px; width: 7px; height: 7px; background: var(--accent-primary); }
|
|
507
|
+
.template-hero { margin: 0; max-width: none; justify-content: center; align-items: flex-start; }
|
|
508
|
+
.template-hero > [data-template-slot="hero"] { width: 100%; }
|
|
509
|
+
.template-hero header { padding-bottom: 0; }
|
|
510
|
+
.template-hero-title { font-size: 120px; line-height: 1.18; color: white; font-weight: 800; opacity: 0.8; padding: 12px 0 20px; max-width: 1320px; }
|
|
511
|
+
.template-hero .template-eyebrow { color: rgba(255,255,255,0.78); }
|
|
512
|
+
.template-hero--cover, .template-hero--section-divider { justify-content: center; align-items: flex-start; }
|
|
513
|
+
.template-hero--closing { justify-content: flex-end; align-items: flex-end; }
|
|
514
|
+
.template-hero--closing > [data-template-slot="hero"] { display: flex; justify-content: flex-end; text-align: right; }
|
|
515
|
+
.template-hero--closing .template-hero-title { max-width: 1120px; }
|
|
516
|
+
.template-slide[data-template="agenda"] .template-frame { display: grid; grid-template-rows: 1fr auto; gap: 28px; }
|
|
517
|
+
.template-slide[data-template="cover"] .slide-canvas,
|
|
518
|
+
.template-slide[data-template="section-divider"] .slide-canvas,
|
|
519
|
+
.template-slide[data-template="closing"] .slide-canvas {
|
|
520
|
+
background:
|
|
521
|
+
radial-gradient(circle at 80% 14%, rgba(24,168,216,0.32), transparent 28%),
|
|
522
|
+
linear-gradient(135deg, #07111f, #101a2b 62%, #243a73);
|
|
523
|
+
}
|
|
524
|
+
${lucentCoverBackgroundCss}
|
|
525
|
+
.template-slide[data-template="closing"] .slide-canvas { background: linear-gradient(135deg, #07111f, #315eea 58%, #18a8d8); }
|
|
526
|
+
${lucentClosingBackgroundCss}
|
|
527
|
+
.template-agenda-panel { height: 100%; min-height: 0; display: flex; overflow: hidden; color: white; }
|
|
528
|
+
.template-agenda-inner { width: 100%; display: grid; grid-template-columns: 37% minmax(0, 1fr); align-items: stretch; gap: 76px; }
|
|
529
|
+
.template-agenda-header { display: flex; flex-direction: column; min-height: 0; padding: 10px 0 0; }
|
|
530
|
+
.template-agenda-header .template-eyebrow { color: rgba(255,255,255,0.64); }
|
|
531
|
+
.template-agenda-header .template-title { max-width: 440px; font-size: 54px; line-height: 1.16; letter-spacing: 0; text-transform: uppercase; color: white; padding-bottom: 8px; overflow: visible; }
|
|
532
|
+
.template-agenda-footer { margin: auto 0 0; font-size: 13px; line-height: 1.4; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 800; color: rgba(255,255,255,0.84); }
|
|
533
|
+
.template-agenda-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; justify-content: center; gap: 40px; height: 100%; }
|
|
534
|
+
.template-agenda-item { display: grid; grid-template-columns: 86px minmax(0, 1fr); gap: 44px; align-items: center; min-height: 58px; overflow: visible; }
|
|
535
|
+
.template-agenda-item span { font-family: var(--font-display); font-size: 44px; line-height: 1; letter-spacing: 0.03em; color: var(--accent-cyan, #18a8d8); font-weight: 800; font-variant-numeric: tabular-nums; }
|
|
536
|
+
.template-agenda-item strong { font-size: 18px; line-height: 1.45; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700; color: rgba(255,255,255,0.92); }
|
|
537
|
+
.template-metric-layout { height: 100%; min-height: 0; display: grid; gap: 26px; }
|
|
538
|
+
.template-metric-layout--insight-top { grid-template-rows: auto minmax(0, 1fr); }
|
|
539
|
+
.template-metric-layout--insight-bottom { grid-template-rows: minmax(0, 1fr) auto; }
|
|
540
|
+
.template-stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; align-items: stretch; }
|
|
541
|
+
.template-stat-value { display: block; min-height: 96px; font-size: 58px; line-height: 1.42; color: var(--accent-primary); font-weight: 800; margin-bottom: 18px; padding-bottom: 14px; overflow: visible; }
|
|
542
|
+
.template-chart-panel { min-height: 520px; display: grid; place-items: center; border: 1px solid var(--line); background: rgba(255,255,255,0.72); }
|
|
543
|
+
.template-chart-placeholder { width: 76%; height: 56%; border-left: 2px solid var(--line-strong); border-bottom: 2px solid var(--line-strong); display: flex; align-items: end; gap: 28px; padding: 0 28px 24px; }
|
|
544
|
+
.template-visual-slot-panel { width: 100%; min-height: 520px; border: 1px dashed var(--line-strong); border-radius: var(--surface-radius); background: linear-gradient(135deg, rgba(49,94,234,0.08), rgba(24,168,216,0.08)); display: grid; place-items: center; padding: 0; }
|
|
545
|
+
.template-visual-slot-label { font-size: 13px; line-height: 1.35; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); font-weight: 800; }
|
|
546
|
+
.template-text-panel.template-chart-takeaway-panel { gap: 28px; background: linear-gradient(135deg, #5f82c8 0%, var(--accent-primary) 58%, #18a8d8 115%); color: white; box-shadow: 0 22px 56px rgba(49,94,234,0.24); }
|
|
547
|
+
.template-chart-takeaway-panel .template-text-panel-title { color: white; }
|
|
548
|
+
.template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
|
|
549
|
+
.template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
|
|
550
|
+
.template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
|
|
551
|
+
.template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: white; }
|
|
552
|
+
.template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: rgba(255,255,255,0.78); }
|
|
553
|
+
.template-bar { flex: 1; background: linear-gradient(180deg, var(--accent-primary), var(--accent-cyan)); min-height: 80px; }
|
|
554
|
+
.template-table-wrap { display: grid; grid-template-rows: minmax(0, auto) auto; gap: 22px; height: 100%; align-content: start; }
|
|
555
|
+
.template-table { width: 100%; border-collapse: collapse; background: rgba(255,255,255,0.86); box-shadow: 0 18px 44px var(--shadow-soft); }
|
|
556
|
+
.template-table th, .template-table td { padding: 22px 24px; border-bottom: 1px solid var(--line); text-align: left; font-size: 21px; }
|
|
557
|
+
.template-table th { color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.12em; font-size: 15px; }
|
|
558
|
+
.template-text-panel { min-height: 0; display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 20px; background: rgba(255,255,255,0.74); border-radius: var(--surface-radius); padding: 42px; }
|
|
559
|
+
.template-text-panel-title { margin: 0; font-size: 34px; line-height: 1.28; color: var(--text-primary); padding-bottom: 4px; overflow: visible; }
|
|
560
|
+
.template-text-panel-body { margin: 0; font-size: 23px; line-height: 1.52; color: var(--text-secondary); }
|
|
561
|
+
.template-side-panel { align-self: stretch; }
|
|
562
|
+
.template-side-panel-title { margin: 0; }
|
|
563
|
+
.template-side-panel-body { margin: 0; }
|
|
564
|
+
.template-insight-panel { display: grid; gap: 10px; background: rgba(255,255,255,0.88); border: 1px solid var(--line); border-radius: var(--surface-radius); padding: 22px 24px; box-shadow: 0 14px 34px var(--shadow-soft); }
|
|
565
|
+
.template-insight-title { margin: 0; display: flex; align-items: center; gap: 12px; font-size: 24px; line-height: 1.24; color: var(--text-primary); }
|
|
566
|
+
.template-insight-icon { width: 24px; height: 24px; color: var(--accent-primary); stroke-width: 2.2; flex: 0 0 auto; }
|
|
567
|
+
.template-insight-body { margin: 0; font-size: 20px; line-height: 1.42; color: var(--text-secondary); }
|
|
568
|
+
.template-metric-layout .template-insight-panel { border: 0; box-shadow: none; background: rgba(255,255,255,0.74); padding: 24px 28px; }
|
|
569
|
+
.template-metric-layout .template-insight-title { font-size: 18px; line-height: 1.22; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
|
|
570
|
+
.template-metric-layout .template-insight-icon { width: 20px; height: 20px; }
|
|
571
|
+
.template-metric-layout .template-insight-body { font-size: 24px; line-height: 1.42; color: var(--text-primary); }
|
|
572
|
+
.template-timeline { position: relative; height: 100%; display: grid; align-items: center; }
|
|
573
|
+
.template-timeline-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; }
|
|
574
|
+
.template-timeline-layout--left { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
|
|
575
|
+
.template-timeline-layout--left .template-side-panel { grid-column: 1; grid-row: 1; }
|
|
576
|
+
.template-timeline-layout--left .template-timeline { grid-column: 2; grid-row: 1; }
|
|
577
|
+
.template-timeline-layout--right { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
|
|
578
|
+
.template-timeline-layout--right .template-timeline { grid-column: 1; grid-row: 1; }
|
|
579
|
+
.template-timeline-layout--right .template-side-panel { grid-column: 2; grid-row: 1; }
|
|
580
|
+
.template-timeline-layout .template-text-panel { background: linear-gradient(135deg, #7a7fe8 0%, #5f82c8 58%, #315eea 115%); color: white; box-shadow: 0 22px 56px rgba(49,94,234,0.22); }
|
|
581
|
+
.template-timeline-layout .template-text-panel-title { color: white; }
|
|
582
|
+
.template-timeline-layout .template-text-panel-body { color: rgba(255,255,255,0.78); }
|
|
583
|
+
.template-timeline--horizontal { grid-template-columns: repeat(var(--timeline-count), 1fr); column-gap: 18px; align-items: stretch; --timeline-axis-y: 86%; }
|
|
584
|
+
.template-timeline--horizontal::before { content: ""; position: absolute; left: 4%; right: 4%; top: var(--timeline-axis-y); border-top: 2px solid var(--line-strong); transform: translateY(-1px); }
|
|
585
|
+
.template-timeline-item { position: relative; min-height: 400px; display: grid; justify-items: center; align-items: center; }
|
|
586
|
+
.template-timeline--horizontal .template-timeline-item { grid-template-rows: minmax(0, 1fr) 42px 86px; align-items: stretch; }
|
|
587
|
+
.template-timeline-dot { z-index: 2; width: 22px; height: 22px; border-radius: 999px; background: var(--accent-primary); box-shadow: 0 0 0 8px rgba(49,94,234,0.12); }
|
|
588
|
+
.template-timeline-copy { z-index: 2; width: 86%; padding: 18px 4px; background: transparent; border: 0; box-shadow: none; }
|
|
589
|
+
.template-timeline--horizontal .template-timeline-copy.template-card { width: 94%; height: calc(100% - 55px); min-height: 254px; align-self: end; justify-self: center; display: flex; flex-direction: column; justify-content: flex-start; padding: 28px 24px 24px; margin-bottom: 22px; }
|
|
590
|
+
.template-timeline--horizontal .template-timeline-item--highlight .template-timeline-copy.template-card { height: calc(100% - 22px); min-height: 254px; }
|
|
591
|
+
.template-timeline--horizontal .template-insight-icon { width: 28px; height: 28px; margin: 0 auto 28px; color: var(--accent-primary); stroke-width: 2; flex: 0 0 auto; }
|
|
592
|
+
.template-timeline--horizontal .template-timeline-item--highlight .template-insight-icon { color: #f37021; }
|
|
593
|
+
.template-timeline--horizontal .template-timeline-item--highlight .template-timeline-copy h3 { color: #f37021; }
|
|
594
|
+
.template-timeline--horizontal .template-timeline-dot { grid-row: 2; align-self: center; justify-self: center; }
|
|
595
|
+
.template-timeline--horizontal .template-timeline-date { grid-row: 3; align-self: start; justify-self: center; margin: 8px 0 0; font-size: 38px; line-height: 1; font-weight: 300; letter-spacing: 0.03em; color: var(--text-muted); }
|
|
596
|
+
.template-timeline-date { margin: 0 0 8px; font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--accent-primary); font-weight: 800; }
|
|
597
|
+
.template-timeline-copy h3 { margin: 0 0 8px; font-size: 27px; line-height: 1.28; padding-bottom: 4px; overflow: visible; }
|
|
598
|
+
.template-timeline-copy p:last-child { margin: 0; font-size: 19px; color: var(--text-secondary); }
|
|
599
|
+
.template-timeline--vertical { grid-template-columns: 1fr; align-items: stretch; padding: 18px 0; }
|
|
600
|
+
.template-timeline--vertical::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; border-left: 2px solid var(--line-strong); }
|
|
601
|
+
.template-timeline--vertical .template-timeline-item { min-height: 128px; grid-template-columns: 1fr 56px 1fr; justify-items: stretch; }
|
|
602
|
+
.template-timeline--vertical .template-timeline-dot { grid-column: 2; grid-row: 1; justify-self: center; align-self: center; }
|
|
603
|
+
.template-timeline--vertical .template-timeline-copy { grid-row: 1; width: auto; align-self: center; background: transparent; border: 0; box-shadow: none; }
|
|
604
|
+
.template-timeline--vertical .template-timeline-item:nth-child(odd) .template-timeline-copy { grid-column: 1; text-align: right; align-self: center; }
|
|
605
|
+
.template-timeline--vertical .template-timeline-item:nth-child(even) .template-timeline-copy { grid-column: 3; text-align: left; align-self: center; }
|
|
606
|
+
.template-steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; }
|
|
607
|
+
.template-step-number { font-size: 48px; color: var(--accent-primary); font-weight: 800; margin-bottom: 30px; }
|
|
608
|
+
.template-image-card { width: 100%; margin: 18px 0 0; display: grid; gap: 8px; }
|
|
609
|
+
.template-image-frame { width: 100%; height: 128px; border-radius: var(--surface-radius); overflow: hidden; background: var(--surface-tint, #f1f6fc); border: 1px solid var(--line); }
|
|
610
|
+
.template-image-frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
611
|
+
.template-image-caption { margin: 0; font-size: 13px; line-height: 1.35; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
612
|
+
.template-visual-placeholder { width: 100%; margin: 18px 0 0; display: grid; gap: 8px; }
|
|
613
|
+
.template-visual-placeholder-frame { width: 100%; height: 148px; border-radius: var(--surface-radius); border: 1px dashed var(--line-strong); background: linear-gradient(135deg, rgba(49,94,234,0.08), rgba(24,168,216,0.08)); display: grid; place-items: center; }
|
|
614
|
+
.template-visual-placeholder-label { font-size: 13px; line-height: 1.35; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); font-weight: 800; }
|
|
615
|
+
.template-page-number { position: absolute; right: 72px; bottom: 52px; font-size: 15px; color: var(--text-muted); letter-spacing: 0.18em; }
|
|
616
|
+
.template-catalog-panel { flex: 0 0 auto; margin-top: auto; background: rgba(255,255,255,0.9); border: 1px solid var(--line); border-radius: var(--surface-radius); box-shadow: 0 18px 44px var(--shadow-soft); padding: 16px 22px; color: var(--text-primary); }
|
|
617
|
+
.template-hero .template-catalog-panel { background: rgba(247,249,252,0.92); }
|
|
618
|
+
.template-catalog-kicker { margin: 0 0 4px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.16em; color: var(--accent-primary); font-weight: 800; }
|
|
619
|
+
.template-catalog-title { margin: 0 0 10px; font-size: 20px; line-height: 1.28; font-weight: 800; }
|
|
620
|
+
.template-catalog-grid { display: grid; grid-template-columns: 1.15fr 1fr 1fr; gap: 16px; }
|
|
621
|
+
.template-catalog-section { min-width: 0; }
|
|
622
|
+
.template-catalog-section h3 { margin: 0 0 6px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-muted); }
|
|
623
|
+
.template-catalog-section p { margin: 0; font-size: 15px; line-height: 1.36; color: var(--text-secondary); }
|
|
624
|
+
.template-catalog-list { margin: 0; padding-left: 16px; display: grid; gap: 2px; }
|
|
625
|
+
.template-catalog-list li { font-size: 14px; line-height: 1.3; color: var(--text-secondary); }
|
|
626
|
+
.template-frame--catalog .template-title { font-size: 52px; line-height: 1.18; }
|
|
627
|
+
.template-slide[data-template="agenda"] .template-frame--catalog .template-title { font-size: 54px; line-height: 1.04; }
|
|
628
|
+
.template-frame--catalog .template-card { padding: 22px; }
|
|
629
|
+
.template-frame--catalog .template-card h2,
|
|
630
|
+
.template-frame--catalog .template-card h3 { font-size: 24px; line-height: 1.22; margin-bottom: 8px; }
|
|
631
|
+
.template-frame--catalog .template-card p { font-size: 18px; line-height: 1.32; }
|
|
632
|
+
.template-frame--catalog .template-key-message-panel { gap: 16px; }
|
|
633
|
+
.template-frame--catalog .template-key-message-kicker { font-size: 23px; line-height: 1.2; }
|
|
634
|
+
.template-frame--catalog .template-key-message-panel p { font-size: 19px; line-height: 1.42; }
|
|
635
|
+
.template-frame--catalog .template-claim-text-panel { gap: 12px; }
|
|
636
|
+
.template-frame--catalog .template-claim-text-title { font-size: 24px; line-height: 1.24; }
|
|
637
|
+
.template-frame--catalog .template-claim-text-body { font-size: 18px; line-height: 1.36; }
|
|
638
|
+
.template-frame--catalog .template-evidence-grid { gap: 18px; }
|
|
639
|
+
.template-frame--catalog .template-list { gap: 12px; }
|
|
640
|
+
.template-frame--catalog .template-list li { font-size: 20px; line-height: 1.28; }
|
|
641
|
+
.template-frame--catalog .template-metric-layout { gap: 18px; }
|
|
642
|
+
.template-frame--catalog .template-metric-layout .template-card { padding: 20px; }
|
|
643
|
+
.template-frame--catalog .template-metric-layout .template-stat-value { min-height: 70px; font-size: 48px; line-height: 1.24; margin-bottom: 10px; padding-bottom: 8px; }
|
|
644
|
+
.template-frame--catalog .template-metric-layout .template-insight-panel { padding: 18px 22px; gap: 7px; }
|
|
645
|
+
.template-frame--catalog .template-metric-layout .template-insight-title { font-size: 13px; line-height: 1.22; }
|
|
646
|
+
.template-frame--catalog .template-metric-layout .template-insight-icon { width: 16px; height: 16px; }
|
|
647
|
+
.template-frame--catalog .template-metric-layout .template-insight-body { font-size: 19px; line-height: 1.34; }
|
|
648
|
+
.template-frame--catalog .template-chart-panel { min-height: 360px; }
|
|
649
|
+
.template-frame--catalog .template-visual-slot-panel { min-height: 360px; }
|
|
650
|
+
.template-frame--catalog .template-visual-slot-label { font-size: 11px; }
|
|
651
|
+
.template-frame--catalog .template-chart-takeaway-panel { padding: 24px; gap: 16px; }
|
|
652
|
+
.template-frame--catalog .template-chart-takeaway-list { gap: 13px; }
|
|
653
|
+
.template-frame--catalog .template-chart-takeaway-item { gap: 4px; padding-top: 11px; }
|
|
654
|
+
.template-frame--catalog .template-chart-takeaway-item h3 { font-size: 19px; line-height: 1.2; }
|
|
655
|
+
.template-frame--catalog .template-chart-takeaway-item p { font-size: 15px; line-height: 1.3; }
|
|
656
|
+
.template-frame--catalog .template-table-wrap { gap: 16px; }
|
|
657
|
+
.template-frame--catalog .template-table th,
|
|
658
|
+
.template-frame--catalog .template-table td { padding: 14px 18px; font-size: 17px; line-height: 1.32; }
|
|
659
|
+
.template-frame--catalog .template-table th { font-size: 12px; }
|
|
660
|
+
.template-frame--catalog .template-insight-panel { padding: 16px 18px; gap: 6px; }
|
|
661
|
+
.template-frame--catalog .template-insight-title { font-size: 20px; line-height: 1.2; }
|
|
662
|
+
.template-frame--catalog .template-insight-icon { width: 20px; height: 20px; }
|
|
663
|
+
.template-frame--catalog .template-insight-body { font-size: 16px; line-height: 1.32; }
|
|
664
|
+
.template-frame--catalog .template-timeline--vertical { padding: 6px 0; }
|
|
665
|
+
.template-frame--catalog .template-timeline--vertical .template-timeline-item { min-height: 96px; }
|
|
666
|
+
.template-frame--catalog .template-timeline-layout { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 22px; }
|
|
667
|
+
.template-frame--catalog .template-timeline-layout--left { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
|
|
668
|
+
.template-frame--catalog .template-timeline-layout--right { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
|
|
669
|
+
.template-frame--catalog .template-timeline-layout .template-text-panel { padding: 22px; gap: 10px; }
|
|
670
|
+
.template-frame--catalog .template-timeline-layout .template-text-panel-title { font-size: 25px; line-height: 1.3; }
|
|
671
|
+
.template-frame--catalog .template-timeline-layout .template-text-panel-body { font-size: 18px; line-height: 1.4; }
|
|
672
|
+
.template-frame--catalog .template-timeline--horizontal .template-timeline-item { grid-template-rows: minmax(0, 1fr) 32px 58px; min-height: 330px; }
|
|
673
|
+
.template-frame--catalog .template-timeline--horizontal .template-timeline-copy.template-card { height: calc(100% - 51px); min-height: 178px; padding: 20px 18px 18px; margin-bottom: 18px; }
|
|
674
|
+
.template-frame--catalog .template-timeline--horizontal .template-timeline-item--highlight .template-timeline-copy.template-card { height: calc(100% - 18px); min-height: 178px; }
|
|
675
|
+
.template-frame--catalog .template-timeline--horizontal .template-insight-icon { width: 22px; height: 22px; margin-bottom: 18px; }
|
|
676
|
+
.template-frame--catalog .template-timeline--horizontal .template-timeline-date { font-size: 28px; }
|
|
677
|
+
.template-frame--catalog .template-timeline-copy { padding: 8px 4px; }
|
|
678
|
+
.template-frame--catalog .template-timeline-copy h3 { font-size: 21px; line-height: 1.18; margin-bottom: 4px; }
|
|
679
|
+
.template-frame--catalog .template-timeline-date { font-size: 12px; margin-bottom: 4px; }
|
|
680
|
+
.template-frame--catalog .template-timeline-copy p:last-child { font-size: 15px; line-height: 1.24; }
|
|
681
|
+
.template-frame--catalog .template-steps { gap: 16px; }
|
|
682
|
+
.template-frame--catalog .template-step-number { font-size: 40px; margin-bottom: 20px; }
|
|
683
|
+
.template-frame--catalog .template-image-frame { height: 86px; }
|
|
684
|
+
.template-frame--catalog .template-image-caption { font-size: 11px; }
|
|
685
|
+
.template-frame--catalog .template-visual-placeholder-frame { height: 110px; }
|
|
686
|
+
.template-frame--catalog .template-visual-placeholder-label { font-size: 11px; }
|
|
687
|
+
`
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function ensureInlineLucideRuntime(html: string): string {
|
|
691
|
+
if (html.includes("function revelaRenderLucideIcons")) return html
|
|
692
|
+
const runtime = `<script>
|
|
693
|
+
function revelaRenderLucideIcons() {
|
|
694
|
+
var icons = {
|
|
695
|
+
lightbulb: '<svg xmlns="http://www.w3.org/2000/svg" class="template-insight-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 14c.2-1 .7-1.7 1.5-2.5A5.5 5.5 0 0 0 18 7.5C18 4.5 15.5 2 12 2S6 4.5 6 7.5c0 1.5.6 2.9 1.5 4 .8.8 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>',
|
|
696
|
+
'scan-search': '<svg xmlns="http://www.w3.org/2000/svg" class="template-insight-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 3H5a2 2 0 0 0-2 2v2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M14 14l4 4"/><circle cx="11" cy="11" r="3"/></svg>'
|
|
697
|
+
};
|
|
698
|
+
document.querySelectorAll("[data-lucide]").forEach(function (node) {
|
|
699
|
+
var name = node.getAttribute("data-lucide") || "lightbulb";
|
|
700
|
+
var svg = icons[name] || icons.lightbulb;
|
|
701
|
+
var wrapper = document.createElement("span");
|
|
702
|
+
wrapper.innerHTML = svg;
|
|
703
|
+
var next = wrapper.firstElementChild;
|
|
704
|
+
if (next) node.replaceWith(next);
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
revelaRenderLucideIcons();
|
|
708
|
+
</script>`
|
|
709
|
+
return html.replace("</body>", ` ${runtime}\n</body>`)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function slidePresentationRuntime(): string {
|
|
713
|
+
return ` <script>
|
|
714
|
+
class SlidePresentation {
|
|
715
|
+
constructor() {
|
|
716
|
+
this.slides = document.querySelectorAll('.slide');
|
|
717
|
+
this.currentSlide = 0;
|
|
718
|
+
this.setupScaling();
|
|
719
|
+
this.setupIntersectionObserver();
|
|
720
|
+
this.setupKeyboardNav();
|
|
721
|
+
this.setupTouchNav();
|
|
722
|
+
this.setupMouseWheel();
|
|
723
|
+
}
|
|
724
|
+
setupScaling() {
|
|
725
|
+
const canvases = document.querySelectorAll('.slide-canvas');
|
|
726
|
+
const BASE_W = 1920;
|
|
727
|
+
const BASE_H = 1080;
|
|
728
|
+
const update = () => {
|
|
729
|
+
const scale = Math.min(window.innerWidth / BASE_W, window.innerHeight / BASE_H);
|
|
730
|
+
canvases.forEach((canvas) => { canvas.style.transform = \`scale(\${scale})\`; });
|
|
731
|
+
};
|
|
732
|
+
window.addEventListener('resize', update);
|
|
733
|
+
update();
|
|
734
|
+
}
|
|
735
|
+
setupIntersectionObserver() {
|
|
736
|
+
const observer = new IntersectionObserver((entries) => {
|
|
737
|
+
entries.forEach((entry) => {
|
|
738
|
+
if (entry.isIntersecting) entry.target.querySelectorAll('.reveal').forEach((el) => el.classList.add('visible'));
|
|
739
|
+
});
|
|
740
|
+
}, { threshold: 0.2 });
|
|
741
|
+
this.slides.forEach((slide) => observer.observe(slide));
|
|
742
|
+
}
|
|
743
|
+
setupKeyboardNav() {
|
|
744
|
+
document.addEventListener('keydown', (event) => {
|
|
745
|
+
if (['ArrowDown', 'ArrowRight', ' ', 'PageDown'].includes(event.key)) { event.preventDefault(); this.goTo(this.currentSlide + 1); }
|
|
746
|
+
else if (['ArrowUp', 'ArrowLeft', 'PageUp'].includes(event.key)) { event.preventDefault(); this.goTo(this.currentSlide - 1); }
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
setupTouchNav() {
|
|
750
|
+
let startY = 0;
|
|
751
|
+
document.addEventListener('touchstart', (event) => { startY = event.touches[0].clientY; }, { passive: true });
|
|
752
|
+
document.addEventListener('touchend', (event) => {
|
|
753
|
+
const deltaY = startY - event.changedTouches[0].clientY;
|
|
754
|
+
if (Math.abs(deltaY) > 40) this.goTo(this.currentSlide + (deltaY > 0 ? 1 : -1));
|
|
755
|
+
}, { passive: true });
|
|
756
|
+
}
|
|
757
|
+
setupMouseWheel() {
|
|
758
|
+
let last = 0;
|
|
759
|
+
document.addEventListener('wheel', (event) => {
|
|
760
|
+
const now = Date.now();
|
|
761
|
+
if (now - last < 800) return;
|
|
762
|
+
last = now;
|
|
763
|
+
this.goTo(this.currentSlide + (event.deltaY > 0 ? 1 : -1));
|
|
764
|
+
}, { passive: true });
|
|
765
|
+
}
|
|
766
|
+
goTo(index) {
|
|
767
|
+
const clamped = Math.max(0, Math.min(this.slides.length - 1, index));
|
|
768
|
+
this.slides[clamped].scrollIntoView({ behavior: 'smooth' });
|
|
769
|
+
this.currentSlide = clamped;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (document.querySelector(".slide")) { new SlidePresentation(); }
|
|
773
|
+
</script>`
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function cssUrl(value: string): string {
|
|
777
|
+
return value.replace(/\\/g, "/").replace(/"/g, "%22").replace(/\n|\r|\f/g, "")
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function validatePageTemplateContracts(filePath: string): PageTemplateContractReport {
|
|
781
|
+
const html = readFileSync(filePath, "utf-8")
|
|
782
|
+
const issues: PageTemplateContractIssue[] = []
|
|
783
|
+
for (const section of html.matchAll(/<section\b[^>]*class=["'][^"']*\bslide\b[^"']*["'][^>]*data-template=["']([^"']+)["'][^>]*>([\s\S]*?)<\/section>/gi)) {
|
|
784
|
+
const templateId = section[1]
|
|
785
|
+
const body = section[2]
|
|
786
|
+
const slideIndex = Number(/data-slide-index=["'](\d+)["']/i.exec(section[0])?.[1])
|
|
787
|
+
issues.push(...validateVocabularyContract(templateId, body, Number.isInteger(slideIndex) ? slideIndex : undefined))
|
|
788
|
+
if (templateId === "timeline-roadmap") issues.push(...validateTimelineContract(body, Number.isInteger(slideIndex) ? slideIndex : undefined))
|
|
789
|
+
}
|
|
790
|
+
return { ok: !issues.some((issue) => issue.severity === "error"), issues }
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export function formatPageTemplateContractReport(report: PageTemplateContractReport): string {
|
|
794
|
+
if (report.issues.length === 0) return "Template contracts passed."
|
|
795
|
+
return report.issues.map((issue) => `- ${issue.severity.toUpperCase()}: ${issue.templateId}${issue.slideIndex ? ` slide ${issue.slideIndex}` : ""}: ${issue.message}`).join("\n")
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export function validateBoundedTemplateEdit(input: BoundedTemplateEditInput): PageTemplateContractReport {
|
|
799
|
+
const issues: PageTemplateContractIssue[] = []
|
|
800
|
+
const slideIndex = positiveIndex(input.slideIndex)
|
|
801
|
+
const beforeSlides = slideSections(input.beforeHtml)
|
|
802
|
+
const afterSlides = slideSections(input.afterHtml)
|
|
803
|
+
const beforeKeys = [...beforeSlides.keys()].sort((a, b) => a - b)
|
|
804
|
+
const afterKeys = [...afterSlides.keys()].sort((a, b) => a - b)
|
|
805
|
+
if (beforeKeys.join(",") !== afterKeys.join(",")) {
|
|
806
|
+
issues.push({ severity: "error", templateId: "bounded-edit", message: "Bounded edit must preserve the slide index set." })
|
|
807
|
+
}
|
|
808
|
+
for (const index of afterKeys) {
|
|
809
|
+
if (index === slideIndex) continue
|
|
810
|
+
if (beforeSlides.get(index) !== afterSlides.get(index)) issues.push({ severity: "error", templateId: "bounded-edit", slideIndex: index, message: "Bounded edit changed a slide outside the target slide." })
|
|
811
|
+
}
|
|
812
|
+
const target = afterSlides.get(slideIndex)
|
|
813
|
+
if (!target) {
|
|
814
|
+
issues.push({ severity: "error", templateId: "bounded-edit", slideIndex, message: "Bounded edit target slide is missing." })
|
|
815
|
+
} else {
|
|
816
|
+
const templateId = /data-template=["']([^"']+)["']/i.exec(target)?.[1] || "unknown"
|
|
817
|
+
issues.push(...validateVocabularyContract(templateId, target, slideIndex))
|
|
818
|
+
if (templateId === "timeline-roadmap") issues.push(...validateTimelineContract(target, slideIndex))
|
|
819
|
+
}
|
|
820
|
+
return { ok: !issues.some((issue) => issue.severity === "error"), issues }
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function define(id: string, title: string, purpose: string, fields: PageTemplateField[], contentRules: string[], qaRules: string[]): PageTemplateDefinition {
|
|
824
|
+
return { id, title, purpose, status: "renderable", fields, contentRules, qaRules }
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function validateTimelineContract(html: string, slideIndex?: number): PageTemplateContractIssue[] {
|
|
828
|
+
const issues: PageTemplateContractIssue[] = []
|
|
829
|
+
const root = /class=["'][^"']*\btemplate-timeline\b[^"']*["']/i.test(html)
|
|
830
|
+
if (!root) {
|
|
831
|
+
issues.push({ severity: "error", templateId: "timeline-roadmap", slideIndex, message: "Missing .template-timeline root." })
|
|
832
|
+
return issues
|
|
833
|
+
}
|
|
834
|
+
const itemMatches = [...html.matchAll(/<article\b[^>]*class=["'][^"']*\btemplate-timeline-item\b[^"']*["'][^>]*>([\s\S]*?)<\/article>/gi)]
|
|
835
|
+
if (itemMatches.length < 3) issues.push({ severity: "warning", templateId: "timeline-roadmap", slideIndex, message: "Timeline should usually contain at least three milestones." })
|
|
836
|
+
for (let index = 0; index < itemMatches.length; index++) {
|
|
837
|
+
const item = itemMatches[index][1]
|
|
838
|
+
if (!/class=["'][^"']*\btemplate-timeline-dot\b[^"']*["']/i.test(item)) issues.push({ severity: "error", templateId: "timeline-roadmap", slideIndex, message: `Milestone ${index + 1} is missing .template-timeline-dot inside its item.` })
|
|
839
|
+
if (!/class=["'][^"']*\btemplate-timeline-copy\b[^"']*["']/i.test(item)) issues.push({ severity: "error", templateId: "timeline-roadmap", slideIndex, message: `Milestone ${index + 1} is missing .template-timeline-copy inside its item.` })
|
|
840
|
+
}
|
|
841
|
+
const dotCount = (html.match(/\btemplate-timeline-dot\b/g) ?? []).length
|
|
842
|
+
const copyCount = (html.match(/\btemplate-timeline-copy\b/g) ?? []).length
|
|
843
|
+
if (dotCount !== copyCount) issues.push({ severity: "error", templateId: "timeline-roadmap", slideIndex, message: `Timeline dot count (${dotCount}) must match copy count (${copyCount}).` })
|
|
844
|
+
return issues
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function validateVocabularyContract(templateId: string, html: string, slideIndex?: number): PageTemplateContractIssue[] {
|
|
848
|
+
const issues: PageTemplateContractIssue[] = []
|
|
849
|
+
let vocabulary
|
|
850
|
+
try {
|
|
851
|
+
vocabulary = getPageTemplateVocabulary(templateId)
|
|
852
|
+
} catch {
|
|
853
|
+
return issues
|
|
854
|
+
}
|
|
855
|
+
for (const className of vocabulary.requiredClasses) {
|
|
856
|
+
if (!hasClass(html, className)) issues.push({ severity: "error", templateId, slideIndex, message: `Missing required template class .${className}.` })
|
|
857
|
+
}
|
|
858
|
+
for (const slot of vocabulary.slots.filter((item) => item.required)) {
|
|
859
|
+
if (!hasTemplateSlot(html, slot.name)) issues.push({ severity: "error", templateId, slideIndex, message: `Missing required template slot '${slot.name}'.` })
|
|
860
|
+
}
|
|
861
|
+
for (const slot of vocabulary.slots.filter((item) => item.replaceable && item.name === "visual")) {
|
|
862
|
+
if (hasTemplateSlot(html, slot.name) && !hasVisualSemanticContainer(html)) issues.push({ severity: "error", templateId, slideIndex, message: "Visual slot must keep an image, chart, table, diagram, or template visual slot semantic container." })
|
|
863
|
+
}
|
|
864
|
+
return issues
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function slideSections(html: string): Map<number, string> {
|
|
868
|
+
const sections = new Map<number, string>()
|
|
869
|
+
for (const match of html.matchAll(/<section\b[^>]*class=["'][^"']*\bslide\b[^"']*["'][^>]*data-slide-index=["'](\d+)["'][^>]*>[\s\S]*?<\/section>/gi)) {
|
|
870
|
+
sections.set(Number(match[1]), match[0])
|
|
871
|
+
}
|
|
872
|
+
return sections
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function hasClass(html: string, className: string): boolean {
|
|
876
|
+
return new RegExp(`class=["'][^"']*\\b${escapeRegExp(className)}\\b[^"']*["']`, "i").test(html)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function hasTemplateSlot(html: string, slot: string): boolean {
|
|
880
|
+
return new RegExp(`data-template-slot=["']${escapeRegExp(slot)}["']`, "i").test(html)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function hasVisualSemanticContainer(html: string): boolean {
|
|
884
|
+
return /\b(template-visual-slot-panel|template-image-card|template-chart-panel|template-table|echart-panel|data-table|media-frame|<img\b|<svg\b|<canvas\b|<table\b)/i.test(html)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function field(name: string, type: PageTemplateField["type"], description: string, required = false): PageTemplateField {
|
|
888
|
+
return { name, type, description, required }
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function getPageTemplate(templateId: string): PageTemplateDefinition {
|
|
892
|
+
const id = String(templateId || "").trim()
|
|
893
|
+
const template = templates.find((item) => item.id === id)
|
|
894
|
+
if (!template) throw new Error(`Unknown page template: ${templateId}`)
|
|
895
|
+
return template
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function renderSlideShell(input: { template: PageTemplateDefinition; slideIndex: number; designName: string; title: string; body: string; catalog?: any }): string {
|
|
899
|
+
const hero = ["cover", "section-divider", "closing"].includes(input.template.id)
|
|
900
|
+
const slideQa = hero ? "false" : "true"
|
|
901
|
+
const slideRole = input.template.id === "cover" || input.template.id === "closing" ? ` data-slide-role="${input.template.id}"` : ""
|
|
902
|
+
const hasCatalog = Boolean(input.catalog)
|
|
903
|
+
const heroModifier = hero ? ` template-hero--${input.template.id}` : ""
|
|
904
|
+
return ` <section class="slide template-slide" slide-qa="${slideQa}" data-slide-index="${input.slideIndex}"${slideRole} data-design="${escapeAttribute(input.designName)}" data-template="${escapeAttribute(input.template.id)}">
|
|
905
|
+
<div class="slide-canvas">
|
|
906
|
+
<div class="template-frame${hero ? " template-hero" : ""}${heroModifier}${hasCatalog ? " template-frame--catalog" : ""}">
|
|
907
|
+
${input.body}
|
|
908
|
+
${renderCatalogPanel(input.template, input.catalog)}
|
|
909
|
+
</div>
|
|
910
|
+
<div class="template-page-number">${String(input.slideIndex).padStart(2, "0")}</div>
|
|
911
|
+
</div>
|
|
912
|
+
</section>`
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function renderCatalogPanel(template: PageTemplateDefinition, content: any): string {
|
|
916
|
+
if (!content || typeof content !== "object") return ""
|
|
917
|
+
const fields = Array.isArray(content.fields) ? content.fields : template.fields.filter((field) => field.required).map((field) => field.name)
|
|
918
|
+
const qa = Array.isArray(content.qa) ? content.qa : template.qaRules
|
|
919
|
+
return `<aside class="template-catalog-panel">
|
|
920
|
+
<p class="template-catalog-kicker">${escapeHtml(template.id)}</p>
|
|
921
|
+
<h2 class="template-catalog-title">${escapeHtml(content.title || template.title)}</h2>
|
|
922
|
+
<div class="template-catalog-grid">
|
|
923
|
+
<section class="template-catalog-section"><h3>Purpose</h3><p>${escapeHtml(content.purpose || template.purpose)}</p></section>
|
|
924
|
+
<section class="template-catalog-section"><h3>Fields</h3><ul class="template-catalog-list">${fields.slice(0, 5).map((item: any) => `<li>${escapeHtml(String(item))}</li>`).join("")}</ul></section>
|
|
925
|
+
<section class="template-catalog-section"><h3>QA</h3><ul class="template-catalog-list">${qa.slice(0, 3).map((item: any) => `<li>${escapeHtml(String(item))}</li>`).join("")}</ul></section>
|
|
926
|
+
</div>
|
|
927
|
+
</aside>`
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function renderHeader(content: Record<string, any>, fallbackTitle = "", options: { hero?: boolean } = {}): string {
|
|
931
|
+
const eyebrow = stringValue(content.eyebrow)
|
|
932
|
+
const titleClass = options.hero ? "template-title template-hero-title" : "template-title"
|
|
933
|
+
return `<header>
|
|
934
|
+
${eyebrow ? `<p class="template-eyebrow">${escapeHtml(eyebrow)}</p>` : ""}
|
|
935
|
+
<h1 class="${titleClass}">${escapeHtml(stringValue(content.title) || fallbackTitle)}</h1>
|
|
936
|
+
</header>`
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function renderBody(templateId: string, content: Record<string, any>): string {
|
|
940
|
+
if (["cover", "section-divider", "closing"].includes(templateId)) return `<div data-template-slot="hero">${renderHeader(content, templateId, { hero: true })}</div>`
|
|
941
|
+
if (templateId === "agenda") return renderAgenda(content)
|
|
942
|
+
if (templateId === "executive-summary") return `${renderHeader(content, "Executive Summary")}<div class="template-body template-grid cols-3" data-template-slot="summary-cards">${cards(items(content), "h2", { visualPlaceholder: true })}</div>`
|
|
943
|
+
if (templateId === "problem-context") return `${renderHeader(content, "Problem / Context")}<div class="template-body template-grid cols-2"><div class="template-card" data-template-slot="context"><p>${escapeHtml(stringValue(content.body))}</p></div><div class="template-card" data-template-slot="supporting-points">${list(items(content))}</div></div>`
|
|
944
|
+
if (templateId === "key-message-evidence") return `${renderHeader(content, "Key Message + Evidence")}<div class="template-body template-grid cols-2">${keyMessagePanel(content)}<div class="template-evidence-grid" data-template-slot="evidence">${evidenceCards(items(content))}</div></div>`
|
|
945
|
+
if (templateId === "claim-supporting-visual") return `${renderHeader(content, "Claim + Supporting Visual")}<div class="template-body template-grid cols-2">${claimTextPanel(content)}${visualSlotPanel()}</div>`
|
|
946
|
+
if (templateId === "metric-highlight") return `${renderHeader(content, "Metric Highlight")}<div class="template-body">${metricHighlight(content)}</div>`
|
|
947
|
+
if (templateId === "chart-takeaways") return `${renderHeader(content, "Chart + Takeaways")}<div class="template-body template-grid template-chart-layout">${visualSlotPanel()}${chartTakeawayPanel(content)}</div>`
|
|
948
|
+
if (templateId === "table-comparison") return `${renderHeader(content, "Table / Comparison")}<div class="template-body" data-template-slot="table">${table(content)}</div>`
|
|
949
|
+
if (templateId === "timeline-roadmap") return `${renderHeader(content, "Timeline / Roadmap")}<div class="template-body">${timeline(content)}</div>`
|
|
950
|
+
if (templateId === "process-steps") return `${renderHeader(content, "Process / Steps")}<div class="template-body"><div class="template-steps" data-template-slot="steps">${steps(content.steps)}</div></div>`
|
|
951
|
+
if (templateId === "recommendation-decision") return `${renderHeader(content, "Recommendation / Decision")}<div class="template-body template-grid cols-3"><div class="template-card" data-template-slot="recommendation"><h2>Recommendation</h2><p>${escapeHtml(stringValue(content.recommendation))}</p>${imageCard(content)}</div><div data-template-slot="rationale">${cards(items(content).slice(0, 1), "h3")}</div><div class="template-card" data-template-slot="next-steps"><h2>Next steps</h2>${orderedSteps(content.steps)}</div></div>`
|
|
952
|
+
if (templateId === "risks-tradeoffs") return `${renderHeader(content, "Risks / Tradeoffs")}<div class="template-body template-grid cols-3" data-template-slot="risks">${cards(items(content), "h3")}</div>`
|
|
953
|
+
return renderHeader(content, templateId)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function renderAgenda(content: Record<string, any>): string {
|
|
957
|
+
const agendaItems = items(content)
|
|
958
|
+
const eyebrow = stringValue(content.eyebrow)
|
|
959
|
+
const footer = stringValue(content.footer) || "Structure-First-Design"
|
|
960
|
+
return `<div class="template-body template-agenda-panel" data-template-slot="agenda">
|
|
961
|
+
<div class="template-agenda-inner">
|
|
962
|
+
<div class="template-agenda-header">
|
|
963
|
+
${eyebrow ? `<p class="template-eyebrow">${escapeHtml(eyebrow)}</p>` : ""}
|
|
964
|
+
<h1 class="template-title">${escapeHtml(stringValue(content.title) || "Agenda")}</h1>
|
|
965
|
+
<p class="template-agenda-footer">${escapeHtml(footer)}</p>
|
|
966
|
+
</div>
|
|
967
|
+
<ol class="template-agenda-list" data-template-slot="agenda-list">${agendaItems.map((item, index) => `<li class="template-agenda-item"><span>${String(index + 1).padStart(2, "0")}</span><strong>${escapeHtml(item.label)}</strong></li>`).join("")}</ol>
|
|
968
|
+
</div>
|
|
969
|
+
</div>`
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function keyMessagePanel(content: Record<string, any>): string {
|
|
973
|
+
return `<div class="template-key-message-panel" data-template-slot="key-message">
|
|
974
|
+
<h2 class="template-key-message-kicker">Key message</h2>
|
|
975
|
+
<p>${escapeHtml(stringValue(content.body))}</p>
|
|
976
|
+
</div>`
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function claimTextPanel(content: Record<string, any>): string {
|
|
980
|
+
return `<div class="template-claim-text-panel" data-template-slot="claim">
|
|
981
|
+
<h2 class="template-claim-text-title">${escapeHtml(stringValue(content.claim) || stringValue(content.title) || "Claim")}</h2>
|
|
982
|
+
<p class="template-claim-text-body">${escapeHtml(stringValue(content.body))}</p>
|
|
983
|
+
${list(items(content))}
|
|
984
|
+
</div>`
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function evidenceCards(items: Array<{ label: string; description: string; image?: string; imageAlt?: string; imageCaption?: string }>): string {
|
|
988
|
+
return items.map((item, index) => `<article class="template-card template-evidence-card"><h3>${escapeHtml(item.label || `Evidence ${index + 1}`)}</h3><p>${escapeHtml(item.description)}</p>${imageCard(item)}</article>`).join("")
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function chartTakeawayPanel(content: Record<string, any>): string {
|
|
992
|
+
const takeawayItems = items(content)
|
|
993
|
+
const title = stringValue(content.takeawaysTitle) || "What to read"
|
|
994
|
+
return `<div class="template-text-panel template-chart-takeaway-panel" data-template-slot="takeaways">
|
|
995
|
+
<h2 class="template-text-panel-title">${escapeHtml(title)}</h2>
|
|
996
|
+
<div class="template-chart-takeaway-list">${takeawayItems.map((item) => `<section class="template-chart-takeaway-item"><h3>${escapeHtml(item.label)}</h3><p>${escapeHtml(item.description)}</p></section>`).join("")}</div>
|
|
997
|
+
</div>`
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function cards(items: Array<{ label: string; description: string; image?: string; imageAlt?: string; imageCaption?: string }>, heading: "h2" | "h3", options: { visualPlaceholder?: boolean } = {}): string {
|
|
1001
|
+
return items.map((item) => `<article class="template-card"><${heading}>${escapeHtml(item.label)}</${heading}><p>${escapeHtml(item.description)}</p>${imageCard(item) || (options.visualPlaceholder ? visualPlaceholder() : "")}</article>`).join("")
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function list(items: Array<{ label: string; description: string }>): string {
|
|
1005
|
+
return `<ul class="template-list">${items.map((item) => `<li><strong>${escapeHtml(item.label)}</strong>${item.description ? ` ${escapeHtml(item.description)}` : ""}</li>`).join("")}</ul>`
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function metrics(input: any): string {
|
|
1009
|
+
const values = Array.isArray(input) ? input : []
|
|
1010
|
+
return values.slice(0, 4).map((item) => `<article class="template-card"><div class="template-stat-value">${escapeHtml(stringValue(item.value) || "0")}</div><h2>${escapeHtml(stringValue(item.label) || "Metric")}</h2><p>${escapeHtml(stringValue(item.description))}</p></article>`).join("")
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function metricHighlight(content: Record<string, any>): string {
|
|
1014
|
+
const statGrid = `<div class="template-stat-grid" data-template-slot="metrics">${metrics(content.metrics)}</div>`
|
|
1015
|
+
const panel = renderInsightPanel(content)
|
|
1016
|
+
if (!panel) return statGrid
|
|
1017
|
+
const position = stringValue(content.insightPosition) === "top" ? "top" : "bottom"
|
|
1018
|
+
return `<div class="template-metric-layout template-metric-layout--insight-${position}">${position === "top" ? `${panel}${statGrid}` : `${statGrid}${panel}`}</div>`
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function table(content: Record<string, any>): string {
|
|
1022
|
+
const columns = Array.isArray(content.columns) ? content.columns.map(stringValue).filter(Boolean) : ["Dimension", "Current", "Target"]
|
|
1023
|
+
const rows = Array.isArray(content.rows) ? content.rows : []
|
|
1024
|
+
const insight = renderInsightPanel(content)
|
|
1025
|
+
return `<div class="template-table-wrap"><table class="template-table"><thead><tr>${columns.map((column) => `<th>${escapeHtml(column)}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${columns.map((column, index) => `<td>${escapeHtml(Array.isArray(row) ? stringValue(row[index]) : stringValue(row[column]) || stringValue(row[slug(column)]))}</td>`).join("")}</tr>`).join("")}</tbody></table>${insight}</div>`
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function renderInsightPanel(content: Record<string, any>): string {
|
|
1029
|
+
const body = stringValue(content.insightBody)
|
|
1030
|
+
if (!body) return ""
|
|
1031
|
+
const title = stringValue(content.insightTitle) || "Insight"
|
|
1032
|
+
const icon = safeLucideIconName(stringValue(content.insightIcon) || "lightbulb")
|
|
1033
|
+
return `<div class="template-insight-panel">
|
|
1034
|
+
<h2 class="template-insight-title"><i class="template-insight-icon" data-lucide="${escapeAttribute(icon)}" aria-hidden="true"></i><span>${escapeHtml(title)}</span></h2>
|
|
1035
|
+
<p class="template-insight-body">${escapeHtml(body)}</p>
|
|
1036
|
+
</div>`
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function timeline(content: Record<string, any>): string {
|
|
1040
|
+
const milestones = Array.isArray(content.milestones) ? content.milestones : []
|
|
1041
|
+
const orientation = stringValue(content.orientation) === "vertical" ? "vertical" : "horizontal"
|
|
1042
|
+
const sidePanel = renderSidePanel(content)
|
|
1043
|
+
const side = stringValue(content.insightSide) === "right" ? "right" : "left"
|
|
1044
|
+
const timelineHtml = `<div class="template-timeline template-timeline--${orientation}" data-template-slot="timeline" style="--timeline-count:${Math.max(1, milestones.length)}">${milestones.map((item) => timelineMilestone(item, orientation)).join("")}</div>`
|
|
1045
|
+
if (!sidePanel) return timelineHtml
|
|
1046
|
+
return `<div class="template-timeline-layout template-timeline-layout--${side}">${side === "left" ? `${sidePanel}${timelineHtml}` : `${timelineHtml}${sidePanel}`}</div>`
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function timelineMilestone(item: any, orientation: "horizontal" | "vertical"): string {
|
|
1050
|
+
const itemClass = item?.highlight ? "template-timeline-item template-timeline-item--highlight" : "template-timeline-item"
|
|
1051
|
+
const copyClass = orientation === "horizontal" ? "template-timeline-copy template-card" : "template-timeline-copy"
|
|
1052
|
+
if (orientation === "horizontal") {
|
|
1053
|
+
return `<article class="${itemClass}">
|
|
1054
|
+
<div class="${copyClass}">
|
|
1055
|
+
<i class="template-insight-icon" data-lucide="scan-search" aria-hidden="true"></i>
|
|
1056
|
+
<h3>${escapeHtml(stringValue(item.label))}</h3>
|
|
1057
|
+
<p>${escapeHtml(stringValue(item.description))}</p>
|
|
1058
|
+
</div>
|
|
1059
|
+
<span class="template-timeline-dot" aria-hidden="true"></span>
|
|
1060
|
+
<p class="template-timeline-date">${escapeHtml(stringValue(item.date))}</p>
|
|
1061
|
+
</article>`
|
|
1062
|
+
}
|
|
1063
|
+
return `<article class="${itemClass}">
|
|
1064
|
+
<span class="template-timeline-dot" aria-hidden="true"></span>
|
|
1065
|
+
<div class="${copyClass}">
|
|
1066
|
+
<p class="template-timeline-date">${escapeHtml(stringValue(item.date))}</p>
|
|
1067
|
+
<h3>${escapeHtml(stringValue(item.label))}</h3>
|
|
1068
|
+
<p>${escapeHtml(stringValue(item.description))}</p>
|
|
1069
|
+
</div>
|
|
1070
|
+
</article>`
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function renderSidePanel(content: Record<string, any>): string {
|
|
1074
|
+
return renderTextPanel(content)
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function renderTextPanel(content: Record<string, any>): string {
|
|
1078
|
+
const body = stringValue(content.insightBody)
|
|
1079
|
+
if (!body) return ""
|
|
1080
|
+
const title = stringValue(content.insightTitle) || "Insight"
|
|
1081
|
+
return `<div class="template-side-panel template-text-panel" data-template-slot="insight"><h2 class="template-side-panel-title template-text-panel-title">${escapeHtml(title)}</h2><p class="template-side-panel-body template-text-panel-body">${escapeHtml(body)}</p></div>`
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function imageCard(input: any): string {
|
|
1085
|
+
const image = safeImagePath(stringValue(input?.image))
|
|
1086
|
+
if (!image) return ""
|
|
1087
|
+
const alt = stringValue(input?.imageAlt) || ""
|
|
1088
|
+
const caption = stringValue(input?.imageCaption)
|
|
1089
|
+
return `<figure class="template-image-card"><div class="template-image-frame"><img src="${escapeAttribute(image)}" alt="${escapeAttribute(alt)}"></div>${caption ? `<figcaption class="template-image-caption">${escapeHtml(caption)}</figcaption>` : ""}</figure>`
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function visualPlaceholder(): string {
|
|
1093
|
+
return `<figure class="template-visual-placeholder"><div class="template-visual-placeholder-frame"><span class="template-visual-placeholder-label">image / chart slot (optional)</span></div></figure>`
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function visualSlotPanel(): string {
|
|
1097
|
+
return `<div class="template-chart-panel template-visual-slot-panel" data-template-slot="visual"><span class="template-visual-slot-label">image / chart slot (optional)</span></div>`
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function steps(input: any): string {
|
|
1101
|
+
const values = Array.isArray(input) ? input : []
|
|
1102
|
+
return values.slice(0, 5).map((item, index) => `<article class="template-card"><div class="template-step-number">${index + 1}</div><h2>${escapeHtml(stringValue(item.label) || `Step ${index + 1}`)}</h2><p>${escapeHtml(stringValue(item.description))}</p>${visualPlaceholder()}</article>`).join("")
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function orderedSteps(input: any): string {
|
|
1106
|
+
const values = Array.isArray(input) ? input : []
|
|
1107
|
+
return `<ol class="template-list">${values.slice(0, 5).map((item) => `<li><strong>${escapeHtml(stringValue(item.label))}</strong>${stringValue(item.description) ? ` ${escapeHtml(stringValue(item.description))}` : ""}${imageCard(item)}</li>`).join("")}</ol>`
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function items(content: Record<string, any>): Array<{ label: string; description: string; image?: string; imageAlt?: string; imageCaption?: string }> {
|
|
1111
|
+
const raw = Array.isArray(content.items) ? content.items : []
|
|
1112
|
+
return raw.map((item, index) => ({
|
|
1113
|
+
label: stringValue(typeof item === "string" ? item : item.label) || `Item ${index + 1}`,
|
|
1114
|
+
description: stringValue(typeof item === "string" ? "" : item.description || item.body || item.text),
|
|
1115
|
+
image: typeof item === "string" ? "" : stringValue(item.image),
|
|
1116
|
+
imageAlt: typeof item === "string" ? "" : stringValue(item.imageAlt),
|
|
1117
|
+
imageCaption: typeof item === "string" ? "" : stringValue(item.imageCaption),
|
|
1118
|
+
}))
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function scaffoldSeed(templateId: string, seed: Record<string, any>): Record<string, any> {
|
|
1122
|
+
const title = stringValue(seed.title) || getPageTemplate(templateId).title
|
|
1123
|
+
const base = { ...seed, title }
|
|
1124
|
+
if (templateId === "cover") return { eyebrow: "Deck", ...base }
|
|
1125
|
+
if (templateId === "section-divider") return { eyebrow: "Section", ...base }
|
|
1126
|
+
if (templateId === "closing") return { ...base }
|
|
1127
|
+
if (templateId === "agenda") return { items: defaultItems(["Situation", "Evidence", "Decision"]), ...base }
|
|
1128
|
+
if (templateId === "executive-summary") return { items: defaultItems(["Decision is ready", "Risk is bounded", "Next step is narrow"]), ...base }
|
|
1129
|
+
if (templateId === "problem-context") return { body: "Replace with context, tension, and why now.", items: defaultItems(["Context", "Implication"]), ...base }
|
|
1130
|
+
if (templateId === "key-message-evidence") return { body: "Replace with the key message the audience should remember.", items: defaultItems(["Evidence 1", "Evidence 2", "Evidence 3"]), ...base }
|
|
1131
|
+
if (templateId === "claim-supporting-visual") return { claim: "Replace with one visual claim.", body: "Use this copy to guide how the visual should be read.", items: defaultItems(["Anchor", "Callout"]), ...base }
|
|
1132
|
+
if (templateId === "metric-highlight") return { metrics: [{ value: "67%", label: "Metric", description: "Replace with interpretation." }, { value: "3x", label: "Comparison", description: "Replace with reading note." }, { value: "14d", label: "Window", description: "Replace with time context." }], insightTitle: "Read the signal", insightBody: "Replace with the decision implication, caveat, or next reading step.", ...base }
|
|
1133
|
+
if (templateId === "chart-takeaways") return { takeawaysTitle: "What to read", items: defaultItems(["Trend", "Driver", "Decision use"]), ...base }
|
|
1134
|
+
if (templateId === "table-comparison") return { columns: ["Dimension", "Current", "Target"], rows: [["Replace", "Current state", "Target state"], ["Caveat", "Known limit", "Next proof"]], insightTitle: "Insight", insightBody: "Replace with the table reading note or caveat.", ...base }
|
|
1135
|
+
if (templateId === "timeline-roadmap") return { orientation: "horizontal", milestones: [{ date: "2022", label: "Signal", description: "Name the starting condition." }, { date: "2023", label: "Proof", description: "Show the evidence threshold." }, { date: "2024", label: "Inflection", description: "Use the pivotal moment to frame the shift." }, { date: "2025", label: "Scale", description: "Use a taller card for the highlighted milestone.", highlight: true }, { date: "2026", label: "Decision", description: "State what changes next." }], ...base }
|
|
1136
|
+
if (templateId === "process-steps") return { steps: defaultItems(["Step 1", "Step 2", "Step 3"]), ...base }
|
|
1137
|
+
if (templateId === "recommendation-decision") return { recommendation: "Replace with the recommended decision.", items: defaultItems(["Rationale"]), steps: defaultItems(["Pilot", "Validate", "Ship"]), ...base }
|
|
1138
|
+
if (templateId === "risks-tradeoffs") return { items: defaultItems(["Risk", "Tradeoff", "Mitigation"]), ...base }
|
|
1139
|
+
return base
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function defaultItems(labels: string[]): Array<{ label: string; description: string }> {
|
|
1143
|
+
return labels.map((label) => ({ label, description: "Replace with slide-specific content." }))
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function validateRequiredFields(template: PageTemplateDefinition, content: Record<string, any>): string[] {
|
|
1147
|
+
const warnings: string[] = []
|
|
1148
|
+
for (const item of template.fields) {
|
|
1149
|
+
if (!item.required) continue
|
|
1150
|
+
const value = content[item.name]
|
|
1151
|
+
if (Array.isArray(value) ? value.length === 0 : !stringValue(value)) warnings.push(`Missing required template field '${item.name}'.`)
|
|
1152
|
+
}
|
|
1153
|
+
return warnings
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function positiveIndex(value: number): number {
|
|
1157
|
+
if (!Number.isInteger(value) || value < 1) throw new Error("slideIndex must be a positive 1-based integer.")
|
|
1158
|
+
return value
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function stringValue(value: any): string {
|
|
1162
|
+
if (value === undefined || value === null) return ""
|
|
1163
|
+
return String(value).trim()
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function slug(value: string): string {
|
|
1167
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function safeLucideIconName(value: string): string {
|
|
1171
|
+
const icon = value.trim().toLowerCase().replace(/[^a-z0-9-]/g, "")
|
|
1172
|
+
return icon || "lightbulb"
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function safeImagePath(value: string): string {
|
|
1176
|
+
const image = value.trim()
|
|
1177
|
+
if (!image) return ""
|
|
1178
|
+
if (/^(?:https?:|data:|javascript:|file:)/i.test(image)) return ""
|
|
1179
|
+
if (image.includes("\0")) return ""
|
|
1180
|
+
const parts = image.split(/[\\/]+/)
|
|
1181
|
+
const parentRefs = parts.filter((part) => part === "..").length
|
|
1182
|
+
if (parentRefs > 0 && !image.startsWith("../designs/") && !image.startsWith("..\\designs\\")) return ""
|
|
1183
|
+
if (parentRefs > 1) return ""
|
|
1184
|
+
return image
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function escapeHtml(value: string): string {
|
|
1188
|
+
return value
|
|
1189
|
+
.replace(/&/g, "&")
|
|
1190
|
+
.replace(/</g, "<")
|
|
1191
|
+
.replace(/>/g, ">")
|
|
1192
|
+
.replace(/"/g, """)
|
|
1193
|
+
.replace(/'/g, "'")
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function escapeAttribute(value: string): string {
|
|
1197
|
+
return escapeHtml(value.trim())
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function escapeRegExp(value: string): string {
|
|
1201
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
1202
|
+
}
|