@cyber-dash-tech/revela 0.19.6 → 0.19.8

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.
@@ -66,7 +66,7 @@ Accent usage guidance:
66
66
  - `--accent-earth` — warm secondary accent, image captions, secondary labels
67
67
  - `--accent-olive` — muted structural accent, chart fills, subtle dividers
68
68
  - `--accent-stone` — lightest accent, disabled states, faint decorative lines
69
- - `--accent-sage` — desaturated cool green; use for environmental, sustainability, or positive-signal content (e.g. quote decorations, positive indicators, nature-themed slides)
69
+ - `--accent-sage` — desaturated cool green; use for environmental, sustainability, or positive-signal content (e.g. positive indicators, nature-themed slides)
70
70
  - `--accent-danger` — negative indicators, alerts, down-trend markers only
71
71
 
72
72
  ### Typography
@@ -470,7 +470,7 @@ These rules are mandatory for Summit.
470
470
  - **Visual hierarchy is strict:** eyebrow -> heading -> body -> caption.
471
471
  - **Content pages need a stable title block.** Except cover, TOC, closing, section divider, and full-bleed hero slides, every normal content slide should include a visible title block from the upper-left safe area. It should contain a compact chapter/section label plus a slide title written as the page's claim or takeaway.
472
472
  - **Do not hide the page title inside a card.** Body components may have their own headings, but the slide-level title block should remain separate and easy to scan unless the chosen layout explicitly defines a compact side-title variant.
473
- - **Text panels are not decorative rule panels.** Do not add a default left border, vertical accent bar, yellow/gold line, or inline rule to `text-panel`. Use typography, spacing, boxes, stats, quotes, or layout-level dividers for emphasis.
473
+ - **Text panels are not decorative rule panels.** Do not add a default left border, vertical accent bar, yellow/gold line, or inline rule to `text-panel`. Use typography, spacing, boxes, stats, italic quote text inside `text-panel`, or layout-level dividers for emphasis.
474
474
  - **Titles are Title Case.** Do not set `text-transform:uppercase` on `h1`, `h2`, `h3`, or `h4` titles. Uppercase is reserved for eyebrows, captions, metadata labels, short codes, and date/code-like markers.
475
475
  - **Components are transparent by default.** Component primitives should not bring their own paper/background fill. Let `.page`, layout containers, or explicit modifier variants provide background color when needed.
476
476
  - **Icon system is Lucide.** For ordinary UI, semantic, status, category, process, and navigation icons, use Lucide (`data-lucide`). Do not hand-write inline SVG for icons. SVG is allowed only for intentional decorative motifs, illustrations, or design-specific artwork. If any `data-lucide` icon is present, load Lucide via CDN and call `lucide.createIcons()` after `SlidePresentation`.
@@ -859,7 +859,7 @@ Structural intent:
859
859
 
860
860
  Use these components when a page needs repeatable editorial modules inside a larger layout. Components define the block itself, not the page grid around it.
861
861
 
862
- Use this hierarchy: `layout -> box/card -> text-panel + media/chart/table/stat/quote`.
862
+ Use this hierarchy: `layout -> box/card -> text-panel + media/chart/table/stat`.
863
863
 
864
864
  Component defaults are transparent. Use explicit variants such as `box--paper`, `box--dark`, `text-panel--light`, or `text-panel--dark` only when a component intentionally needs its own reading field. Do not add default fills to component primitives.
865
865
 
@@ -867,14 +867,14 @@ Source and citation text should use `.source` or `.source-note`, not `.caption`.
867
867
 
868
868
  LLM-facing vocabulary:
869
869
  - `box` — card/group primitive for one idea, case, evidence item, metric, objection, risk, or action.
870
- - `text-panel` — language module for title, body text, bullets, and source notes.
870
+ - `text-panel` — language module for title, body text, bullets, italic quote text, formula text, and source notes.
871
871
  - `media` — normal image/screenshot/diagram/logo/portrait component; use `hero` for full-bleed covers.
872
872
  - `echart-panel` — chart frame with caption/source structure.
873
873
  - `data-table` — structured table component for tabular data and source notes.
874
874
  - `steps` — process or phase sequence; compatibility implementation may use `.flow-*` classes.
875
875
  - `roadmap-horizontal` and `roadmap-vertical` — dated phases, milestones, historical evolution, or future plans; compatibility implementation may use `.tjh` and `.tjv` classes.
876
876
  - `hero` — full-bleed cover, section divider, closing, or strong visual statement with overlaid title/subtitle.
877
- - `stat-card`, `quote`, and `toc` — pattern components for their specific use cases.
877
+ - `stat-card` and `toc` — pattern components for their specific use cases.
878
878
  - `page-number` and `brand-watermark` — utility components.
879
879
 
880
880
  Do not expose `image-title`, `media--cover`, `editorial-*`, `flow-*`, `timeline-journey-*`, or decorative SVG as new component choices. Old classes may remain in CSS as compatibility implementation details.
@@ -884,7 +884,7 @@ Density guidance: normal content slides usually need 2-4 boxes. Evidence slides
884
884
  <!-- @component:box:start -->
885
885
  #### Box
886
886
 
887
- Card/group primitive for one idea, case, evidence item, metric, objection, risk, or action. Put `text-panel`, `media`, `echart-panel`, `data-table`, `stat-card`, or `quote` inside a box when they support the same idea.
887
+ Card/group primitive for one idea, case, evidence item, metric, objection, risk, or action. Put `text-panel`, `media`, `echart-panel`, `data-table`, or `stat-card` inside a box when they support the same idea.
888
888
 
889
889
  ```html
890
890
  <div class="box">
@@ -913,9 +913,9 @@ Card/group primitive for one idea, case, evidence item, metric, objection, risk,
913
913
 
914
914
  <!-- renamed from report-text-panel -->
915
915
 
916
- Unified narrative text container. Use inside any layout slot that needs a self-contained reading surface with heading, body copy, and optional footer metadata. The body zone accepts prose, a bullet list, or both — choose based on content, not convention.
916
+ Unified narrative text container. Use inside any layout slot that needs a self-contained reading surface with heading, body copy, italic quote text, formula text, and optional footer metadata. The body zone accepts prose, a bullet list, quote, formula, or a mix — choose based on content, not convention.
917
917
 
918
- `text-panel` is a neutral language container. Do not add a default left border, vertical accent bar, yellow/gold rule, or decorative stripe to it. Summit may use thin rules at the layout level or in `toc`, but not as a default `text-panel` treatment.
918
+ `text-panel` is a neutral language container. Do not add a default left border, vertical accent bar, yellow/gold rule, or decorative stripe to it. Summit may use thin rules at the layout level or in `toc`, but not as a default `text-panel` treatment. Quotes and formulas are text members inside `.text-panel-body`, not standalone components.
919
919
 
920
920
  ```html
921
921
  <!-- variant A: prose only (--dark) -->
@@ -925,6 +925,11 @@ Unified narrative text container. Use inside any layout slot that needs a self-c
925
925
  <h2 style="margin-top:16px;font-size:56px;line-height:1;letter-spacing:-0.03em;color:#f7f4ee;max-width:390px;">Narrative Heading</h2>
926
926
  <div class="text-panel-body" style="margin-top:20px;">
927
927
  <p style="color:rgba(243,238,230,0.84);max-width:390px;">Use one or two compact paragraphs when continuous prose fits the content better than a list.</p>
928
+ <blockquote class="text-panel-quote">Italic quote text belongs inside the text panel body.</blockquote>
929
+ <figure class="text-panel-formula" data-latex="\mathrm{ROI}=\frac{\mathrm{Gain}-\mathrm{Cost}}{\mathrm{Cost}}">
930
+ <span class="katex">Rendered formula</span>
931
+ <p class="text-panel-formula-caption">Formula text member</p>
932
+ </figure>
928
933
  </div>
929
934
  </div>
930
935
  <div class="text-panel-footer" style="color:rgba(243,238,230,0.68);">
@@ -1004,6 +1009,38 @@ Unified narrative text container. Use inside any layout slot that needs a self-c
1004
1009
  gap: 12px;
1005
1010
  }
1006
1011
 
1012
+ .text-panel-quote {
1013
+ margin: 0;
1014
+ font-style: italic;
1015
+ line-height: 1.46;
1016
+ color: var(--text-secondary);
1017
+ }
1018
+
1019
+ .text-panel-formula {
1020
+ margin: 0;
1021
+ display: grid;
1022
+ gap: 8px;
1023
+ color: var(--text-primary);
1024
+ }
1025
+
1026
+ .text-panel-formula-fallback {
1027
+ display: block;
1028
+ white-space: normal;
1029
+ overflow-wrap: anywhere;
1030
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1031
+ font-size: 0.82em;
1032
+ line-height: 1.35;
1033
+ color: inherit;
1034
+ }
1035
+
1036
+ .text-panel-formula-caption {
1037
+ margin: 0;
1038
+ font-size: 12px;
1039
+ letter-spacing: 0.08em;
1040
+ text-transform: uppercase;
1041
+ color: var(--text-muted);
1042
+ }
1043
+
1007
1044
  /* renamed from .report-panel-footer */
1008
1045
  .text-panel-footer {
1009
1046
  display: flex;
@@ -1014,7 +1051,7 @@ Unified narrative text container. Use inside any layout slot that needs a self-c
1014
1051
  ```
1015
1052
 
1016
1053
  Rules:
1017
- - `.text-panel-body` is the only required structural child. Place `<p>` elements, an `<ul class="editorial-list">`, or both inside it.
1054
+ - `.text-panel-body` is the only required structural child. Place `<p>` elements, an `<ul class="editorial-list">`, `<blockquote class="text-panel-quote">`, `<figure class="text-panel-formula">`, or a deliberate mix inside it. Use `.text-panel-formula-fallback` only inside formula figures when LaTeX cannot be rendered.
1018
1055
  - Eyebrow, heading, and footer are all optional — include them only when the content calls for them.
1019
1056
  - Default text panels should remain transparent. Choose `--dark` or `--light` only when the component intentionally needs its own reading field; do not mix variants within a single panel.
1020
1057
  - Pair with a visually dominant neighbor (image, chart) when the layout needs strong contrast against the text zone.
@@ -2107,110 +2144,6 @@ Minimal table-of-contents slide with a quiet title block on the left and a spaci
2107
2144
  - **Left footer stays small.** The footer should read like a restrained production note, not a secondary headline.
2108
2145
  <!-- @component:toc:end -->
2109
2146
 
2110
- <!-- @component:quote:start -->
2111
- #### Quote (.quote-block)
2112
-
2113
- Flat editorial quote block. Wide and short (width > height). Transparent background — place it inside any layout slot. The large decorative quotation mark is CSS-rendered (no icon dependency).
2114
-
2115
- ```html
2116
- <div class="quote-block">
2117
- <div class="quote-mark" aria-hidden="true">“</div>
2118
- <p class="quote-text">The mountains teach us that progress is measured not in speed, but in the ground gained against resistance.</p>
2119
- <div class="quote-attribution">
2120
- <div class="quote-avatar">JD</div><!-- or <img src="avatar.jpg" alt="Jane Doe"> -->
2121
- <div class="quote-meta">
2122
- <p class="quote-name">Jane Doe</p>
2123
- <p class="caption">CEO, Acme Corporation</p>
2124
- </div>
2125
- </div>
2126
- </div>
2127
- ```
2128
-
2129
- ```css
2130
- .quote-block {
2131
- position: relative;
2132
- padding: 36px 44px 32px;
2133
- overflow: hidden;
2134
- }
2135
-
2136
- .quote-mark {
2137
- position: absolute;
2138
- top: -18px;
2139
- left: 28px;
2140
- font-family: Baskerville, Georgia, serif;
2141
- font-size: 140px;
2142
- font-weight: 700;
2143
- line-height: 1;
2144
- color: var(--accent-sage);
2145
- opacity: 0.42;
2146
- pointer-events: none;
2147
- user-select: none;
2148
- }
2149
-
2150
- .quote-text {
2151
- position: relative;
2152
- font-size: 20px;
2153
- font-style: italic;
2154
- line-height: 1.5;
2155
- color: var(--text-primary);
2156
- max-width: 860px;
2157
- padding-top: 48px; /* clears the decorative mark */
2158
- }
2159
-
2160
- .quote-attribution {
2161
- display: flex;
2162
- align-items: center;
2163
- gap: 14px;
2164
- margin-top: 24px;
2165
- }
2166
-
2167
- .quote-avatar {
2168
- width: 48px;
2169
- height: 48px;
2170
- border-radius: 50%;
2171
- background: var(--bg-page-alt);
2172
- border: 1px solid var(--line-strong);
2173
- display: flex;
2174
- align-items: center;
2175
- justify-content: center;
2176
- font-family: var(--font-display);
2177
- font-size: var(--font-size-body);
2178
- font-weight: 700;
2179
- color: var(--text-muted);
2180
- flex-shrink: 0;
2181
- overflow: hidden;
2182
- }
2183
-
2184
- .quote-avatar img {
2185
- width: 100%;
2186
- height: 100%;
2187
- object-fit: cover;
2188
- }
2189
-
2190
- .quote-meta {
2191
- display: flex;
2192
- flex-direction: column;
2193
- gap: 2px;
2194
- }
2195
-
2196
- .quote-name {
2197
- font-size: var(--font-size-body);
2198
- font-weight: 600;
2199
- color: var(--text-primary);
2200
- line-height: 1.3;
2201
- }
2202
- ```
2203
-
2204
- **Tips:**
2205
-
2206
- - **Dark background**: override text colors on the parent slot — `color: var(--bg-page)` for `.quote-text` and `.quote-name`; increase `.quote-mark` opacity to `0.15` (the sage hue reads better against dark at lower opacity).
2207
- - **Avatar with photo**: replace `<div class="quote-avatar">JD</div>` with `<div class="quote-avatar"><img src="path/to/photo.jpg" alt="Jane Doe"></div>`. The `overflow: hidden` + `object-fit: cover` handles any image aspect ratio.
2208
- - **Quote text length**: adjust `font-size` between `18px` (longer quotes, 3+ lines) and `24px` (short punchy quotes, 1 line). Keep `line-height: 1.5`.
2209
- - **Opacity guidance**: on `--bg-page` (warm paper), `.quote-mark` opacity `0.25` works well. On dark `--bg-frame` backgrounds, reduce to `0.15`.
2210
- - **Source-only attribution** (no person): omit `.quote-avatar` entirely and use `.quote-name` for the source text (e.g. a report title or publication name).
2211
-
2212
- <!-- @component:quote:end -->
2213
-
2214
2147
  <!-- @component:brand-watermark:start -->
2215
2148
  #### Brand Watermark
2216
2149
 
@@ -146,6 +146,21 @@ body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primar
146
146
  .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); }
147
147
  .template-text-panel--color .template-text-panel-title { color: white; }
148
148
  .template-text-panel--color .template-text-panel-body { color: rgba(255,255,255,0.78); }
149
+ .template-text-panel-quote { margin: 2px 0 0; font-size: 22px; line-height: 1.44; font-style: italic; color: var(--text-secondary); }
150
+ .template-text-panel--color .template-text-panel-quote { color: rgba(255,255,255,0.82); }
151
+ .template-text-panel-formula { margin: 0; width: 100%; display: grid; gap: 8px; color: var(--text-primary); }
152
+ .template-text-panel--color .template-text-panel-formula { color: white; }
153
+ .template-text-panel-formula .katex-display { margin: 0; overflow: visible; }
154
+ .template-text-panel-formula .katex { font-size: 1.08em; color: inherit; }
155
+ .template-text-panel-formula-fallback { display: block; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82em; line-height: 1.35; color: inherit; }
156
+ .template-text-panel-formula-caption { margin: 0; font-size: 14px; line-height: 1.35; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
157
+ .template-text-panel--color .template-text-panel-formula-caption { color: rgba(255,255,255,0.72); }
158
+ .text-panel-quote { margin: 0; font-style: italic; line-height: 1.46; color: var(--text-secondary); }
159
+ .text-panel-formula { margin: 0; display: grid; gap: 8px; color: var(--text-primary); }
160
+ .text-panel-formula .katex-display { margin: 0; overflow: visible; }
161
+ .text-panel-formula .katex { color: inherit; }
162
+ .text-panel-formula-fallback { display: block; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82em; line-height: 1.35; color: inherit; }
163
+ .text-panel-formula-caption { margin: 0; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
149
164
  .template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
150
165
  .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
151
166
  .template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
@@ -1321,7 +1321,7 @@ function inferComponentNesting(name: string): DesignInventoryComponent["nesting"
1321
1321
  return {
1322
1322
  role: "container",
1323
1323
  acceptsChildren: true,
1324
- allowedChildren: ["text-panel", "media", "echart-panel", "data-table", "stat-card", "quote", "steps", "roadmap-horizontal", "roadmap-vertical", "toc"],
1324
+ allowedChildren: ["text-panel", "media", "echart-panel", "data-table", "stat-card", "steps", "roadmap-horizontal", "roadmap-vertical", "toc"],
1325
1325
  }
1326
1326
  }
1327
1327
  if (name === "hero") return { role: "fullbleed", acceptsChildren: false }
@@ -0,0 +1,14 @@
1
+ import { dirname, resolve } from "path"
2
+ import { pathToFileURL } from "url"
3
+
4
+ export function withExportBaseHref(html: string, htmlFilePath: string): string {
5
+ if (/<base\b/i.test(html)) return html
6
+
7
+ const baseHref = pathToFileURL(`${dirname(resolve(htmlFilePath))}/`).href
8
+ const baseTag = `<base href="${baseHref}">`
9
+
10
+ if (/<head[^>]*>/i.test(html)) {
11
+ return html.replace(/<head([^>]*)>/i, `<head$1>\n${baseTag}`)
12
+ }
13
+ return `${baseTag}\n${html}`
14
+ }
@@ -141,6 +141,7 @@
141
141
  </header><div class="template-body template-grid template-chart-layout"><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><div class="template-text-panel template-text-panel--color template-chart-takeaway-panel" data-template-slot="takeaways">
142
142
  <h2 class="template-text-panel-title">What to read</h2>
143
143
  <div class="template-chart-takeaway-list"><section class="template-chart-takeaway-item"><h3>Trend</h3><p>Call out the movement or comparison the chart is meant to prove, including the direction and the comparison baseline.</p></section><section class="template-chart-takeaway-item"><h3>Driver</h3><p>Name the likely reason without overclaiming; separate observed movement from the interpretation or hypothesis.</p></section><section class="template-chart-takeaway-item"><h3>Decision use</h3><p>Explain how the chart changes the recommendation, what threshold matters, and what follow-up evidence would reduce risk.</p></section></div>
144
+ <figure class="template-text-panel-formula" data-latex="\mathrm{CAGR}=\left(\frac{\mathrm{FY26\ Plan}}{\mathrm{FY25}}\right)^{1/n}-1"><span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mrow><mi mathvariant="normal">C</mi><mi mathvariant="normal">A</mi><mi mathvariant="normal">G</mi><mi mathvariant="normal">R</mi></mrow><mo>=</mo><msup><mrow><mo fence="true">(</mo><mfrac><mrow><mi mathvariant="normal">F</mi><mi mathvariant="normal">Y</mi><mn>26</mn><mtext> </mtext><mi mathvariant="normal">P</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">n</mi></mrow><mrow><mi mathvariant="normal">F</mi><mi mathvariant="normal">Y</mi><mn>25</mn></mrow></mfrac><mo fence="true">)</mo></mrow><mrow><mn>1</mn><mi mathvariant="normal">/</mi><mi>n</mi></mrow></msup><mo>−</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">\mathrm{CAGR}=\left(\frac{\mathrm{FY26\ Plan}}{\mathrm{FY25}}\right)^{1/n}-1</annotation></semantics></math></span><p class="template-text-panel-formula-caption">Formula text member</p></figure>
144
145
  </div></div>
145
146
 
146
147
  </div>
@@ -153,7 +154,7 @@
153
154
  <header>
154
155
  <p class="template-eyebrow">Template 11 / 17</p>
155
156
  <h1 class="template-title">table</h1>
156
- </header><div class="template-body"><div class="template-table-layout"><div class="template-side-panel template-text-panel template-text-panel--clear" data-template-slot="text-card"><h2 class="template-side-panel-title template-text-panel-title">Financial readout</h2><p class="template-side-panel-body template-text-panel-body">Read top-line growth first, then check margin, cash conversion, and retention to see whether the plan is financially durable.</p></div><div class="template-table-region" data-template-slot="table"><div class="template-table-wrap"><table class="template-table"><thead><tr><th>Line item</th><th>FY2025</th><th>FY2026 Plan</th><th>YoY / note</th></tr></thead><tbody><tr><td>Revenue</td><td>$84.2M</td><td>$104.8M</td><td>+24% planned growth</td></tr><tr><td>Gross margin</td><td>68.4%</td><td>71.2%</td><td>+280 bps mix shift</td></tr><tr><td>Operating expense</td><td>$42.7M</td><td>$49.1M</td><td>Scale hiring below revenue growth</td></tr><tr><td>EBITDA</td><td>$14.9M</td><td>$23.6M</td><td>+58% operating leverage</td></tr><tr><td>Free cash flow</td><td>$9.8M</td><td>$16.4M</td><td>Cash conversion improves to 69%</td></tr><tr><td>Net retention</td><td>116%</td><td>121%</td><td>Expansion supports plan quality</td></tr></tbody></table></div></div></div></div>
157
+ </header><div class="template-body"><div class="template-table-layout"><div class="template-side-panel template-text-panel template-text-panel--clear" data-template-slot="text-card"><h2 class="template-side-panel-title template-text-panel-title">Financial readout</h2><p class="template-side-panel-body template-text-panel-body">Read top-line growth first, then check margin, cash conversion, and retention to see whether the plan is financially durable.</p><blockquote class="template-text-panel-quote">Durability shows up when growth, margin, and cash all point in the same direction.</blockquote></div><div class="template-table-region" data-template-slot="table"><div class="template-table-wrap"><table class="template-table"><thead><tr><th>Line item</th><th>FY2025</th><th>FY2026 Plan</th><th>YoY / note</th></tr></thead><tbody><tr><td>Revenue</td><td>$84.2M</td><td>$104.8M</td><td>+24% planned growth</td></tr><tr><td>Gross margin</td><td>68.4%</td><td>71.2%</td><td>+280 bps mix shift</td></tr><tr><td>Operating expense</td><td>$42.7M</td><td>$49.1M</td><td>Scale hiring below revenue growth</td></tr><tr><td>EBITDA</td><td>$14.9M</td><td>$23.6M</td><td>+58% operating leverage</td></tr><tr><td>Free cash flow</td><td>$9.8M</td><td>$16.4M</td><td>Cash conversion improves to 69%</td></tr><tr><td>Net retention</td><td>116%</td><td>121%</td><td>Expansion supports plan quality</td></tr></tbody></table></div></div></div></div>
157
158
 
158
159
  </div>
159
160
  <div class="template-page-number">11</div>
@@ -232,7 +233,7 @@
232
233
  <header>
233
234
  <p class="template-eyebrow">Template 14 / 17</p>
234
235
  <h1 class="template-title">timeline</h1>
235
- </header><div class="template-body"><div class="template-timeline-layout template-timeline-layout--left"><div class="template-side-panel template-text-panel template-text-panel--color" data-template-slot="insight"><h2 class="template-side-panel-title template-text-panel-title">Reading the journey</h2><p class="template-side-panel-body template-text-panel-body">The timeline should show sequence and decision rhythm, while the side panel explains why the milestones matter.</p></div><div class="template-timeline template-timeline--vertical" data-template-slot="timeline" style="--timeline-count:4"><article class="template-timeline-item">
236
+ </header><div class="template-body"><div class="template-timeline-layout template-timeline-layout--left"><div class="template-side-panel template-text-panel template-text-panel--color" data-template-slot="insight"><h2 class="template-side-panel-title template-text-panel-title">Reading the journey</h2><p class="template-side-panel-body template-text-panel-body">The timeline should show sequence and decision rhythm, while the side panel explains why the milestones matter.</p><blockquote class="template-text-panel-quote">Sequence is evidence when each step changes what the audience can believe.</blockquote></div><div class="template-timeline template-timeline--vertical" data-template-slot="timeline" style="--timeline-count:4"><article class="template-timeline-item">
236
237
  <span class="template-timeline-dot" aria-hidden="true"></span>
237
238
  <div class="template-timeline-copy">
238
239
  <p class="template-timeline-date">Mar 2019</p>
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "fs"
2
2
  import { isAbsolute, normalize, resolve } from "path"
3
+ import katex from "katex"
3
4
  import { getPageTemplateVocabulary } from "./vocabulary"
4
5
 
5
6
  export type PageTemplateStatus = "metadata-only" | "renderable"
@@ -141,6 +142,9 @@ const templates: PageTemplateDefinition[] = [
141
142
  field("chartTitle", "string", "Chart title."),
142
143
  field("takeawaysTitle", "string", "Title for the interpretation text panel."),
143
144
  field("items", "items[]", "Takeaways.", true),
145
+ field("quote", "string", "Optional italic quote text member inside the text panel."),
146
+ field("formulaLatex", "string", "Optional LaTeX formula text member inside the text panel."),
147
+ field("formulaCaption", "string", "Optional formula caption."),
144
148
  ], ["Chart area must be explicit and bounded."], ["Chart panel and takeaways both exist."]),
145
149
  define("table", "Table", "Explain a structured table with a left reading card and right table region.", [
146
150
  field("title", "string", "Slide title.", true),
@@ -278,6 +282,8 @@ export function builtInPreviewFixtures(): BuiltInPreviewFixture[] {
278
282
  fixture("chart-takeaways", {
279
283
  title: "chart-takeaways",
280
284
  takeawaysTitle: "What to read",
285
+ formulaLatex: "\\mathrm{CAGR}=\\left(\\frac{\\mathrm{FY26\\ Plan}}{\\mathrm{FY25}}\\right)^{1/n}-1",
286
+ formulaCaption: "Formula text member",
281
287
  items: [
282
288
  { label: "Trend", description: "Call out the movement or comparison the chart is meant to prove, including the direction and the comparison baseline." },
283
289
  { label: "Driver", description: "Name the likely reason without overclaiming; separate observed movement from the interpretation or hypothesis." },
@@ -288,6 +294,7 @@ export function builtInPreviewFixtures(): BuiltInPreviewFixture[] {
288
294
  title: "table",
289
295
  textTitle: "Financial readout",
290
296
  textBody: "Read top-line growth first, then check margin, cash conversion, and retention to see whether the plan is financially durable.",
297
+ quote: "Durability shows up when growth, margin, and cash all point in the same direction.",
291
298
  columns: ["Line item", "FY2025", "FY2026 Plan", "YoY / note"],
292
299
  rows: [
293
300
  ["Revenue", "$84.2M", "$104.8M", "+24% planned growth"],
@@ -324,6 +331,7 @@ export function builtInPreviewFixtures(): BuiltInPreviewFixture[] {
324
331
  title: "timeline",
325
332
  insightTitle: "Reading the journey",
326
333
  insightBody: "The timeline should show sequence and decision rhythm, while the side panel explains why the milestones matter.",
334
+ quote: "Sequence is evidence when each step changes what the audience can believe.",
327
335
  milestones: [
328
336
  { date: "Mar 2019", label: "Launch", description: "Baseline mapping." },
329
337
  { date: "Nov 2019", label: "Audit", description: "Evidence sprint." },
@@ -573,6 +581,15 @@ ${lucentClosingBackgroundCss}
573
581
  .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
582
  .template-text-panel--color .template-text-panel-title { color: white; }
575
583
  .template-text-panel--color .template-text-panel-body { color: rgba(255,255,255,0.78); }
584
+ .template-text-panel-quote { margin: 2px 0 0; font-size: 22px; line-height: 1.44; font-style: italic; color: var(--text-secondary); }
585
+ .template-text-panel--color .template-text-panel-quote { color: rgba(255,255,255,0.82); }
586
+ .template-text-panel-formula { margin: 0; width: 100%; display: grid; gap: 8px; color: var(--text-primary); }
587
+ .template-text-panel--color .template-text-panel-formula { color: white; }
588
+ .template-text-panel-formula .katex-display { margin: 0; overflow: visible; }
589
+ .template-text-panel-formula .katex { font-size: 1.08em; color: inherit; }
590
+ .template-text-panel-formula-fallback { display: block; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82em; line-height: 1.35; color: inherit; }
591
+ .template-text-panel-formula-caption { margin: 0; font-size: 14px; line-height: 1.35; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
592
+ .template-text-panel--color .template-text-panel-formula-caption { color: rgba(255,255,255,0.72); }
576
593
  .template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; }
577
594
  .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid rgba(255,255,255,0.24); }
578
595
  .template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; }
@@ -1045,6 +1062,7 @@ function chartTakeawayPanel(content: Record<string, any>): string {
1045
1062
  return `<div class="template-text-panel template-text-panel--color template-chart-takeaway-panel" data-template-slot="takeaways">
1046
1063
  <h2 class="template-text-panel-title">${escapeHtml(title)}</h2>
1047
1064
  <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>
1065
+ ${renderTextMembers(content)}
1048
1066
  </div>`
1049
1067
  }
1050
1068
 
@@ -1080,6 +1098,7 @@ function tablePage(content: Record<string, any>): string {
1080
1098
  const panelContent = {
1081
1099
  insightTitle: stringValue(content.textTitle) || "What to read",
1082
1100
  insightBody: stringValue(content.textBody) || "Use this card to explain the comparison, caveat, or decision implication before the audience scans the table.",
1101
+ quote: stringValue(content.quote),
1083
1102
  }
1084
1103
  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
1104
  }
@@ -1137,7 +1156,38 @@ function renderTextPanel(content: Record<string, any>, slot = "insight", variant
1137
1156
  const body = stringValue(content.insightBody)
1138
1157
  if (!body) return ""
1139
1158
  const title = stringValue(content.insightTitle) || "Insight"
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>`
1159
+ 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>${renderTextMembers(content)}</div>`
1160
+ }
1161
+
1162
+ function renderTextMembers(content: Record<string, any>): string {
1163
+ const quote = stringValue(content.quote)
1164
+ const formulaLatex = stringValue(content.formulaLatex)
1165
+ const parts: string[] = []
1166
+ if (quote) parts.push(`<blockquote class="template-text-panel-quote">${escapeHtml(quote)}</blockquote>`)
1167
+ if (formulaLatex) parts.push(renderFormulaMember(formulaLatex, stringValue(content.formulaCaption)))
1168
+ return parts.join("")
1169
+ }
1170
+
1171
+ function renderFormulaMember(latex: string, caption = ""): string {
1172
+ const escapedLatex = escapeAttribute(latex)
1173
+ const rendered = renderLatex(latex)
1174
+ const captionHtml = caption ? `<p class="template-text-panel-formula-caption">${escapeHtml(caption)}</p>` : ""
1175
+ return `<figure class="template-text-panel-formula" data-latex="${escapedLatex}">${rendered}${captionHtml}</figure>`
1176
+ }
1177
+
1178
+ function renderLatex(latex: string): string {
1179
+ try {
1180
+ return katex.renderToString(latex, {
1181
+ displayMode: true,
1182
+ output: "mathml",
1183
+ strict: "warn",
1184
+ throwOnError: true,
1185
+ trust: false,
1186
+ })
1187
+ } catch (error) {
1188
+ const message = error instanceof Error ? error.message : String(error)
1189
+ return `<code class="template-text-panel-formula-fallback" data-formula-error="${escapeAttribute(message)}">${escapeHtml(latex)}</code>`
1190
+ }
1141
1191
  }
1142
1192
 
1143
1193
  function imageCard(input: any): string {
@@ -1189,11 +1239,11 @@ function scaffoldSeed(templateId: string, seed: Record<string, any>): Record<str
1189
1239
  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 }
1190
1240
  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 }
1191
1241
  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 }
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 }
1242
+ if (templateId === "chart-takeaways") return { takeawaysTitle: "What to read", items: defaultItems(["Trend", "Driver", "Decision use"]), formulaLatex: "\\mathrm{CAGR}=\\left(\\frac{\\mathrm{FY26\\ Plan}}{\\mathrm{FY25}}\\right)^{1/n}-1", formulaCaption: "Formula text member", ...base }
1243
+ if (templateId === "table") return { textTitle: "Financial readout", textBody: "Replace with the table reading note, caveat, or decision implication.", quote: "Durability shows up when growth, margin, and cash all point in the same direction.", 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 }
1194
1244
  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 }
1195
1245
  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 }
1246
+ if (templateId === "timeline") return { orientation: "vertical", insightTitle: "Reading the journey", insightBody: "Replace with the timeline interpretation or caveat.", quote: "Sequence is evidence when each step changes what the audience can believe.", 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 }
1197
1247
  if (templateId === "process-steps") return { steps: defaultItems(["Step 1", "Step 2", "Step 3"]), ...base }
1198
1248
  if (templateId === "recommendation-decision") return { recommendation: "Replace with the recommended decision.", items: defaultItems(["Rationale"]), steps: defaultItems(["Pilot", "Validate", "Ship"]), ...base }
1199
1249
  if (templateId === "risks-tradeoffs") return { items: defaultItems(["Risk", "Tradeoff", "Mitigation"]), ...base }
@@ -55,8 +55,8 @@ 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", "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."]),
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.", "Text panels may include quote and formula text members; do not model them as standalone components."]),
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.", "Text panels may include quote text members; do not model quotes as standalone components."]),
60
60
  vocab("table-comparison", ["template-table-wrap", "template-table"], ["table"], ["table", "insight"], ["Table headers and body should remain structured, not prose-only."]),
61
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
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."]),
@@ -107,6 +107,10 @@ const additionalClasses = [
107
107
  "template-text-panel--color",
108
108
  "template-text-panel-title",
109
109
  "template-text-panel-body",
110
+ "template-text-panel-quote",
111
+ "template-text-panel-formula",
112
+ "template-text-panel-formula-caption",
113
+ "template-text-panel-formula-fallback",
110
114
  "template-insight-panel",
111
115
  "template-insight-title",
112
116
  "template-insight-icon",
package/lib/pdf/export.ts CHANGED
@@ -31,6 +31,7 @@ import { randomBytes } from "crypto"
31
31
  import { launchChrome } from "../browser/chrome"
32
32
  import { detectDeckHtml } from "../html-export/deck-detect"
33
33
  import { exportSinglePageHtmlPdf } from "../html-export"
34
+ import { withExportBaseHref } from "../export/html"
34
35
 
35
36
  // ── Constants ────────────────────────────────────────────────────────────────
36
37
 
@@ -187,16 +188,6 @@ async function toDataUrlFromRef(ref: string, baseDir: string): Promise<string |
187
188
  }
188
189
  }
189
190
 
190
- function withExportBaseHref(html: string, htmlFilePath: string): string {
191
- const baseHref = pathToFileURL(`${dirname(resolve(htmlFilePath))}/`).href
192
- const baseTag = `<base href="${baseHref}">`
193
- if (/<base\b/i.test(html)) return html
194
- if (/<head[^>]*>/i.test(html)) {
195
- return html.replace(/<head([^>]*)>/i, `<head$1>\n${baseTag}`)
196
- }
197
- return `${baseTag}\n${html}`
198
- }
199
-
200
191
  async function prepareSlidesForExport(page: any): Promise<void> {
201
192
  await page.evaluate((canvasWidth: number, canvasHeight: number) => {
202
193
  document.documentElement.style.scrollSnapType = "none"