@cyber-dash-tech/revela 0.19.1 → 0.19.3
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 +16 -16
- package/README.zh-CN.md +16 -16
- package/designs/lucent/DESIGN.md +4 -2
- package/designs/lucent/design.css +16 -6
- package/designs/lucent-dark/DESIGN.md +4 -2
- package/designs/lucent-dark/design.css +27 -12
- package/designs/monet/DESIGN.md +4 -2
- package/designs/monet/design.css +18 -8
- package/designs/starter/DESIGN.md +4 -2
- package/designs/starter/design.css +16 -6
- package/designs/summit/DESIGN.md +4 -2
- package/designs/summit/design.css +18 -8
- package/lib/page-templates/built-in-preview.html +44 -32
- package/lib/page-templates/render.ts +94 -33
- package/lib/page-templates/templates/index.ts +3 -0
- package/lib/page-templates/templates/milestone.ts +3 -0
- package/lib/page-templates/templates/table.ts +3 -0
- package/lib/page-templates/templates/timeline.ts +3 -0
- package/lib/page-templates/vocabulary.ts +12 -4
- package/package.json +1 -1
- package/plugins/revela/.codex-plugin/plugin.json +1 -1
- package/plugins/revela/mcp/revela-server.ts +6 -6
|
@@ -142,6 +142,13 @@ const templates: PageTemplateDefinition[] = [
|
|
|
142
142
|
field("takeawaysTitle", "string", "Title for the interpretation text panel."),
|
|
143
143
|
field("items", "items[]", "Takeaways.", true),
|
|
144
144
|
], ["Chart area must be explicit and bounded."], ["Chart panel and takeaways both exist."]),
|
|
145
|
+
define("table", "Table", "Explain a structured table with a left reading card and right table region.", [
|
|
146
|
+
field("title", "string", "Slide title.", true),
|
|
147
|
+
field("textTitle", "string", "Left text card title."),
|
|
148
|
+
field("textBody", "string", "Left text card body."),
|
|
149
|
+
field("columns", "string[]", "Column labels.", true),
|
|
150
|
+
field("rows", "rows[]", "Table rows.", true),
|
|
151
|
+
], ["Use the text card to tell the audience how to read the table.", "Keep table rows structured and scannable."], ["Left text card and right table region both exist.", "Table has headers and body rows."]),
|
|
145
152
|
define("table-comparison", "Table / Comparison", "Compare options, segments, or facts in a structured table.", [
|
|
146
153
|
field("title", "string", "Slide title.", true),
|
|
147
154
|
field("columns", "string[]", "Column labels.", true),
|
|
@@ -150,10 +157,13 @@ const templates: PageTemplateDefinition[] = [
|
|
|
150
157
|
field("insightBody", "string", "Interpretation, reading note, or caveat below the table."),
|
|
151
158
|
field("insightIcon", "string", "Lucide icon name for the insight title."),
|
|
152
159
|
], ["Keep rows scannable.", "Do not use a table for pure prose."], ["Table has headers and body rows."]),
|
|
153
|
-
define("
|
|
160
|
+
define("milestone", "Milestone", "Show dated phases or milestones on a horizontal roadmap axis.", [
|
|
161
|
+
field("title", "string", "Slide title.", true),
|
|
162
|
+
field("milestones", "milestones[]", "Horizontal milestones.", true),
|
|
163
|
+
], ["Use 3-6 milestones.", "Milestone cards sit above the axis and date labels sit 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."]),
|
|
164
|
+
define("timeline", "Timeline", "Show dated events, journey steps, or sequence as a vertical timeline.", [
|
|
154
165
|
field("title", "string", "Slide title.", true),
|
|
155
|
-
field("
|
|
156
|
-
field("milestones", "milestones[]", "Timeline milestones.", true),
|
|
166
|
+
field("milestones", "milestones[]", "Timeline events.", true),
|
|
157
167
|
field("insightTitle", "string", "Side panel title."),
|
|
158
168
|
field("insightBody", "string", "Timeline interpretation, so-what, or caveat."),
|
|
159
169
|
field("insightSide", "string", "left or right side panel placement."),
|
|
@@ -188,19 +198,21 @@ export function listPageTemplates(): { ok: true; templates: PageTemplateDefiniti
|
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
export function renderTemplateSlide(input: RenderTemplateSlideInput): RenderTemplateSlideResult {
|
|
191
|
-
const
|
|
201
|
+
const requestedTemplateId = normalizeTemplateId(input.templateId)
|
|
202
|
+
const template = getPageTemplate(requestedTemplateId)
|
|
192
203
|
const slideIndex = positiveIndex(input.slideIndex)
|
|
193
204
|
const content = input.content ?? {}
|
|
194
205
|
const designName = input.designName || "lucent"
|
|
195
206
|
const warnings = validateRequiredFields(template, content)
|
|
207
|
+
const outputTemplate = requestedTemplateId === "timeline-roadmap" ? { ...template, id: "timeline-roadmap", title: "Timeline / Roadmap" } : template
|
|
196
208
|
const html = renderSlideShell({
|
|
197
|
-
template,
|
|
209
|
+
template: outputTemplate,
|
|
198
210
|
slideIndex,
|
|
199
211
|
designName,
|
|
200
212
|
title: stringValue(content.title) || template.title,
|
|
201
|
-
body: renderBody(
|
|
213
|
+
body: renderBody(requestedTemplateId, content),
|
|
202
214
|
})
|
|
203
|
-
return { ok: true, templateId:
|
|
215
|
+
return { ok: true, templateId: outputTemplate.id, slideIndex, designName, html, warnings }
|
|
204
216
|
}
|
|
205
217
|
|
|
206
218
|
export function builtInPreviewFixtures(): BuiltInPreviewFixture[] {
|
|
@@ -272,6 +284,20 @@ export function builtInPreviewFixtures(): BuiltInPreviewFixture[] {
|
|
|
272
284
|
{ label: "Decision use", description: "Explain how the chart changes the recommendation, what threshold matters, and what follow-up evidence would reduce risk." },
|
|
273
285
|
],
|
|
274
286
|
}),
|
|
287
|
+
fixture("table", {
|
|
288
|
+
title: "table",
|
|
289
|
+
textTitle: "Financial readout",
|
|
290
|
+
textBody: "Read top-line growth first, then check margin, cash conversion, and retention to see whether the plan is financially durable.",
|
|
291
|
+
columns: ["Line item", "FY2025", "FY2026 Plan", "YoY / note"],
|
|
292
|
+
rows: [
|
|
293
|
+
["Revenue", "$84.2M", "$104.8M", "+24% planned growth"],
|
|
294
|
+
["Gross margin", "68.4%", "71.2%", "+280 bps mix shift"],
|
|
295
|
+
["Operating expense", "$42.7M", "$49.1M", "Scale hiring below revenue growth"],
|
|
296
|
+
["EBITDA", "$14.9M", "$23.6M", "+58% operating leverage"],
|
|
297
|
+
["Free cash flow", "$9.8M", "$16.4M", "Cash conversion improves to 69%"],
|
|
298
|
+
["Net retention", "116%", "121%", "Expansion supports plan quality"],
|
|
299
|
+
],
|
|
300
|
+
}),
|
|
275
301
|
fixture("table-comparison", {
|
|
276
302
|
title: "table-comparison",
|
|
277
303
|
columns: ["Layer", "Owns", "Agent task"],
|
|
@@ -284,9 +310,8 @@ export function builtInPreviewFixtures(): BuiltInPreviewFixture[] {
|
|
|
284
310
|
insightIcon: "lightbulb",
|
|
285
311
|
insightBody: "The template layer owns structure, while the design layer owns visual treatment. This keeps agent edits bounded without freezing the final look.",
|
|
286
312
|
}),
|
|
287
|
-
fixture("
|
|
288
|
-
title: "
|
|
289
|
-
orientation: "horizontal",
|
|
313
|
+
fixture("milestone", {
|
|
314
|
+
title: "milestone",
|
|
290
315
|
milestones: [
|
|
291
316
|
{ date: "2022", label: "Signal", description: "Map the baseline." },
|
|
292
317
|
{ date: "2023", label: "Proof", description: "Validate the evidence threshold." },
|
|
@@ -295,9 +320,8 @@ export function builtInPreviewFixtures(): BuiltInPreviewFixture[] {
|
|
|
295
320
|
{ date: "2026", label: "Decision", description: "Commit to the next path." },
|
|
296
321
|
],
|
|
297
322
|
}),
|
|
298
|
-
fixture("timeline
|
|
299
|
-
title: "timeline
|
|
300
|
-
orientation: "vertical",
|
|
323
|
+
fixture("timeline", {
|
|
324
|
+
title: "timeline",
|
|
301
325
|
insightTitle: "Reading the journey",
|
|
302
326
|
insightBody: "The timeline should show sequence and decision rhythm, while the side panel explains why the milestones matter.",
|
|
303
327
|
milestones: [
|
|
@@ -543,19 +567,27 @@ ${lucentClosingBackgroundCss}
|
|
|
543
567
|
.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
568
|
.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
569
|
.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;
|
|
547
|
-
.template-
|
|
570
|
+
.template-text-panel.template-chart-takeaway-panel { gap: 28px; }
|
|
571
|
+
.template-text-panel--plain { background: rgba(255,255,255,0.74); border: 1px solid transparent; box-shadow: none; }
|
|
572
|
+
.template-text-panel--clear { background: transparent; border: 0; border-radius: 0; box-shadow: none; padding-left: 0; padding-right: 0; }
|
|
573
|
+
.template-text-panel--color { 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); }
|
|
574
|
+
.template-text-panel--color .template-text-panel-title { color: white; }
|
|
575
|
+
.template-text-panel--color .template-text-panel-body { color: rgba(255,255,255,0.78); }
|
|
548
576
|
.template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
|
|
549
577
|
.template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
|
|
550
578
|
.template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
|
|
551
579
|
.template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: white; }
|
|
552
580
|
.template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: rgba(255,255,255,0.78); }
|
|
553
581
|
.template-bar { flex: 1; background: linear-gradient(180deg, var(--accent-primary), var(--accent-cyan)); min-height: 80px; }
|
|
582
|
+
.template-table-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; }
|
|
583
|
+
.template-table-layout .template-side-panel { grid-column: 1; grid-row: 1; }
|
|
584
|
+
.template-table-region { grid-column: 2; grid-row: 1; min-width: 0; min-height: 0; height: 100%; }
|
|
585
|
+
.template-table-region .template-table-wrap { height: 100%; }
|
|
554
586
|
.template-table-wrap { display: grid; grid-template-rows: minmax(0, auto) auto; gap: 22px; height: 100%; align-content: start; }
|
|
555
587
|
.template-table { width: 100%; border-collapse: collapse; background: rgba(255,255,255,0.86); box-shadow: 0 18px 44px var(--shadow-soft); }
|
|
556
588
|
.template-table th, .template-table td { padding: 22px 24px; border-bottom: 1px solid var(--line); text-align: left; font-size: 21px; }
|
|
557
589
|
.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;
|
|
590
|
+
.template-text-panel { min-height: 0; display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 20px; border-radius: var(--surface-radius); padding: 42px; }
|
|
559
591
|
.template-text-panel-title { margin: 0; font-size: 34px; line-height: 1.28; color: var(--text-primary); padding-bottom: 4px; overflow: visible; }
|
|
560
592
|
.template-text-panel-body { margin: 0; font-size: 23px; line-height: 1.52; color: var(--text-secondary); }
|
|
561
593
|
.template-side-panel { align-self: stretch; }
|
|
@@ -577,9 +609,7 @@ ${lucentClosingBackgroundCss}
|
|
|
577
609
|
.template-timeline-layout--right { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
|
|
578
610
|
.template-timeline-layout--right .template-timeline { grid-column: 1; grid-row: 1; }
|
|
579
611
|
.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); }
|
|
612
|
+
.template-timeline-layout .template-text-panel--color { background: linear-gradient(135deg, #7a7fe8 0%, #5f82c8 58%, #315eea 115%); color: white; box-shadow: 0 22px 56px rgba(49,94,234,0.22); }
|
|
583
613
|
.template-timeline--horizontal { grid-template-columns: repeat(var(--timeline-count), 1fr); column-gap: 18px; align-items: stretch; --timeline-axis-y: 86%; }
|
|
584
614
|
.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
615
|
.template-timeline-item { position: relative; min-height: 400px; display: grid; justify-items: center; align-items: center; }
|
|
@@ -653,6 +683,10 @@ ${lucentClosingBackgroundCss}
|
|
|
653
683
|
.template-frame--catalog .template-chart-takeaway-item { gap: 4px; padding-top: 11px; }
|
|
654
684
|
.template-frame--catalog .template-chart-takeaway-item h3 { font-size: 19px; line-height: 1.2; }
|
|
655
685
|
.template-frame--catalog .template-chart-takeaway-item p { font-size: 15px; line-height: 1.3; }
|
|
686
|
+
.template-frame--catalog .template-table-layout { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 22px; }
|
|
687
|
+
.template-frame--catalog .template-table-layout .template-text-panel { padding: 22px; gap: 10px; }
|
|
688
|
+
.template-frame--catalog .template-table-layout .template-text-panel-title { font-size: 25px; line-height: 1.3; }
|
|
689
|
+
.template-frame--catalog .template-table-layout .template-text-panel-body { font-size: 18px; line-height: 1.4; }
|
|
656
690
|
.template-frame--catalog .template-table-wrap { gap: 16px; }
|
|
657
691
|
.template-frame--catalog .template-table th,
|
|
658
692
|
.template-frame--catalog .template-table td { padding: 14px 18px; font-size: 17px; line-height: 1.32; }
|
|
@@ -785,7 +819,7 @@ export function validatePageTemplateContracts(filePath: string): PageTemplateCon
|
|
|
785
819
|
const body = section[2]
|
|
786
820
|
const slideIndex = Number(/data-slide-index=["'](\d+)["']/i.exec(section[0])?.[1])
|
|
787
821
|
issues.push(...validateVocabularyContract(templateId, body, Number.isInteger(slideIndex) ? slideIndex : undefined))
|
|
788
|
-
if (templateId
|
|
822
|
+
if (isTimelineTemplateId(templateId)) issues.push(...validateTimelineContract(templateId, body, Number.isInteger(slideIndex) ? slideIndex : undefined))
|
|
789
823
|
}
|
|
790
824
|
return { ok: !issues.some((issue) => issue.severity === "error"), issues }
|
|
791
825
|
}
|
|
@@ -815,7 +849,7 @@ export function validateBoundedTemplateEdit(input: BoundedTemplateEditInput): Pa
|
|
|
815
849
|
} else {
|
|
816
850
|
const templateId = /data-template=["']([^"']+)["']/i.exec(target)?.[1] || "unknown"
|
|
817
851
|
issues.push(...validateVocabularyContract(templateId, target, slideIndex))
|
|
818
|
-
if (templateId
|
|
852
|
+
if (isTimelineTemplateId(templateId)) issues.push(...validateTimelineContract(templateId, target, slideIndex))
|
|
819
853
|
}
|
|
820
854
|
return { ok: !issues.some((issue) => issue.severity === "error"), issues }
|
|
821
855
|
}
|
|
@@ -824,23 +858,23 @@ function define(id: string, title: string, purpose: string, fields: PageTemplate
|
|
|
824
858
|
return { id, title, purpose, status: "renderable", fields, contentRules, qaRules }
|
|
825
859
|
}
|
|
826
860
|
|
|
827
|
-
function validateTimelineContract(html: string, slideIndex?: number): PageTemplateContractIssue[] {
|
|
861
|
+
function validateTimelineContract(templateId: string, html: string, slideIndex?: number): PageTemplateContractIssue[] {
|
|
828
862
|
const issues: PageTemplateContractIssue[] = []
|
|
829
863
|
const root = /class=["'][^"']*\btemplate-timeline\b[^"']*["']/i.test(html)
|
|
830
864
|
if (!root) {
|
|
831
|
-
issues.push({ severity: "error", templateId
|
|
865
|
+
issues.push({ severity: "error", templateId, slideIndex, message: "Missing .template-timeline root." })
|
|
832
866
|
return issues
|
|
833
867
|
}
|
|
834
868
|
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
|
|
869
|
+
if (itemMatches.length < 3) issues.push({ severity: "warning", templateId, slideIndex, message: "Timeline should usually contain at least three milestones." })
|
|
836
870
|
for (let index = 0; index < itemMatches.length; index++) {
|
|
837
871
|
const item = itemMatches[index][1]
|
|
838
|
-
if (!/class=["'][^"']*\btemplate-timeline-dot\b[^"']*["']/i.test(item)) issues.push({ severity: "error", templateId
|
|
839
|
-
if (!/class=["'][^"']*\btemplate-timeline-copy\b[^"']*["']/i.test(item)) issues.push({ severity: "error", templateId
|
|
872
|
+
if (!/class=["'][^"']*\btemplate-timeline-dot\b[^"']*["']/i.test(item)) issues.push({ severity: "error", templateId, slideIndex, message: `Milestone ${index + 1} is missing .template-timeline-dot inside its item.` })
|
|
873
|
+
if (!/class=["'][^"']*\btemplate-timeline-copy\b[^"']*["']/i.test(item)) issues.push({ severity: "error", templateId, slideIndex, message: `Milestone ${index + 1} is missing .template-timeline-copy inside its item.` })
|
|
840
874
|
}
|
|
841
875
|
const dotCount = (html.match(/\btemplate-timeline-dot\b/g) ?? []).length
|
|
842
876
|
const copyCount = (html.match(/\btemplate-timeline-copy\b/g) ?? []).length
|
|
843
|
-
if (dotCount !== copyCount) issues.push({ severity: "error", templateId
|
|
877
|
+
if (dotCount !== copyCount) issues.push({ severity: "error", templateId, slideIndex, message: `Timeline dot count (${dotCount}) must match copy count (${copyCount}).` })
|
|
844
878
|
return issues
|
|
845
879
|
}
|
|
846
880
|
|
|
@@ -889,12 +923,26 @@ function field(name: string, type: PageTemplateField["type"], description: strin
|
|
|
889
923
|
}
|
|
890
924
|
|
|
891
925
|
function getPageTemplate(templateId: string): PageTemplateDefinition {
|
|
892
|
-
const id =
|
|
926
|
+
const id = canonicalTemplateId(templateId)
|
|
893
927
|
const template = templates.find((item) => item.id === id)
|
|
894
928
|
if (!template) throw new Error(`Unknown page template: ${templateId}`)
|
|
895
929
|
return template
|
|
896
930
|
}
|
|
897
931
|
|
|
932
|
+
function normalizeTemplateId(templateId: string): string {
|
|
933
|
+
return String(templateId || "").trim()
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function canonicalTemplateId(templateId: string): string {
|
|
937
|
+
const id = normalizeTemplateId(templateId)
|
|
938
|
+
if (id === "timeline-roadmap") return "milestone"
|
|
939
|
+
return id
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function isTimelineTemplateId(templateId: string): boolean {
|
|
943
|
+
return ["milestone", "timeline", "timeline-roadmap"].includes(templateId)
|
|
944
|
+
}
|
|
945
|
+
|
|
898
946
|
function renderSlideShell(input: { template: PageTemplateDefinition; slideIndex: number; designName: string; title: string; body: string; catalog?: any }): string {
|
|
899
947
|
const hero = ["cover", "section-divider", "closing"].includes(input.template.id)
|
|
900
948
|
const slideQa = hero ? "false" : "true"
|
|
@@ -945,7 +993,10 @@ function renderBody(templateId: string, content: Record<string, any>): string {
|
|
|
945
993
|
if (templateId === "claim-supporting-visual") return `${renderHeader(content, "Claim + Supporting Visual")}<div class="template-body template-grid cols-2">${claimTextPanel(content)}${visualSlotPanel()}</div>`
|
|
946
994
|
if (templateId === "metric-highlight") return `${renderHeader(content, "Metric Highlight")}<div class="template-body">${metricHighlight(content)}</div>`
|
|
947
995
|
if (templateId === "chart-takeaways") return `${renderHeader(content, "Chart + Takeaways")}<div class="template-body template-grid template-chart-layout">${visualSlotPanel()}${chartTakeawayPanel(content)}</div>`
|
|
996
|
+
if (templateId === "table") return `${renderHeader(content, "Table")}<div class="template-body">${tablePage(content)}</div>`
|
|
948
997
|
if (templateId === "table-comparison") return `${renderHeader(content, "Table / Comparison")}<div class="template-body" data-template-slot="table">${table(content)}</div>`
|
|
998
|
+
if (templateId === "milestone") return `${renderHeader(content, "Milestone")}<div class="template-body">${timeline({ ...content, orientation: "horizontal" })}</div>`
|
|
999
|
+
if (templateId === "timeline") return `${renderHeader(content, "Timeline")}<div class="template-body">${timeline({ ...content, orientation: "vertical" })}</div>`
|
|
949
1000
|
if (templateId === "timeline-roadmap") return `${renderHeader(content, "Timeline / Roadmap")}<div class="template-body">${timeline(content)}</div>`
|
|
950
1001
|
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
1002
|
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>`
|
|
@@ -991,7 +1042,7 @@ function evidenceCards(items: Array<{ label: string; description: string; image?
|
|
|
991
1042
|
function chartTakeawayPanel(content: Record<string, any>): string {
|
|
992
1043
|
const takeawayItems = items(content)
|
|
993
1044
|
const title = stringValue(content.takeawaysTitle) || "What to read"
|
|
994
|
-
return `<div class="template-text-panel template-chart-takeaway-panel" data-template-slot="takeaways">
|
|
1045
|
+
return `<div class="template-text-panel template-text-panel--color template-chart-takeaway-panel" data-template-slot="takeaways">
|
|
995
1046
|
<h2 class="template-text-panel-title">${escapeHtml(title)}</h2>
|
|
996
1047
|
<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
1048
|
</div>`
|
|
@@ -1025,6 +1076,14 @@ function table(content: Record<string, any>): string {
|
|
|
1025
1076
|
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
1077
|
}
|
|
1027
1078
|
|
|
1079
|
+
function tablePage(content: Record<string, any>): string {
|
|
1080
|
+
const panelContent = {
|
|
1081
|
+
insightTitle: stringValue(content.textTitle) || "What to read",
|
|
1082
|
+
insightBody: stringValue(content.textBody) || "Use this card to explain the comparison, caveat, or decision implication before the audience scans the table.",
|
|
1083
|
+
}
|
|
1084
|
+
return `<div class="template-table-layout">${renderTextPanel(panelContent, "text-card", "clear")}<div class="template-table-region" data-template-slot="table">${table({ ...content, insightTitle: "", insightBody: "" })}</div></div>`
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1028
1087
|
function renderInsightPanel(content: Record<string, any>): string {
|
|
1029
1088
|
const body = stringValue(content.insightBody)
|
|
1030
1089
|
if (!body) return ""
|
|
@@ -1071,14 +1130,14 @@ function timelineMilestone(item: any, orientation: "horizontal" | "vertical"): s
|
|
|
1071
1130
|
}
|
|
1072
1131
|
|
|
1073
1132
|
function renderSidePanel(content: Record<string, any>): string {
|
|
1074
|
-
return renderTextPanel(content)
|
|
1133
|
+
return renderTextPanel(content, "insight", "color")
|
|
1075
1134
|
}
|
|
1076
1135
|
|
|
1077
|
-
function renderTextPanel(content: Record<string, any
|
|
1136
|
+
function renderTextPanel(content: Record<string, any>, slot = "insight", variant: "plain" | "clear" | "color" = "plain"): string {
|
|
1078
1137
|
const body = stringValue(content.insightBody)
|
|
1079
1138
|
if (!body) return ""
|
|
1080
1139
|
const title = stringValue(content.insightTitle) || "Insight"
|
|
1081
|
-
return `<div class="template-side-panel template-text-panel" data-template-slot="
|
|
1140
|
+
return `<div class="template-side-panel template-text-panel template-text-panel--${variant}" data-template-slot="${escapeAttribute(slot)}"><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
1141
|
}
|
|
1083
1142
|
|
|
1084
1143
|
function imageCard(input: any): string {
|
|
@@ -1131,8 +1190,10 @@ function scaffoldSeed(templateId: string, seed: Record<string, any>): Record<str
|
|
|
1131
1190
|
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
1191
|
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
1192
|
if (templateId === "chart-takeaways") return { takeawaysTitle: "What to read", items: defaultItems(["Trend", "Driver", "Decision use"]), ...base }
|
|
1193
|
+
if (templateId === "table") return { textTitle: "Financial readout", textBody: "Replace with the table reading note, caveat, or decision implication.", columns: ["Line item", "FY2025", "FY2026 Plan", "YoY / note"], rows: [["Revenue", "$84.2M", "$104.8M", "+24% planned growth"], ["Gross margin", "68.4%", "71.2%", "+280 bps mix shift"], ["Operating expense", "$42.7M", "$49.1M", "Scale hiring below revenue growth"], ["EBITDA", "$14.9M", "$23.6M", "+58% operating leverage"], ["Free cash flow", "$9.8M", "$16.4M", "Cash conversion improves"], ["Net retention", "116%", "121%", "Expansion supports plan quality"]], ...base }
|
|
1134
1194
|
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 }
|
|
1195
|
+
if (templateId === "milestone" || 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 }
|
|
1196
|
+
if (templateId === "timeline") return { orientation: "vertical", insightTitle: "Reading the journey", insightBody: "Replace with the timeline interpretation or caveat.", milestones: [{ date: "Mar 2019", label: "Launch", description: "Name the starting event." }, { date: "Nov 2019", label: "Audit", description: "Show the evidence threshold." }, { date: "May 2020", label: "Scale", description: "Explain the operating cadence." }, { date: "Feb 2021", label: "Review", description: "State what changes next." }], ...base }
|
|
1136
1197
|
if (templateId === "process-steps") return { steps: defaultItems(["Step 1", "Step 2", "Step 3"]), ...base }
|
|
1137
1198
|
if (templateId === "recommendation-decision") return { recommendation: "Replace with the recommended decision.", items: defaultItems(["Rationale"]), steps: defaultItems(["Pilot", "Validate", "Ship"]), ...base }
|
|
1138
1199
|
if (templateId === "risks-tradeoffs") return { items: defaultItems(["Risk", "Tradeoff", "Mitigation"]), ...base }
|
|
@@ -10,10 +10,13 @@ export * from "./cover"
|
|
|
10
10
|
export * from "./executive-summary"
|
|
11
11
|
export * from "./key-message-evidence"
|
|
12
12
|
export * from "./metric-highlight"
|
|
13
|
+
export * from "./milestone"
|
|
13
14
|
export * from "./problem-context"
|
|
14
15
|
export * from "./process-steps"
|
|
15
16
|
export * from "./recommendation-decision"
|
|
16
17
|
export * from "./risks-tradeoffs"
|
|
17
18
|
export * from "./section-divider"
|
|
19
|
+
export * from "./table"
|
|
18
20
|
export * from "./table-comparison"
|
|
21
|
+
export * from "./timeline"
|
|
19
22
|
export * from "./timeline-roadmap"
|
|
@@ -55,9 +55,11 @@ export const PAGE_TEMPLATE_VOCABULARY: PageTemplateVocabulary[] = [
|
|
|
55
55
|
vocab("key-message-evidence", ["template-key-message-panel", "template-evidence-grid"], ["key-message", "evidence"], ["key-message", "evidence"], ["Key message and evidence regions must remain distinct."]),
|
|
56
56
|
vocab("claim-supporting-visual", ["template-claim-text-panel", "template-visual-slot-panel"], ["claim", "visual"], ["claim", "visual"], ["Visual slot may be replaced by image, chart, table, or diagram container."]),
|
|
57
57
|
vocab("metric-highlight", ["template-stat-grid"], ["metrics"], ["metrics", "insight"], ["Metric values should remain visible outside prose."]),
|
|
58
|
-
vocab("chart-takeaways", ["template-chart-panel", "template-chart-takeaway-panel"], ["visual", "takeaways"], ["visual", "takeaways"], ["Chart/image slot and takeaway text panel must both remain present."]),
|
|
58
|
+
vocab("chart-takeaways", ["template-chart-panel", "template-chart-takeaway-panel", "template-text-panel--color"], ["visual", "takeaways"], ["visual", "takeaways"], ["Chart/image slot and color takeaway text panel must both remain present."]),
|
|
59
|
+
vocab("table", ["template-table-layout", "template-table-wrap", "template-table", "template-side-panel", "template-text-panel", "template-text-panel--clear"], ["text-card", "table"], ["text-card", "table"], ["Left clear text card explains how to read the structured table.", "Table headers and body should remain structured, not prose-only."]),
|
|
59
60
|
vocab("table-comparison", ["template-table-wrap", "template-table"], ["table"], ["table", "insight"], ["Table headers and body should remain structured, not prose-only."]),
|
|
60
|
-
vocab("
|
|
61
|
+
vocab("milestone", ["template-timeline", "template-timeline-item", "template-timeline-dot", "template-timeline-copy", "template-insight-icon"], ["timeline"], ["timeline"], ["Each milestone item must keep dot and copy as sibling anchors inside one item.", "Milestone cards reuse .template-card; highlight uses the item modifier."]),
|
|
62
|
+
vocab("timeline", ["template-timeline", "template-timeline-item", "template-timeline-dot", "template-timeline-copy"], ["timeline"], ["timeline", "insight"], ["Each timeline item must keep dot and copy as sibling anchors inside one item.", "The optional color insight slot explains the sequence without replacing event copy."]),
|
|
61
63
|
vocab("process-steps", ["template-steps", "template-step-number"], ["steps"], ["steps"], ["Steps should remain ordered in DOM order."]),
|
|
62
64
|
vocab("recommendation-decision", ["template-card"], ["recommendation", "rationale", "next-steps"], ["recommendation", "rationale", "next-steps"], ["Keep recommendation, rationale, and next steps separate."]),
|
|
63
65
|
vocab("risks-tradeoffs", ["template-card"], ["risks"], ["risks"], ["Risk/tradeoff cards should name uncertainty explicitly."]),
|
|
@@ -90,6 +92,8 @@ const additionalClasses = [
|
|
|
90
92
|
"template-chart-takeaway-list",
|
|
91
93
|
"template-chart-takeaway-item",
|
|
92
94
|
"template-bar",
|
|
95
|
+
"template-table-layout",
|
|
96
|
+
"template-table-region",
|
|
93
97
|
"template-table",
|
|
94
98
|
"template-table-wrap",
|
|
95
99
|
"template-side-panel",
|
|
@@ -98,6 +102,9 @@ const additionalClasses = [
|
|
|
98
102
|
"template-side-panel--left",
|
|
99
103
|
"template-side-panel--right",
|
|
100
104
|
"template-text-panel",
|
|
105
|
+
"template-text-panel--plain",
|
|
106
|
+
"template-text-panel--clear",
|
|
107
|
+
"template-text-panel--color",
|
|
101
108
|
"template-text-panel-title",
|
|
102
109
|
"template-text-panel-body",
|
|
103
110
|
"template-insight-panel",
|
|
@@ -132,9 +139,10 @@ export function listPageTemplateVocabulary(): PageTemplateVocabulary[] {
|
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
export function getPageTemplateVocabulary(templateId: string): PageTemplateVocabulary {
|
|
135
|
-
const
|
|
142
|
+
const id = templateId === "timeline-roadmap" ? "milestone" : templateId
|
|
143
|
+
const vocabulary = PAGE_TEMPLATE_VOCABULARY.find((item) => item.templateId === id)
|
|
136
144
|
if (!vocabulary) throw new Error(`Unknown page template vocabulary: ${templateId}`)
|
|
137
|
-
return vocabulary
|
|
145
|
+
return templateId === "timeline-roadmap" ? { ...vocabulary, templateId } : vocabulary
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
function vocab(templateId: string, requiredClasses: string[], slotNames: string[], replaceableSlots: string[], contractNotes: string[]): PageTemplateVocabulary {
|
package/package.json
CHANGED
|
@@ -113,7 +113,7 @@ const tools = [
|
|
|
113
113
|
inputSchema: objectSchema({
|
|
114
114
|
workspaceRoot: stringProp("Optional workspace root."),
|
|
115
115
|
designName: stringProp("Optional design name. Defaults to the active design."),
|
|
116
|
-
templateId: requiredStringProp("Built-in template id, such as timeline
|
|
116
|
+
templateId: requiredStringProp("Built-in template id, such as milestone or timeline."),
|
|
117
117
|
slideIndex: requiredNumberProp("Positive 1-based slide index."),
|
|
118
118
|
content: objectProp("Template content fields. The built-in template renderer owns the HTML skeleton."),
|
|
119
119
|
}, ["templateId", "slideIndex", "content"]),
|
|
@@ -122,14 +122,14 @@ const tools = [
|
|
|
122
122
|
name: "revela_page_template_foundation",
|
|
123
123
|
description: "Read the built-in template foundation for custom design authoring: scaffold HTML, CSS hooks, slots, and contract notes.",
|
|
124
124
|
inputSchema: objectSchema({
|
|
125
|
-
templateId: requiredStringProp("Built-in template id, such as timeline
|
|
125
|
+
templateId: requiredStringProp("Built-in template id, such as milestone or timeline."),
|
|
126
126
|
}, ["templateId"]),
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
name: "revela_page_template_vocabulary",
|
|
130
130
|
description: "Read machine-readable classes, slots, editable regions, replaceable regions, and contract notes for one page template.",
|
|
131
131
|
inputSchema: objectSchema({
|
|
132
|
-
templateId: requiredStringProp("Built-in template id, such as timeline
|
|
132
|
+
templateId: requiredStringProp("Built-in template id, such as milestone or timeline."),
|
|
133
133
|
}, ["templateId"]),
|
|
134
134
|
},
|
|
135
135
|
{
|
|
@@ -138,7 +138,7 @@ const tools = [
|
|
|
138
138
|
inputSchema: objectSchema({
|
|
139
139
|
workspaceRoot: stringProp("Optional workspace root."),
|
|
140
140
|
designName: stringProp("Optional design name. Defaults to the active design."),
|
|
141
|
-
templateId: requiredStringProp("Built-in template id, such as timeline
|
|
141
|
+
templateId: requiredStringProp("Built-in template id, such as milestone or timeline."),
|
|
142
142
|
slideIndex: requiredNumberProp("Positive 1-based slide index."),
|
|
143
143
|
seed: objectProp("Optional scaffold seed fields. This is not the final authoring interface."),
|
|
144
144
|
}, ["templateId", "slideIndex"]),
|
|
@@ -150,7 +150,7 @@ const tools = [
|
|
|
150
150
|
workspaceRoot: stringProp("Optional workspace root."),
|
|
151
151
|
outputPath: requiredStringProp("Workspace-relative HTML deck path."),
|
|
152
152
|
designName: stringProp("Optional design name. Defaults to the active design."),
|
|
153
|
-
templateId: requiredStringProp("Built-in template id, such as timeline
|
|
153
|
+
templateId: requiredStringProp("Built-in template id, such as milestone or timeline."),
|
|
154
154
|
slideIndex: requiredNumberProp("Positive 1-based slide index."),
|
|
155
155
|
seed: objectProp("Optional scaffold seed fields. LLM should bounded-edit the inserted slide after scaffold creation."),
|
|
156
156
|
}, ["outputPath", "templateId", "slideIndex"]),
|
|
@@ -162,7 +162,7 @@ const tools = [
|
|
|
162
162
|
workspaceRoot: stringProp("Optional workspace root."),
|
|
163
163
|
outputPath: requiredStringProp("Workspace-relative HTML deck path."),
|
|
164
164
|
designName: stringProp("Optional design name. Defaults to the active design."),
|
|
165
|
-
templateId: requiredStringProp("Built-in template id, such as timeline
|
|
165
|
+
templateId: requiredStringProp("Built-in template id, such as milestone or timeline."),
|
|
166
166
|
slideIndex: requiredNumberProp("Positive 1-based slide index."),
|
|
167
167
|
content: objectProp("Template content fields. Prefer scaffold-first flow for new deck creation."),
|
|
168
168
|
}, ["outputPath", "templateId", "slideIndex", "content"]),
|