@bendyline/squisq 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/{Doc-ylqWKDc_.d.ts → Doc-DynJleo-.d.ts} +159 -2
  2. package/dist/{ImageEditDoc-D828IXJ1.d.ts → ImageEditDoc-Ca4Tr9uN.d.ts} +1 -1
  3. package/dist/{chunk-PXLVXTLW.js → chunk-ILWDXLUK.js} +24 -6
  4. package/dist/chunk-ILWDXLUK.js.map +1 -0
  5. package/dist/{chunk-W4O2TX5I.js → chunk-JHZWYRSW.js} +569 -1
  6. package/dist/chunk-JHZWYRSW.js.map +1 -0
  7. package/dist/{chunk-X6DUONGT.js → chunk-JI77MGAK.js} +123 -10
  8. package/dist/chunk-JI77MGAK.js.map +1 -0
  9. package/dist/{chunk-TFJVXUHH.js → chunk-OTKVFVL6.js} +1403 -41
  10. package/dist/chunk-OTKVFVL6.js.map +1 -0
  11. package/dist/{chunk-MMPBOUK7.js → chunk-QUUPYOSP.js} +8 -2
  12. package/dist/chunk-QUUPYOSP.js.map +1 -0
  13. package/dist/chunk-VWCJ2WXC.js +215 -0
  14. package/dist/chunk-VWCJ2WXC.js.map +1 -0
  15. package/dist/{chunk-2PWOO4ZE.js → chunk-VYFRI7R6.js} +2 -2
  16. package/dist/{chunk-CY2R2VPR.js → chunk-XNLB57UD.js} +2 -2
  17. package/dist/doc/index.d.ts +144 -5
  18. package/dist/doc/index.js +24 -4
  19. package/dist/generate/index.d.ts +1 -1
  20. package/dist/imageEdit/index.d.ts +3 -3
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.js +66 -8
  23. package/dist/jsonForm/index.d.ts +1 -1
  24. package/dist/jsonForm/index.js +4 -4
  25. package/dist/materializePageSection-DDzubQXo.d.ts +223 -0
  26. package/dist/narration/index.d.ts +1 -1
  27. package/dist/narration/index.js +3 -3
  28. package/dist/schemas/index.d.ts +30 -5
  29. package/dist/schemas/index.js +41 -3
  30. package/dist/{themeLibrary-RjVgLpUi.d.ts → themeLibrary-B72m0W1y.d.ts} +1 -1
  31. package/dist/transform/index.d.ts +8 -9
  32. package/dist/transform/index.js +1 -1
  33. package/package.json +1 -1
  34. package/src/__tests__/materializeBlockLayers.test.ts +32 -0
  35. package/src/__tests__/materializePageSections.test.ts +404 -0
  36. package/src/__tests__/pageStyle.test.ts +258 -0
  37. package/src/__tests__/templateAnnotationParse.test.ts +13 -0
  38. package/src/doc/index.ts +34 -0
  39. package/src/doc/materializeBlockLayers.ts +170 -9
  40. package/src/doc/page/PageSection.ts +174 -0
  41. package/src/doc/page/materializePageSection.ts +616 -0
  42. package/src/doc/page/resolvePageBlock.ts +108 -0
  43. package/src/doc/page/sectionExtractors.ts +466 -0
  44. package/src/doc/pageCss.ts +628 -0
  45. package/src/doc/templateInputs.ts +1 -0
  46. package/src/schemas/Doc.ts +22 -1
  47. package/src/schemas/PageStyle.ts +213 -0
  48. package/src/schemas/Theme.ts +7 -0
  49. package/src/schemas/index.ts +2 -0
  50. package/src/schemas/pageStyleDefaults.ts +204 -0
  51. package/src/schemas/themeCompile.ts +7 -0
  52. package/src/schemas/themeValidator.ts +183 -0
  53. package/src/schemas/themes/bold.json +29 -0
  54. package/src/schemas/themes/cinematic.json +32 -0
  55. package/src/schemas/themes/documentary.json +29 -0
  56. package/src/schemas/themes/gezellig.json +30 -0
  57. package/src/schemas/themes/magazine.json +37 -0
  58. package/src/schemas/themes/minimalist.json +29 -0
  59. package/src/schemas/themes/morning-light.json +32 -0
  60. package/src/schemas/themes/standard-dark.json +24 -0
  61. package/src/schemas/themes/standard.json +24 -0
  62. package/src/schemas/themes/tech-dark.json +42 -0
  63. package/src/schemas/themes/warm-earth.json +24 -0
  64. package/src/transform/registry.ts +19 -0
  65. package/src/transform/styles/dataDriven.ts +1 -0
  66. package/src/transform/styles/documentary.ts +1 -0
  67. package/src/transform/styles/magazine.ts +1 -0
  68. package/src/transform/styles/minimal.ts +1 -0
  69. package/src/transform/styles/narrative.ts +1 -0
  70. package/src/transform/types.ts +7 -0
  71. package/dist/chunk-FVJUUTEM.js +0 -74
  72. package/dist/chunk-FVJUUTEM.js.map +0 -1
  73. package/dist/chunk-MMPBOUK7.js.map +0 -1
  74. package/dist/chunk-PXLVXTLW.js.map +0 -1
  75. package/dist/chunk-TFJVXUHH.js.map +0 -1
  76. package/dist/chunk-W4O2TX5I.js.map +0 -1
  77. package/dist/chunk-X6DUONGT.js.map +0 -1
  78. /package/dist/{chunk-2PWOO4ZE.js.map → chunk-VYFRI7R6.js.map} +0 -0
  79. /package/dist/{chunk-CY2R2VPR.js.map → chunk-XNLB57UD.js.map} +0 -0
@@ -0,0 +1,628 @@
1
+ /**
2
+ * Page CSS — the single source of truth for Page (linear) mode styling.
3
+ *
4
+ * `PAGE_BASE_CSS` is structural and theme-independent: every color, font,
5
+ * and dimension flows through `--squisq-page-*` custom properties
6
+ * (`buildPageCssVars`) and the token data-attributes the renderer stamps on
7
+ * the `.squisq-page` wrapper (`pageStyleDataAttributes`). One stylesheet
8
+ * therefore serves all themes and all design families — a theme changes the
9
+ * page's look purely through vars + data attributes, never through
10
+ * generated CSS.
11
+ *
12
+ * Consumers:
13
+ * - React `LinearDocView` injects `<style data-squisq-page>` with
14
+ * `PAGE_BASE_CSS` and sets the vars inline (self-contained — no CSS
15
+ * import needed, which keeps the standalone bundle and single-file HTML
16
+ * exports working).
17
+ * - A string-HTML exporter can call `buildPageCss(theme)` for the same
18
+ * bytes plus a `.squisq-page { … }` var block.
19
+ *
20
+ * Responsive behavior uses container queries against the scroll container
21
+ * (named `squisq-page`), with a media-query fallback for engines without
22
+ * container support.
23
+ */
24
+
25
+ import type { Theme } from '../schemas/Theme.js';
26
+ import type { ThemePageStyle } from '../schemas/PageStyle.js';
27
+ import { resolveFontFamily } from '../schemas/fontStacks.js';
28
+ import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
29
+ import { resolvePageStyle } from './page/materializePageSection.js';
30
+
31
+ /** Subtle noise texture (tiny inline SVG) for the `noise` page pattern. */
32
+ const NOISE_DATA_URI =
33
+ "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.35'/%3E%3C/svg%3E\")";
34
+
35
+ /**
36
+ * Custom-property values for a theme's page rendition. Set these inline on
37
+ * the `.squisq-page` wrapper (React) or in a `.squisq-page { … }` rule
38
+ * (string export). Per-section accent vars (`--squisq-page-accent*`) get
39
+ * page-level defaults here and are overridden per section by the renderer.
40
+ */
41
+ export function buildPageCssVars(
42
+ theme?: Theme,
43
+ pageStyle?: ThemePageStyle,
44
+ ): Record<string, string> {
45
+ const t = theme ?? DEFAULT_THEME;
46
+ const style = pageStyle ?? resolvePageStyle(t);
47
+ const colors = t.colors;
48
+ const typography = t.typography;
49
+ return {
50
+ '--squisq-page-bg': colors.background,
51
+ '--squisq-page-bg-alt': colors.backgroundLight,
52
+ '--squisq-page-text': colors.text,
53
+ '--squisq-page-text-muted': colors.textMuted,
54
+ '--squisq-page-primary': colors.primary,
55
+ '--squisq-page-secondary': colors.secondary,
56
+ '--squisq-page-highlight': colors.highlight,
57
+ '--squisq-page-title-font': resolveFontFamily(typography.titleFont, 'Georgia, serif'),
58
+ '--squisq-page-body-font': resolveFontFamily(typography.bodyFont, 'system-ui, sans-serif'),
59
+ '--squisq-page-mono-font': resolveFontFamily(
60
+ typography.monoFont,
61
+ 'ui-monospace, SFMono-Regular, Menlo, monospace',
62
+ ),
63
+ '--squisq-page-line-height': String(typography.lineHeight ?? 1.65),
64
+ '--squisq-page-title-weight': typography.titleWeight === 'normal' ? '400' : '700',
65
+ '--squisq-page-content-max': `${style.tokens.contentMaxWidth}px`,
66
+ '--squisq-page-wide-max': `${style.tokens.wideMaxWidth}px`,
67
+ '--squisq-page-radius': `${style.tokens.cornerRadius}px`,
68
+ // Per-section accent defaults; the renderer overrides these per section.
69
+ '--squisq-page-accent': colors.primary,
70
+ '--squisq-page-accent-bg': colors.backgroundLight,
71
+ '--squisq-page-accent-text': colors.text,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * The token data-attributes the renderer stamps on `.squisq-page` so the
77
+ * structural CSS can branch per design decision. Shared by React and any
78
+ * string exporter so both emit identical markup.
79
+ */
80
+ export function pageStyleDataAttributes(pageStyle: ThemePageStyle): Record<string, string> {
81
+ const tokens = pageStyle.tokens;
82
+ const attrs: Record<string, string> = {
83
+ 'data-family': pageStyle.family,
84
+ 'data-spacing': tokens.sectionSpacing,
85
+ 'data-divider': tokens.divider,
86
+ 'data-rhythm': tokens.backgroundRhythm,
87
+ 'data-hero-style': tokens.heroStyle,
88
+ 'data-eyebrow': tokens.headingTreatment.eyebrow,
89
+ 'data-heading-scale': tokens.headingTreatment.scale,
90
+ 'data-framing': tokens.imageFraming,
91
+ 'data-shadow': tokens.shadow,
92
+ 'data-quote-mark': tokens.quoteMark,
93
+ 'data-numerals': tokens.numeralStyle,
94
+ };
95
+ if (tokens.headingTreatment.case && tokens.headingTreatment.case !== 'none') {
96
+ attrs['data-heading-case'] = tokens.headingTreatment.case;
97
+ }
98
+ if (tokens.headingTreatment.underline && tokens.headingTreatment.underline !== 'none') {
99
+ attrs['data-underline'] = tokens.headingTreatment.underline;
100
+ }
101
+ if (tokens.pattern && tokens.pattern !== 'none') {
102
+ attrs['data-pattern'] = tokens.pattern;
103
+ }
104
+ return attrs;
105
+ }
106
+
107
+ /** Shared responsive collapse rules (referenced from both query forms). */
108
+ const NARROW_RULES = `
109
+ .squisq-page-hero-grid { grid-template-columns: 1fr; gap: 1.5rem; }
110
+ .squisq-page-feature { grid-template-columns: 1fr; }
111
+ .squisq-page-feature--media-right .squisq-page-feature-media { order: 0; }
112
+ .squisq-page-stats { flex-direction: column; gap: 1.75rem; }
113
+ .squisq-page-cards { grid-template-columns: 1fr; }
114
+ .squisq-page-gallery { grid-template-columns: repeat(2, 1fr); }
115
+ .squisq-page-milestone { grid-template-columns: 1fr; gap: 0.5rem; }
116
+ .squisq-page-milestone-rail { display: none; }
117
+ .squisq-page-section-inner { padding-inline: 18px; }
118
+ `;
119
+
120
+ /**
121
+ * Structural, theme-independent stylesheet for the page rendition.
122
+ * Everything is scoped under `.squisq-page`.
123
+ */
124
+ export const PAGE_BASE_CSS = `
125
+ /* ── Base ─────────────────────────────────────────────────────────── */
126
+ .squisq-page {
127
+ background: var(--squisq-page-bg);
128
+ color: var(--squisq-page-text);
129
+ font-family: var(--squisq-page-body-font);
130
+ line-height: var(--squisq-page-line-height);
131
+ --squisq-page-divider-color: color-mix(in srgb, var(--squisq-page-text) 16%, transparent);
132
+ --squisq-page-section-pad: 3.5rem;
133
+ --squisq-page-shadow: none;
134
+ }
135
+ .squisq-page *, .squisq-page *::before, .squisq-page *::after { box-sizing: border-box; }
136
+ .squisq-page img { max-width: 100%; height: auto; }
137
+
138
+ /* Page pattern washes (very low intensity) */
139
+ .squisq-page[data-pattern='dots'] {
140
+ background-image: radial-gradient(color-mix(in srgb, var(--squisq-page-text) 7%, transparent) 1px, transparent 1px);
141
+ background-size: 22px 22px;
142
+ }
143
+ .squisq-page[data-pattern='grid'] {
144
+ background-image:
145
+ linear-gradient(color-mix(in srgb, var(--squisq-page-primary) 7%, transparent) 1px, transparent 1px),
146
+ linear-gradient(90deg, color-mix(in srgb, var(--squisq-page-primary) 7%, transparent) 1px, transparent 1px);
147
+ background-size: 44px 44px;
148
+ }
149
+ .squisq-page[data-pattern='diagonal'] {
150
+ background-image: repeating-linear-gradient(
151
+ -45deg,
152
+ color-mix(in srgb, var(--squisq-page-text) 4%, transparent) 0 1px,
153
+ transparent 1px 14px
154
+ );
155
+ }
156
+ .squisq-page[data-pattern='noise'] { background-image: ${NOISE_DATA_URI}; }
157
+
158
+ /* Spacing scale */
159
+ .squisq-page[data-spacing='compact'] { --squisq-page-section-pad: 2.25rem; }
160
+ .squisq-page[data-spacing='comfortable'] { --squisq-page-section-pad: 3.5rem; }
161
+ .squisq-page[data-spacing='generous'] { --squisq-page-section-pad: 5.25rem; }
162
+
163
+ /* Shadow language */
164
+ .squisq-page[data-shadow='soft'] { --squisq-page-shadow: 0 14px 36px color-mix(in srgb, var(--squisq-page-text) 14%, transparent); }
165
+ .squisq-page[data-shadow='crisp'] { --squisq-page-shadow: 0 2px 10px color-mix(in srgb, var(--squisq-page-text) 26%, transparent); }
166
+ .squisq-page[data-shadow='heavy'] { --squisq-page-shadow: 10px 10px 0 color-mix(in srgb, var(--squisq-page-text) 82%, transparent); }
167
+
168
+ /* ── Sections ─────────────────────────────────────────────────────── */
169
+ .squisq-page-section { position: relative; padding-block: var(--squisq-page-section-pad); }
170
+ .squisq-page-section-inner {
171
+ max-width: var(--squisq-page-content-max);
172
+ margin-inline: auto;
173
+ padding-inline: 24px;
174
+ }
175
+ /* Backdrops anchor to the section; these content roots stack above them. */
176
+ .squisq-page-hero-body,
177
+ .squisq-page-banner-body,
178
+ .squisq-page-quote-figure { position: relative; }
179
+ .squisq-page-section--feature-split .squisq-page-section-inner,
180
+ .squisq-page-section--gallery .squisq-page-section-inner,
181
+ .squisq-page-section--table-section .squisq-page-section-inner,
182
+ .squisq-page-section--canvas-embed .squisq-page-section-inner,
183
+ .squisq-page-section--stat-band .squisq-page-section-inner,
184
+ .squisq-page-section--card-grid .squisq-page-section-inner,
185
+ .squisq-page-section--media-figure .squisq-page-section-inner,
186
+ .squisq-page-section--hero .squisq-page-section-inner {
187
+ max-width: var(--squisq-page-wide-max);
188
+ }
189
+
190
+ /* Background rhythm */
191
+ .squisq-page-section--bg-alternate { background: var(--squisq-page-bg-alt); }
192
+ .squisq-page-section--bg-accent {
193
+ background: var(--squisq-page-accent-bg);
194
+ color: var(--squisq-page-accent-text);
195
+ }
196
+ .squisq-page-section--bg-media { color: #ffffff; }
197
+
198
+ /* Dividers between adjacent sections */
199
+ .squisq-page[data-divider='hairline'] .squisq-page-section + .squisq-page-section { border-top: 1px solid var(--squisq-page-divider-color); }
200
+ .squisq-page[data-divider='thick-rule'] .squisq-page-section + .squisq-page-section { border-top: 3px solid var(--squisq-page-primary); }
201
+ .squisq-page[data-divider='double-rule'] .squisq-page-section + .squisq-page-section { border-top: 4px double var(--squisq-page-divider-color); }
202
+ .squisq-page[data-divider='dotted'] .squisq-page-section + .squisq-page-section { border-top: 2px dotted var(--squisq-page-divider-color); }
203
+ /* Banded sections carry their own edges; media sections never need rules. */
204
+ .squisq-page .squisq-page-section--bg-media,
205
+ .squisq-page .squisq-page-section--bg-media + .squisq-page-section,
206
+ .squisq-page .squisq-page-section--timeline-rail + .squisq-page-section--timeline-rail { border-top: none; }
207
+
208
+ /* ── Headings, eyebrows ───────────────────────────────────────────── */
209
+ .squisq-page-section-title {
210
+ font-family: var(--squisq-page-title-font);
211
+ font-weight: var(--squisq-page-title-weight);
212
+ line-height: 1.15;
213
+ margin: 0 0 0.5em;
214
+ }
215
+ .squisq-page[data-heading-case='uppercase'] .squisq-page-section-title { text-transform: uppercase; letter-spacing: 0.03em; }
216
+ .squisq-page[data-underline='accent-bar'] .squisq-page-section-title::after {
217
+ content: '';
218
+ display: block;
219
+ width: 56px;
220
+ height: 4px;
221
+ margin-top: 0.4em;
222
+ border-radius: 2px;
223
+ background: var(--squisq-page-accent);
224
+ }
225
+ .squisq-page[data-underline='full-rule'] .squisq-page-section-title {
226
+ border-bottom: 1px solid var(--squisq-page-divider-color);
227
+ padding-bottom: 0.35em;
228
+ }
229
+
230
+ .squisq-page-eyebrow {
231
+ display: block;
232
+ font-size: 0.8125rem;
233
+ font-weight: 600;
234
+ letter-spacing: 0.14em;
235
+ text-transform: uppercase;
236
+ color: var(--squisq-page-accent);
237
+ margin: 0 0 0.9em;
238
+ }
239
+ .squisq-page[data-eyebrow='numbered'] .squisq-page-eyebrow::after { content: ' —'; color: var(--squisq-page-text-muted); }
240
+ .squisq-page[data-eyebrow='mono-tag'] .squisq-page-eyebrow {
241
+ display: inline-block;
242
+ font-family: var(--squisq-page-mono-font);
243
+ text-transform: none;
244
+ letter-spacing: 0.06em;
245
+ border: 1px solid color-mix(in srgb, var(--squisq-page-accent) 55%, transparent);
246
+ border-radius: var(--squisq-page-radius);
247
+ padding: 0.15em 0.6em;
248
+ background: color-mix(in srgb, var(--squisq-page-accent) 10%, transparent);
249
+ }
250
+ .squisq-page[data-eyebrow='mono-tag'] .squisq-page-eyebrow::before { content: '['; opacity: 0.6; }
251
+ .squisq-page[data-eyebrow='mono-tag'] .squisq-page-eyebrow::after { content: ']'; opacity: 0.6; }
252
+
253
+ /* ── Hero ─────────────────────────────────────────────────────────── */
254
+ .squisq-page-section--hero { padding-block: calc(var(--squisq-page-section-pad) * 1.5); }
255
+ .squisq-page-hero-title {
256
+ font-family: var(--squisq-page-title-font);
257
+ font-weight: var(--squisq-page-title-weight);
258
+ line-height: 1.08;
259
+ margin: 0;
260
+ font-size: 2.75rem;
261
+ }
262
+ .squisq-page[data-heading-scale='display'] .squisq-page-hero-title { font-size: 3.4rem; }
263
+ .squisq-page[data-heading-scale='oversized'] .squisq-page-hero-title { font-size: 4rem; font-size: clamp(3rem, 9cqw, 6rem); line-height: 1.02; }
264
+ .squisq-page[data-heading-case='uppercase'] .squisq-page-hero-title { text-transform: uppercase; letter-spacing: 0.02em; }
265
+ .squisq-page-hero-subtitle {
266
+ margin: 1.1em 0 0;
267
+ font-size: 1.25rem;
268
+ color: var(--squisq-page-text-muted);
269
+ max-width: 42em;
270
+ white-space: pre-line;
271
+ }
272
+ .squisq-page-section--bg-media .squisq-page-hero-subtitle { color: rgba(255, 255, 255, 0.88); }
273
+
274
+ /* Stacked (default): centered column */
275
+ .squisq-page[data-hero-style='stacked'] .squisq-page-hero-body { text-align: center; }
276
+ .squisq-page[data-hero-style='stacked'] .squisq-page-hero-subtitle { margin-inline: auto; }
277
+
278
+ /* Oversized type: hard left, no decoration */
279
+ .squisq-page[data-hero-style='oversized-type'] .squisq-page-hero-title { font-size: 4.25rem; font-size: clamp(3.25rem, 10cqw, 6.5rem); line-height: 0.98; }
280
+
281
+ /* Split: text and media side by side */
282
+ .squisq-page-hero-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; align-items: center; }
283
+ .squisq-page-hero-grid .squisq-page-hero-media img { width: 100%; object-fit: cover; }
284
+
285
+ /* Full-bleed & letterbox: media behind text */
286
+ .squisq-page-section--hero-media {
287
+ padding-block: 0;
288
+ min-height: min(72svh, 620px);
289
+ display: flex;
290
+ align-items: flex-end;
291
+ }
292
+ .squisq-page[data-hero-style='letterbox'] .squisq-page-section--hero-media { min-height: min(52svh, 460px); }
293
+ .squisq-page-hero-backdrop { position: absolute; inset: 0; overflow: hidden; }
294
+ .squisq-page-hero-backdrop img { width: 100%; height: 100%; object-fit: cover; }
295
+ .squisq-page-hero-backdrop::after {
296
+ content: '';
297
+ position: absolute;
298
+ inset: 0;
299
+ background: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 30%, rgba(0, 0, 0, 0.68) 100%);
300
+ }
301
+ .squisq-page[data-hero-style='letterbox'] .squisq-page-hero-backdrop img { filter: saturate(0.9); }
302
+ .squisq-page-section--hero-media .squisq-page-section-inner { padding-block: 3rem; width: 100%; }
303
+
304
+ /* ── Banner (sectionHeader) ───────────────────────────────────────── */
305
+ .squisq-page-banner-title { font-size: 2rem; }
306
+ .squisq-page[data-heading-scale='display'] .squisq-page-banner-title { font-size: 2.5rem; }
307
+ .squisq-page[data-heading-scale='oversized'] .squisq-page-banner-title { font-size: 3rem; font-size: clamp(2.25rem, 6cqw, 3.75rem); }
308
+ .squisq-page-section--banner.squisq-page-section--bg-media { padding-block: 0; min-height: 280px; display: flex; align-items: flex-end; }
309
+ .squisq-page-section--banner.squisq-page-section--bg-media .squisq-page-section-inner { padding-block: 2rem; width: 100%; }
310
+ .squisq-page-section--banner .squisq-page-section-title { margin-bottom: 0; }
311
+
312
+ /* ── Stat band ────────────────────────────────────────────────────── */
313
+ .squisq-page-stats { display: flex; gap: 3rem; justify-content: center; text-align: center; flex-wrap: wrap; }
314
+ .squisq-page-stat { flex: 1 1 220px; max-width: 420px; }
315
+ .squisq-page-stat-value {
316
+ display: block;
317
+ font-family: var(--squisq-page-title-font);
318
+ font-weight: 700;
319
+ font-size: 3.25rem;
320
+ line-height: 1;
321
+ color: var(--squisq-page-accent);
322
+ }
323
+ .squisq-page[data-numerals='oversized'] .squisq-page-stat-value { font-size: 4.5rem; font-size: clamp(3.5rem, 8cqw, 5.5rem); }
324
+ .squisq-page[data-numerals='mono'] .squisq-page-stat-value { font-family: var(--squisq-page-mono-font); letter-spacing: -0.02em; }
325
+ .squisq-page[data-numerals='boxed'] .squisq-page-stat-value {
326
+ display: inline-block;
327
+ border: 2px solid var(--squisq-page-accent);
328
+ border-radius: var(--squisq-page-radius);
329
+ padding: 0.18em 0.4em;
330
+ }
331
+ .squisq-page-stat-title { display: block; margin-top: 0.9em; font-size: 1.125rem; font-weight: 600; }
332
+ .squisq-page-stat-body { display: block; margin-top: 0.45em; color: var(--squisq-page-text-muted); font-size: 0.95rem; }
333
+ .squisq-page-stat-meta { text-align: center; margin-top: 1.5rem; color: var(--squisq-page-text-muted); font-size: 0.85rem; }
334
+ .squisq-page-stat-bar { height: 10px; border-radius: 999px; background: var(--squisq-page-accent); margin-top: 0.8em; }
335
+ .squisq-page-stat--bars { text-align: left; }
336
+
337
+ /* ── Quote band ───────────────────────────────────────────────────── */
338
+ .squisq-page-quote {
339
+ margin: 0;
340
+ font-family: var(--squisq-page-title-font);
341
+ font-size: 1.6rem;
342
+ line-height: 1.35;
343
+ white-space: pre-line;
344
+ }
345
+ .squisq-page-section--em-strong .squisq-page-quote { font-size: 2.1rem; }
346
+ .squisq-page-section--em-quiet .squisq-page-quote { font-size: 1.25rem; color: var(--squisq-page-text-muted); }
347
+ .squisq-page-quote-attribution { margin-top: 1.1em; font-family: var(--squisq-page-body-font); font-size: 0.95rem; color: var(--squisq-page-text-muted); }
348
+ .squisq-page-quote-attribution::before { content: '— '; }
349
+ .squisq-page[data-quote-mark='accent-bar'] .squisq-page-quote-figure { border-left: 4px solid var(--squisq-page-accent); padding-left: 1.5rem; }
350
+ .squisq-page[data-quote-mark='oversized-glyph'] .squisq-page-quote-figure { position: relative; padding-top: 1.25rem; }
351
+ .squisq-page[data-quote-mark='oversized-glyph'] .squisq-page-quote-figure::before {
352
+ content: '\\201C';
353
+ position: absolute;
354
+ top: -0.12em;
355
+ left: -0.05em;
356
+ font-family: var(--squisq-page-title-font);
357
+ font-size: 5.5rem;
358
+ line-height: 1;
359
+ color: color-mix(in srgb, var(--squisq-page-accent) 42%, transparent);
360
+ pointer-events: none;
361
+ }
362
+ .squisq-page-section--v-display .squisq-page-quote-figure { text-align: center; border-left: none; }
363
+ .squisq-page-section--v-display .squisq-page-quote { font-size: 2.4rem; font-size: clamp(1.9rem, 5cqw, 3rem); }
364
+ .squisq-page-section--v-editorial .squisq-page-quote-figure {
365
+ border-block: 3px solid var(--squisq-page-primary);
366
+ border-left: none;
367
+ padding: 2rem 0.5rem;
368
+ }
369
+ /* Full-bleed quote over media */
370
+ .squisq-page-section--quote-band.squisq-page-section--bg-media { padding-block: 0; min-height: 380px; display: flex; align-items: center; }
371
+ .squisq-page-section--quote-band.squisq-page-section--bg-media .squisq-page-section-inner { padding-block: 3.5rem; }
372
+ .squisq-page-quote-backdrop { position: absolute; inset: 0; overflow: hidden; }
373
+ .squisq-page-quote-backdrop img, .squisq-page-quote-backdrop video { width: 100%; height: 100%; object-fit: cover; }
374
+ .squisq-page-quote-backdrop::after { content: ''; position: absolute; inset: 0; background: rgba(0, 0, 0, 0.55); }
375
+ .squisq-page-section--quote-band[data-hint-vignette] .squisq-page-quote-backdrop::after {
376
+ background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0.75) 100%);
377
+ }
378
+ .squisq-page-section--quote-band.squisq-page-section--bg-media .squisq-page-quote-figure { border: none; text-align: center; }
379
+
380
+ /* ── Feature split ────────────────────────────────────────────────── */
381
+ .squisq-page-feature { display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; align-items: center; }
382
+ .squisq-page-feature--media-right .squisq-page-feature-media { order: 2; }
383
+ .squisq-page-feature-media img { width: 100%; object-fit: cover; }
384
+ .squisq-page-feature-title { font-size: 1.75rem; }
385
+ .squisq-page-feature-body { color: var(--squisq-page-text-muted); font-size: 1.05rem; white-space: pre-line; margin: 0; }
386
+
387
+ /* ── Media figure ─────────────────────────────────────────────────── */
388
+ .squisq-page-figure { margin: 0; }
389
+ .squisq-page-figure img, .squisq-page-figure video { width: 100%; display: block; }
390
+ .squisq-page-figure figcaption { margin-top: 0.9em; font-size: 0.9rem; color: var(--squisq-page-text-muted); text-align: center; }
391
+ .squisq-page-media-credit { font-size: 0.78rem; opacity: 0.75; }
392
+
393
+ /* Media frame treatment (figures, features, gallery tiles, cards) */
394
+ .squisq-page-media-frame { overflow: hidden; }
395
+ .squisq-page[data-framing='rounded'] .squisq-page-media-frame { border-radius: var(--squisq-page-radius); }
396
+ .squisq-page[data-framing='bordered'] .squisq-page-media-frame { border: 3px solid var(--squisq-page-text); }
397
+ .squisq-page[data-framing='polaroid'] .squisq-page-media-frame {
398
+ background: #ffffff;
399
+ padding: 10px 10px 16px;
400
+ border: 1px solid rgba(0, 0, 0, 0.12);
401
+ border-radius: 2px;
402
+ }
403
+ .squisq-page[data-framing='letterboxed'] .squisq-page-media-frame img { aspect-ratio: 21 / 9; object-fit: cover; }
404
+ .squisq-page[data-framing='circle-accent'] .squisq-page-media-frame { border-radius: calc(var(--squisq-page-radius) * 2); }
405
+ .squisq-page .squisq-page-media-frame { box-shadow: var(--squisq-page-shadow); }
406
+
407
+ /* Rich body nodes that the selected template did not consume. This universal
408
+ * supplement keeps diagrams/media visible without making every template
409
+ * duplicate the same preservation logic. */
410
+ .squisq-page-rich-content {
411
+ margin-top: clamp(1.5rem, 4vw, 3rem);
412
+ padding-top: clamp(1.25rem, 3vw, 2rem);
413
+ border-top: 1px solid var(--squisq-page-divider-color);
414
+ }
415
+ .squisq-page-rich-content :is(img, video) { display: block; max-width: 100%; height: auto; margin-inline: auto; }
416
+ .squisq-page-rich-content .squisq-md-mermaid { min-height: min(520px, 62vh); margin-block: 0; }
417
+
418
+ /* ── Gallery ──────────────────────────────────────────────────────── */
419
+ .squisq-page-gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.25rem; }
420
+ .squisq-page-gallery .squisq-page-media-frame img { width: 100%; height: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
421
+ .squisq-page-gallery-caption { margin-top: 1.1rem; text-align: center; font-size: 0.9rem; color: var(--squisq-page-text-muted); }
422
+
423
+ /* ── Callout ──────────────────────────────────────────────────────── */
424
+ .squisq-page-callout {
425
+ border-radius: var(--squisq-page-radius);
426
+ border: 1px solid color-mix(in srgb, var(--squisq-page-accent) 32%, transparent);
427
+ border-left: 5px solid var(--squisq-page-accent);
428
+ background: color-mix(in srgb, var(--squisq-page-accent) 7%, var(--squisq-page-bg));
429
+ padding: 1.75rem 2rem;
430
+ box-shadow: var(--squisq-page-shadow);
431
+ display: flex;
432
+ gap: 1.75rem;
433
+ align-items: flex-start;
434
+ }
435
+ .squisq-page-callout-title { font-family: var(--squisq-page-title-font); font-size: 1.35rem; font-weight: 700; margin: 0 0 0.5em; }
436
+ .squisq-page-callout-body { margin: 0; white-space: pre-line; }
437
+ .squisq-page-callout-meta { margin-top: 0.9em; font-size: 0.82rem; color: var(--squisq-page-text-muted); }
438
+ .squisq-page-callout .squisq-page-media-frame { flex: 0 0 132px; }
439
+ .squisq-page-callout .squisq-page-media-frame img { width: 132px; height: 132px; object-fit: cover; }
440
+ .squisq-page-callout[data-hint-framing='circle-accent'] .squisq-page-media-frame,
441
+ .squisq-page-callout[data-hint-framing='circle-accent'] .squisq-page-media-frame img { border-radius: 50%; }
442
+ .squisq-page-section--v-definition .squisq-page-callout-title { font-style: italic; }
443
+ .squisq-page-section--v-diagnostic .squisq-page-callout { border-left-color: var(--squisq-page-highlight); }
444
+
445
+ /* ── Card grid ────────────────────────────────────────────────────── */
446
+ .squisq-page-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.5rem; }
447
+ .squisq-page-card {
448
+ border-radius: var(--squisq-page-radius);
449
+ background: var(--squisq-page-bg-alt);
450
+ border-top: 4px solid var(--squisq-page-card-accent, var(--squisq-page-accent));
451
+ padding: 1.75rem 1.75rem 1.9rem;
452
+ box-shadow: var(--squisq-page-shadow);
453
+ }
454
+ .squisq-page-card-title { font-family: var(--squisq-page-title-font); font-size: 1.3rem; margin: 0 0 0.4em; }
455
+ .squisq-page-card-body { margin: 0; color: var(--squisq-page-text-muted); white-space: pre-line; }
456
+ .squisq-page-cards-title { text-align: center; font-size: 1.9rem; margin-bottom: 1.75rem; }
457
+
458
+ /* ── Item list ────────────────────────────────────────────────────── */
459
+ .squisq-page-items { list-style: none; margin: 0; padding: 0; counter-reset: squisq-item; max-width: 40em; }
460
+ .squisq-page-items li {
461
+ counter-increment: squisq-item;
462
+ position: relative;
463
+ padding: 0.9em 0 0.9em 3.4em;
464
+ font-size: 1.1rem;
465
+ }
466
+ .squisq-page-items li + li { border-top: 1px solid var(--squisq-page-divider-color); }
467
+ .squisq-page-items li::before {
468
+ content: counter(squisq-item, decimal-leading-zero);
469
+ position: absolute;
470
+ left: 0;
471
+ top: 0.85em;
472
+ font-family: var(--squisq-page-title-font);
473
+ font-weight: 700;
474
+ color: var(--squisq-page-accent);
475
+ font-size: 1.15em;
476
+ }
477
+ .squisq-page[data-numerals='mono'] .squisq-page-items li::before { font-family: var(--squisq-page-mono-font); }
478
+ .squisq-page-items-title { font-size: 1.9rem; }
479
+
480
+ /* ── Timeline rail (dateEvent) ────────────────────────────────────── */
481
+ .squisq-page-section--timeline-rail { padding-block: 1.4rem; }
482
+ .squisq-page-section--timeline-rail:first-child { padding-top: var(--squisq-page-section-pad); }
483
+ .squisq-page-milestone { display: grid; grid-template-columns: 150px 1fr; gap: 2rem; align-items: baseline; position: relative; }
484
+ .squisq-page-milestone-date {
485
+ font-family: var(--squisq-page-title-font);
486
+ font-weight: 700;
487
+ color: var(--squisq-page-accent);
488
+ font-size: 1.05rem;
489
+ text-align: right;
490
+ }
491
+ .squisq-page-milestone-rail {
492
+ position: absolute;
493
+ left: 166px;
494
+ top: -1.4rem;
495
+ bottom: -1.4rem;
496
+ width: 2px;
497
+ background: color-mix(in srgb, var(--squisq-page-accent) 35%, transparent);
498
+ }
499
+ .squisq-page-milestone-rail::after {
500
+ content: '';
501
+ position: absolute;
502
+ left: 50%;
503
+ top: 0.55em;
504
+ width: 12px;
505
+ height: 12px;
506
+ border-radius: 50%;
507
+ transform: translateX(-50%);
508
+ background: var(--squisq-page-accent);
509
+ border: 2px solid var(--squisq-page-bg);
510
+ }
511
+ .squisq-page-milestone-body { padding-left: 2.25rem; white-space: pre-line; }
512
+ .squisq-page-milestone-footer { margin-top: 0.5em; font-size: 0.85rem; color: var(--squisq-page-text-muted); }
513
+
514
+ /* ── Table section ────────────────────────────────────────────────── */
515
+ .squisq-page-table-title { font-size: 1.9rem; }
516
+ .squisq-page-table-scroll { overflow-x: auto; border-radius: var(--squisq-page-radius); box-shadow: var(--squisq-page-shadow); }
517
+ .squisq-page-table { width: 100%; border-collapse: collapse; font-size: 0.98rem; }
518
+ .squisq-page-table th {
519
+ background: var(--squisq-page-accent-bg);
520
+ color: var(--squisq-page-accent-text);
521
+ font-family: var(--squisq-page-title-font);
522
+ text-align: left;
523
+ padding: 0.8em 1em;
524
+ }
525
+ .squisq-page-table td { padding: 0.7em 1em; border-top: 1px solid var(--squisq-page-divider-color); }
526
+ .squisq-page-table tbody tr:nth-child(even) { background: color-mix(in srgb, var(--squisq-page-text) 3.5%, transparent); }
527
+
528
+ /* ── Canvas embed (spatial SVG) ───────────────────────────────────── */
529
+ .squisq-page-canvas {
530
+ width: 100%;
531
+ margin-inline: auto;
532
+ overflow: hidden;
533
+ border-radius: var(--squisq-page-radius);
534
+ }
535
+ .squisq-page-canvas svg { display: block; width: 100%; height: 100%; }
536
+ .squisq-page-canvas--framed { border: 1px solid var(--squisq-page-divider-color); box-shadow: var(--squisq-page-shadow); }
537
+ .squisq-page-canvas--terminal { border: 1px solid color-mix(in srgb, var(--squisq-page-accent) 45%, transparent); }
538
+ .squisq-page-canvas--terminal::before {
539
+ content: '\\25CF \\25CF \\25CF';
540
+ display: block;
541
+ font-size: 9px;
542
+ letter-spacing: 4px;
543
+ padding: 6px 12px;
544
+ color: color-mix(in srgb, var(--squisq-page-accent) 70%, transparent);
545
+ background: color-mix(in srgb, var(--squisq-page-accent) 9%, var(--squisq-page-bg));
546
+ border-bottom: 1px solid color-mix(in srgb, var(--squisq-page-accent) 30%, transparent);
547
+ }
548
+
549
+ /* ── Prose ────────────────────────────────────────────────────────── */
550
+ .squisq-page-section--prose { padding-block: calc(var(--squisq-page-section-pad) * 0.45); }
551
+ .squisq-page[data-divider] .squisq-page-section--prose + .squisq-page-section--prose { border-top: none; }
552
+ .squisq-page-prose { font-size: 1.05rem; }
553
+ .squisq-page-prose p { margin: 0; }
554
+ .squisq-page-prose p + p { margin-top: 1.25em; }
555
+ .squisq-page-prose > * + * { margin-top: 1.1em; }
556
+ .squisq-page-prose a { color: var(--squisq-page-primary); text-decoration-color: color-mix(in srgb, var(--squisq-page-primary) 40%, transparent); }
557
+ .squisq-page-prose code { font-family: var(--squisq-page-mono-font); font-size: 0.9em; background: color-mix(in srgb, var(--squisq-page-text) 8%, transparent); border-radius: 4px; padding: 0.12em 0.35em; }
558
+ .squisq-page-prose pre { background: color-mix(in srgb, var(--squisq-page-text) 6%, transparent); border-radius: var(--squisq-page-radius); padding: 1em 1.25em; overflow-x: auto; }
559
+ .squisq-page-prose pre code { background: none; padding: 0; }
560
+ .squisq-page-prose blockquote { margin: 0; border-left: 4px solid var(--squisq-page-accent); padding-left: 1.25em; color: var(--squisq-page-text-muted); }
561
+ .squisq-page-prose ul, .squisq-page-prose ol { padding-left: 1.5em; margin: 0; }
562
+ .squisq-page-prose li + li { margin-top: 0.4em; }
563
+ .squisq-page-prose hr { border: none; border-top: 1px solid var(--squisq-page-divider-color); }
564
+ .squisq-page-prose table { width: 100%; border-collapse: collapse; }
565
+ .squisq-page-prose th { font-family: var(--squisq-page-title-font); text-align: left; padding: 0.6em 0.8em; border-bottom: 2px solid var(--squisq-page-divider-color); }
566
+ .squisq-page-prose td { padding: 0.55em 0.8em; border-bottom: 1px solid var(--squisq-page-divider-color); }
567
+ .squisq-page-prose img { border-radius: var(--squisq-page-radius); }
568
+ .squisq-page-prose h1, .squisq-page-prose h2, .squisq-page-prose h3,
569
+ .squisq-page-prose h4, .squisq-page-prose h5, .squisq-page-prose h6 {
570
+ font-family: var(--squisq-page-title-font);
571
+ font-weight: var(--squisq-page-title-weight);
572
+ line-height: 1.2;
573
+ margin: 0 0 0.45em;
574
+ }
575
+ .squisq-page-prose h1 { font-size: 2.1rem; }
576
+ .squisq-page-prose h2 { font-size: 1.65rem; }
577
+ .squisq-page-prose h3 { font-size: 1.35rem; }
578
+ .squisq-page-prose h4, .squisq-page-prose h5, .squisq-page-prose h6 { font-size: 1.15rem; }
579
+ .squisq-page-section--prose[data-hint-drop-cap] .squisq-page-prose > p:first-child::first-letter {
580
+ font-family: var(--squisq-page-title-font);
581
+ font-size: 3.4em;
582
+ font-weight: 700;
583
+ float: left;
584
+ line-height: 0.85;
585
+ padding-right: 0.12em;
586
+ color: var(--squisq-page-accent);
587
+ }
588
+
589
+ /* ── Footer band ──────────────────────────────────────────────────── */
590
+ .squisq-page-section--footer { text-align: center; }
591
+
592
+ /* ── Thin-margin embedding (chat bubbles) ─────────────────────────── */
593
+ .squisq-page--thin .squisq-page-section-inner { padding-inline: 0; }
594
+ .squisq-page--thin .squisq-page-section { padding-block: 1.5rem; }
595
+
596
+ /* Thumbnail image mode */
597
+ .squisq-page--thumbnail-images .squisq-page-prose img,
598
+ .squisq-page--thumbnail-images .squisq-page-figure img { max-width: 100px; max-height: 100px; object-fit: cover; }
599
+
600
+ /* ── Reveal animation (progressive enhancement) ───────────────────── */
601
+ @media (prefers-reduced-motion: no-preference) {
602
+ .squisq-page-reveal { opacity: 0; transform: translateY(14px); transition: opacity 0.55s ease, transform 0.55s ease; }
603
+ .squisq-page-reveal--in { opacity: 1; transform: none; }
604
+ }
605
+
606
+ /* ── Responsive ───────────────────────────────────────────────────── */
607
+ @container squisq-page (max-width: 720px) {
608
+ ${NARROW_RULES}
609
+ }
610
+ @supports not (container-type: inline-size) {
611
+ @media (max-width: 720px) {
612
+ ${NARROW_RULES}
613
+ }
614
+ }
615
+ `;
616
+
617
+ /**
618
+ * Complete stylesheet for a themed page: a `.squisq-page { … }` variable
619
+ * block followed by the structural CSS. For string-HTML consumers; the
620
+ * React view sets the vars inline instead.
621
+ */
622
+ export function buildPageCss(theme?: Theme): string {
623
+ const vars = buildPageCssVars(theme);
624
+ const varLines = Object.entries(vars)
625
+ .map(([name, value]) => ` ${name}: ${value};`)
626
+ .join('\n');
627
+ return `.squisq-page {\n${varLines}\n}\n${PAGE_BASE_CSS}`;
628
+ }
@@ -49,6 +49,7 @@ export interface FirstImage {
49
49
  export function extractBodyPlainText(contents?: MarkdownBlockNode[]): string {
50
50
  if (!contents || contents.length === 0) return '';
51
51
  return contents
52
+ .filter((node) => !(node.type === 'code' && node.lang?.trim().toLowerCase() === 'mermaid'))
52
53
  .map((n) => extractPlainText(n))
53
54
  .join('\n')
54
55
  .trim();
@@ -381,7 +381,8 @@ export type Layer =
381
381
  | MapLayer
382
382
  | VideoLayer
383
383
  | TableLayer
384
- | TreeLayer;
384
+ | TreeLayer
385
+ | MermaidLayer;
385
386
 
386
387
  interface BaseLayer {
387
388
  /** Unique identifier for this layer */
@@ -759,6 +760,26 @@ export interface TreeLayerStyle {
759
760
  fileIcon?: string;
760
761
  }
761
762
 
763
+ /**
764
+ * Mermaid diagram layer — renders an authored `mermaid` code fence through
765
+ * the official Mermaid runtime while keeping the fence source authoritative.
766
+ * Like video/table/tree layers, the browser renderer hosts the diagram in an
767
+ * SVG `<foreignObject>` so it participates in slide layout and frame capture.
768
+ */
769
+ export interface MermaidLayer extends BaseLayer {
770
+ type: 'mermaid';
771
+ content: {
772
+ /** Complete Mermaid source, excluding the Markdown fence delimiters. */
773
+ source: string;
774
+ /** Theme-derived panel color behind the generated SVG. */
775
+ background?: string;
776
+ /** Theme-derived foreground used by loading/error states. */
777
+ foreground?: string;
778
+ /** Inner panel padding in pixels. */
779
+ padding?: number;
780
+ };
781
+ }
782
+
762
783
  /**
763
784
  * Available map tile styles from free/open-source providers.
764
785
  */