@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.
@@ -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("timeline-roadmap", "Timeline / Roadmap", "Show dated phases, milestones, or journey steps.", [
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("orientation", "string", "horizontal or vertical."),
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 template = getPageTemplate(input.templateId)
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(template.id, content),
213
+ body: renderBody(requestedTemplateId, content),
202
214
  })
203
- return { ok: true, templateId: template.id, slideIndex, designName, html, warnings }
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("timeline-roadmap", {
288
- title: "timeline-roadmap",
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-roadmap", {
299
- title: "timeline-roadmap-vertical",
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; 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; }
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; background: rgba(255,255,255,0.74); border-radius: var(--surface-radius); padding: 42px; }
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 === "timeline-roadmap") issues.push(...validateTimelineContract(body, Number.isInteger(slideIndex) ? slideIndex : undefined))
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 === "timeline-roadmap") issues.push(...validateTimelineContract(target, slideIndex))
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: "timeline-roadmap", slideIndex, message: "Missing .template-timeline root." })
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: "timeline-roadmap", slideIndex, message: "Timeline should usually contain at least three milestones." })
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: "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.` })
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: "timeline-roadmap", slideIndex, message: `Timeline dot count (${dotCount}) must match copy count (${copyCount}).` })
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 = String(templateId || "").trim()
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>): string {
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="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>`
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"
@@ -0,0 +1,3 @@
1
+ import { templateModule } from "./shared"
2
+
3
+ export const milestoneTemplate = templateModule("milestone")
@@ -0,0 +1,3 @@
1
+ import { templateModule } from "./shared"
2
+
3
+ export const tableTemplate = templateModule("table")
@@ -0,0 +1,3 @@
1
+ import { templateModule } from "./shared"
2
+
3
+ export const timelineTemplate = templateModule("timeline")
@@ -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("timeline-roadmap", ["template-timeline", "template-timeline-item", "template-timeline-dot", "template-timeline-copy", "template-insight-icon"], ["timeline"], ["timeline", "insight"], ["Each timeline item must keep dot and copy as sibling anchors inside one item.", "Horizontal timeline cards reuse .template-card; highlight uses the item modifier."]),
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 vocabulary = PAGE_TEMPLATE_VOCABULARY.find((item) => item.templateId === templateId)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
4
4
  "description": "Codex-first CLI/MCP workspace for trusted narrative artifacts from local sources, research, and evidence",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "revela",
3
- "version": "0.19.0",
3
+ "version": "0.19.3",
4
4
  "description": "Use Revela in Codex to specify, research, plan, make, and export trusted narrative decision artifacts.",
5
5
  "author": {
6
6
  "name": "cyber-dash-tech",
@@ -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-roadmap."),
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-roadmap."),
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-roadmap."),
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-roadmap."),
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-roadmap."),
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-roadmap."),
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"]),