@cyber-dash-tech/revela 0.19.6 → 0.19.7
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 +14 -14
- package/README.zh-CN.md +14 -14
- package/designs/lucent/DESIGN.md +1 -1
- package/designs/lucent/design.css +15 -0
- package/designs/lucent-dark/DESIGN.md +1 -1
- package/designs/lucent-dark/design.css +15 -0
- package/designs/monet/DESIGN.md +40 -127
- package/designs/monet/design.css +15 -0
- package/designs/starter/DESIGN.md +16 -24
- package/designs/starter/design.css +15 -0
- package/designs/summit/DESIGN.md +46 -113
- package/designs/summit/design.css +15 -0
- package/lib/design/designs.ts +1 -1
- package/lib/export/html.ts +14 -0
- package/lib/page-templates/built-in-preview.html +3 -2
- package/lib/page-templates/render.ts +54 -4
- package/lib/page-templates/vocabulary.ts +6 -2
- package/lib/pdf/export.ts +1 -10
- package/lib/pptx/export.ts +164 -15
- package/package.json +2 -1
- package/plugins/revela/.codex-plugin/plugin.json +1 -1
package/designs/summit/DESIGN.md
CHANGED
|
@@ -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.
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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; }
|
package/lib/design/designs.ts
CHANGED
|
@@ -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", "
|
|
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
|
|
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"
|