@bendyline/squisq 2.0.0 → 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.
- package/dist/{Doc-ylqWKDc_.d.ts → Doc-DynJleo-.d.ts} +159 -2
- package/dist/{ImageEditDoc-D828IXJ1.d.ts → ImageEditDoc-Ca4Tr9uN.d.ts} +1 -1
- package/dist/{chunk-PXLVXTLW.js → chunk-ILWDXLUK.js} +24 -6
- package/dist/chunk-ILWDXLUK.js.map +1 -0
- package/dist/{chunk-6PJRB2CF.js → chunk-JHZWYRSW.js} +570 -2
- package/dist/chunk-JHZWYRSW.js.map +1 -0
- package/dist/{chunk-GF3BVNLU.js → chunk-JI77MGAK.js} +195 -27
- package/dist/chunk-JI77MGAK.js.map +1 -0
- package/dist/{chunk-QVANWDPH.js → chunk-OTKVFVL6.js} +1403 -41
- package/dist/chunk-OTKVFVL6.js.map +1 -0
- package/dist/{chunk-LYNJEOAC.js → chunk-QUUPYOSP.js} +8 -2
- package/dist/chunk-QUUPYOSP.js.map +1 -0
- package/dist/chunk-VWCJ2WXC.js +215 -0
- package/dist/chunk-VWCJ2WXC.js.map +1 -0
- package/dist/{chunk-QTSJOY54.js → chunk-VYFRI7R6.js} +2 -2
- package/dist/{chunk-T3TI7GSZ.js → chunk-XNLB57UD.js} +2 -2
- package/dist/doc/index.d.ts +144 -5
- package/dist/doc/index.js +24 -4
- package/dist/generate/index.d.ts +1 -1
- package/dist/imageEdit/index.d.ts +3 -3
- package/dist/index.d.ts +6 -5
- package/dist/index.js +66 -8
- package/dist/jsonForm/index.d.ts +1 -1
- package/dist/jsonForm/index.js +4 -4
- package/dist/materializePageSection-DDzubQXo.d.ts +223 -0
- package/dist/narration/index.d.ts +1 -1
- package/dist/narration/index.js +3 -3
- package/dist/schemas/index.d.ts +30 -5
- package/dist/schemas/index.js +41 -3
- package/dist/{themeLibrary-RjVgLpUi.d.ts → themeLibrary-B72m0W1y.d.ts} +1 -1
- package/dist/transform/index.d.ts +8 -9
- package/dist/transform/index.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/customThemesFrontmatter.test.ts +1 -0
- package/src/__tests__/markdown.test.ts +13 -4
- package/src/__tests__/markdownSanitize.test.ts +7 -5
- package/src/__tests__/materializeBlockLayers.test.ts +32 -0
- package/src/__tests__/materializePageSections.test.ts +404 -0
- package/src/__tests__/pageStyle.test.ts +258 -0
- package/src/__tests__/templateAnnotationParse.test.ts +13 -0
- package/src/doc/index.ts +34 -0
- package/src/doc/markdownToDoc.ts +3 -1
- package/src/doc/materializeBlockLayers.ts +170 -9
- package/src/doc/page/PageSection.ts +174 -0
- package/src/doc/page/materializePageSection.ts +616 -0
- package/src/doc/page/resolvePageBlock.ts +108 -0
- package/src/doc/page/sectionExtractors.ts +466 -0
- package/src/doc/pageCss.ts +628 -0
- package/src/doc/templateInputs.ts +1 -0
- package/src/doc/templates/__tests__/listBlock.test.ts +64 -0
- package/src/doc/templates/listBlock.ts +88 -30
- package/src/schemas/Doc.ts +22 -1
- package/src/schemas/PageStyle.ts +213 -0
- package/src/schemas/Theme.ts +7 -0
- package/src/schemas/index.ts +2 -0
- package/src/schemas/pageStyleDefaults.ts +204 -0
- package/src/schemas/themeCompile.ts +7 -0
- package/src/schemas/themeValidator.ts +183 -0
- package/src/schemas/themes/bold.json +29 -0
- package/src/schemas/themes/cinematic.json +32 -0
- package/src/schemas/themes/documentary.json +29 -0
- package/src/schemas/themes/gezellig.json +31 -1
- package/src/schemas/themes/magazine.json +37 -0
- package/src/schemas/themes/minimalist.json +29 -0
- package/src/schemas/themes/morning-light.json +32 -0
- package/src/schemas/themes/standard-dark.json +24 -0
- package/src/schemas/themes/standard.json +24 -0
- package/src/schemas/themes/tech-dark.json +42 -0
- package/src/schemas/themes/warm-earth.json +24 -0
- package/src/transform/registry.ts +19 -0
- package/src/transform/styles/dataDriven.ts +1 -0
- package/src/transform/styles/documentary.ts +1 -0
- package/src/transform/styles/magazine.ts +1 -0
- package/src/transform/styles/minimal.ts +1 -0
- package/src/transform/styles/narrative.ts +1 -0
- package/src/transform/types.ts +7 -0
- package/dist/chunk-6PJRB2CF.js.map +0 -1
- package/dist/chunk-FVJUUTEM.js +0 -74
- package/dist/chunk-FVJUUTEM.js.map +0 -1
- package/dist/chunk-GF3BVNLU.js.map +0 -1
- package/dist/chunk-LYNJEOAC.js.map +0 -1
- package/dist/chunk-PXLVXTLW.js.map +0 -1
- package/dist/chunk-QVANWDPH.js.map +0 -1
- /package/dist/{chunk-QTSJOY54.js.map → chunk-VYFRI7R6.js.map} +0 -0
- /package/dist/{chunk-T3TI7GSZ.js.map → chunk-XNLB57UD.js.map} +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createTemplateContext } from '../../../schemas/BlockTemplates.js';
|
|
3
|
+
import type { TextLayer } from '../../../schemas/Doc.js';
|
|
4
|
+
import { DEFAULT_THEME } from '../../../schemas/themeLibrary.js';
|
|
5
|
+
import { VIEWPORT_PRESETS } from '../../../schemas/Viewport.js';
|
|
6
|
+
import { listBlock } from '../listBlock.js';
|
|
7
|
+
|
|
8
|
+
const ITEMS = [
|
|
9
|
+
'We launched the scripting platform for Minecraft, which was essential for launching new categories of content for Minecraft (add-ons)',
|
|
10
|
+
"I spec'ed and largely built the Minecraft Creator Tools suite, including a website for beginning to advanced creators and set of NPM commands and tools.",
|
|
11
|
+
'I manage and craft the Minecraft samples and documentation suite',
|
|
12
|
+
'I produce and create the Minecraft Creator Channel on YouTube',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
describe('listBlock', () => {
|
|
16
|
+
it('reserves vertical space for wrapped list items', () => {
|
|
17
|
+
const context = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
18
|
+
const layers = listBlock(
|
|
19
|
+
{
|
|
20
|
+
template: 'list',
|
|
21
|
+
id: 'minecraft-platform',
|
|
22
|
+
duration: 10,
|
|
23
|
+
audioSegment: 0,
|
|
24
|
+
title: 'Minecraft Platform',
|
|
25
|
+
items: ITEMS,
|
|
26
|
+
},
|
|
27
|
+
context,
|
|
28
|
+
);
|
|
29
|
+
const itemLayers = layers.filter(
|
|
30
|
+
(layer): layer is TextLayer => layer.type === 'text' && /^item-\d+$/.test(layer.id),
|
|
31
|
+
);
|
|
32
|
+
const markerLayers = layers.filter(
|
|
33
|
+
(layer): layer is TextLayer => layer.type === 'text' && /^item-\d+-marker$/.test(layer.id),
|
|
34
|
+
);
|
|
35
|
+
const yPositions = itemLayers.map((layer) => parseFloat(String(layer.position.y)));
|
|
36
|
+
|
|
37
|
+
expect(itemLayers).toHaveLength(4);
|
|
38
|
+
expect(markerLayers).toHaveLength(4);
|
|
39
|
+
expect(yPositions[0]).toBe(34);
|
|
40
|
+
|
|
41
|
+
// The marker has its own right-aligned column, while all body lines are
|
|
42
|
+
// rendered by one layer beginning after that column (a hanging indent).
|
|
43
|
+
expect(markerLayers[0]!.content.text).toBe('1.');
|
|
44
|
+
expect(markerLayers[0]!.content.style.textAlign).toBe('right');
|
|
45
|
+
expect(itemLayers[0]!.content.text).toBe(ITEMS[0]);
|
|
46
|
+
const markerRightPx =
|
|
47
|
+
(parseFloat(String(markerLayers[0]!.position.x)) / 100) * context.viewport.width +
|
|
48
|
+
Number(markerLayers[0]!.position.width);
|
|
49
|
+
const bodyLeftPx =
|
|
50
|
+
(parseFloat(String(itemLayers[0]!.position.x)) / 100) * context.viewport.width;
|
|
51
|
+
expect(bodyLeftPx).toBeGreaterThan(markerRightPx);
|
|
52
|
+
|
|
53
|
+
// The first two entries wrap to two lines. Their following baselines
|
|
54
|
+
// therefore advance by two line-heights plus the inter-item gap.
|
|
55
|
+
const fontSize = itemLayers[0]!.content.style.fontSize;
|
|
56
|
+
const twoLineAdvance = ((2 * fontSize * 1.2 + 18) / context.viewport.height) * 100;
|
|
57
|
+
expect(yPositions[1]! - yPositions[0]!).toBeCloseTo(twoLineAdvance);
|
|
58
|
+
expect(yPositions[2]! - yPositions[1]!).toBeCloseTo(twoLineAdvance);
|
|
59
|
+
|
|
60
|
+
// The third entry fits on one line, so the final advance is compact.
|
|
61
|
+
const oneLineAdvance = ((fontSize * 1.2 + 18) / context.viewport.height) * 100;
|
|
62
|
+
expect(yPositions[3]! - yPositions[2]!).toBeCloseTo(oneLineAdvance);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -19,6 +19,43 @@ import {
|
|
|
19
19
|
} from '../utils/themeUtils.js';
|
|
20
20
|
import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
|
|
21
21
|
|
|
22
|
+
const LIST_ITEM_LINE_HEIGHT = 1.2;
|
|
23
|
+
const LIST_ITEM_GAP_PX = 18;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Mirror the renderer's character-based wrapping closely enough to reserve
|
|
27
|
+
* vertical space for each independently positioned text layer.
|
|
28
|
+
*/
|
|
29
|
+
function estimateWrappedLineCount(text: string, fontSize: number, maxWidth: number): number {
|
|
30
|
+
if (!text.trim()) return 1;
|
|
31
|
+
|
|
32
|
+
const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
|
|
33
|
+
if (charsPerLine <= 0) return 1;
|
|
34
|
+
|
|
35
|
+
let lineCount = 0;
|
|
36
|
+
let currentLineLength = 0;
|
|
37
|
+
|
|
38
|
+
for (const word of text.split(/\s+/)) {
|
|
39
|
+
const testLineLength = currentLineLength ? currentLineLength + 1 + word.length : word.length;
|
|
40
|
+
|
|
41
|
+
if (testLineLength <= charsPerLine) {
|
|
42
|
+
currentLineLength = testLineLength;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (currentLineLength) lineCount += 1;
|
|
47
|
+
|
|
48
|
+
let remainingLength = word.length;
|
|
49
|
+
while (remainingLength > charsPerLine) {
|
|
50
|
+
lineCount += 1;
|
|
51
|
+
remainingLength -= charsPerLine;
|
|
52
|
+
}
|
|
53
|
+
currentLineLength = remainingLength;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Math.max(1, lineCount + (currentLineLength ? 1 : 0));
|
|
57
|
+
}
|
|
58
|
+
|
|
22
59
|
export function listBlock(input: ListBlockInput, context: TemplateContext): Layer[] {
|
|
23
60
|
const { title, accentImage } = input;
|
|
24
61
|
// `items` is required by the schema, but malformed / partially-authored
|
|
@@ -66,8 +103,9 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
|
|
|
66
103
|
// defensive against future edits to the accent-layout types).
|
|
67
104
|
const centerX = parseFloat(accentLayout.textCenterX);
|
|
68
105
|
const widthPct = parseFloat(accentLayout.textWidth);
|
|
69
|
-
const
|
|
70
|
-
Number.isFinite(centerX) && Number.isFinite(widthPct) ?
|
|
106
|
+
const leftPct =
|
|
107
|
+
Number.isFinite(centerX) && Number.isFinite(widthPct) ? centerX - widthPct / 2 : 8;
|
|
108
|
+
const leftX = `${leftPct}%`;
|
|
71
109
|
|
|
72
110
|
// Title if provided
|
|
73
111
|
const startY = title ? 34 : 26;
|
|
@@ -95,28 +133,49 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
|
|
|
95
133
|
});
|
|
96
134
|
}
|
|
97
135
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const LIST_ITEM_BASE_PX = 34;
|
|
110
|
-
const LIST_ITEM_LINE_HEIGHT = 1.2;
|
|
111
|
-
const LIST_ITEM_GAP_PX = 18;
|
|
112
|
-
const DESIGN_HEIGHT_PX = 1080;
|
|
113
|
-
const spacing =
|
|
114
|
-
((LIST_ITEM_BASE_PX * LIST_ITEM_LINE_HEIGHT + LIST_ITEM_GAP_PX) / DESIGN_HEIGHT_PX) * 100;
|
|
136
|
+
// Render the number and body in separate columns. The body is therefore a
|
|
137
|
+
// single text layer whose wrapped lines all share the same x coordinate,
|
|
138
|
+
// producing a hanging indent instead of wrapping underneath the number.
|
|
139
|
+
const textWidthPx = (Number.isFinite(widthPct) ? widthPct / 100 : 0.85) * context.viewport.width;
|
|
140
|
+
const markerText = `${items.length}.`;
|
|
141
|
+
const markerWidthPx = Math.max(itemFontSize, markerText.length * itemFontSize * 0.5);
|
|
142
|
+
const markerGapPx = itemFontSize * 0.35;
|
|
143
|
+
const bodyIndentPx = markerWidthPx + markerGapPx;
|
|
144
|
+
const bodyLeftX = `${leftPct + (bodyIndentPx / context.viewport.width) * 100}%`;
|
|
145
|
+
const bodyWidthPx = Math.max(itemFontSize, textWidthPx - bodyIndentPx);
|
|
146
|
+
let itemY = startY;
|
|
115
147
|
|
|
116
148
|
// List items with staggered animation
|
|
117
149
|
for (let i = 0; i < items.length; i++) {
|
|
118
|
-
const
|
|
119
|
-
const
|
|
150
|
+
const itemText = items[i]!;
|
|
151
|
+
const lineCount = estimateWrappedLineCount(itemText, itemFontSize, bodyWidthPx);
|
|
152
|
+
const animation = themedEntrance(context, 'text', {
|
|
153
|
+
type: 'fadeIn',
|
|
154
|
+
duration: 0.8,
|
|
155
|
+
delay: 0.3 + 0.3 * i,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
layers.push({
|
|
159
|
+
type: 'text',
|
|
160
|
+
id: `item-${i}-marker`,
|
|
161
|
+
content: {
|
|
162
|
+
text: `${i + 1}.`,
|
|
163
|
+
style: {
|
|
164
|
+
fontSize: itemFontSize,
|
|
165
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
166
|
+
color: theme.colors.text,
|
|
167
|
+
textAlign: 'right',
|
|
168
|
+
lineHeight: LIST_ITEM_LINE_HEIGHT,
|
|
169
|
+
shadow: shouldUseShadow(context),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
position: {
|
|
173
|
+
x: leftX,
|
|
174
|
+
y: adjustY(`${itemY}%`, accentLayout),
|
|
175
|
+
width: markerWidthPx,
|
|
176
|
+
},
|
|
177
|
+
animation,
|
|
178
|
+
});
|
|
120
179
|
|
|
121
180
|
layers.push({
|
|
122
181
|
type: 'text',
|
|
@@ -128,21 +187,20 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
|
|
|
128
187
|
fontFamily: getThemeFont(context, 'body'),
|
|
129
188
|
color: theme.colors.text,
|
|
130
189
|
textAlign: 'left',
|
|
131
|
-
lineHeight:
|
|
190
|
+
lineHeight: LIST_ITEM_LINE_HEIGHT,
|
|
132
191
|
shadow: shouldUseShadow(context),
|
|
133
192
|
},
|
|
134
193
|
},
|
|
135
194
|
position: {
|
|
136
|
-
x:
|
|
137
|
-
y: adjustY(`${
|
|
138
|
-
width:
|
|
195
|
+
x: bodyLeftX,
|
|
196
|
+
y: adjustY(`${itemY}%`, accentLayout),
|
|
197
|
+
width: bodyWidthPx,
|
|
139
198
|
},
|
|
140
|
-
animation
|
|
141
|
-
type: 'fadeIn',
|
|
142
|
-
duration: 0.8,
|
|
143
|
-
delay: 0.3 + 0.3 * i,
|
|
144
|
-
}),
|
|
199
|
+
animation,
|
|
145
200
|
});
|
|
201
|
+
|
|
202
|
+
const itemHeightPx = lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT;
|
|
203
|
+
itemY += ((itemHeightPx + LIST_ITEM_GAP_PX) / context.viewport.height) * 100;
|
|
146
204
|
}
|
|
147
205
|
|
|
148
206
|
return layers;
|
package/src/schemas/Doc.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Style — the theme's art-direction layer for Page (linear) mode.
|
|
3
|
+
*
|
|
4
|
+
* Slides render through fixed-viewport SVG templates; pages render through
|
|
5
|
+
* a small closed vocabulary of variable-height HTML section layouts
|
|
6
|
+
* (`PageSectionKind`). A `ThemePageStyle` is how a theme art-directs that
|
|
7
|
+
* page rendition: it picks a design family, sets page-wide tokens
|
|
8
|
+
* (column widths, spacing, dividers, hero treatment, image framing …),
|
|
9
|
+
* and can override how individual section kinds or block templates map
|
|
10
|
+
* onto the section vocabulary.
|
|
11
|
+
*
|
|
12
|
+
* Design principles (mirrors the rest of the Theme schema):
|
|
13
|
+
* - Fully JSON-serializable — rides the `squisq-custom-themes` frontmatter
|
|
14
|
+
* codec unchanged.
|
|
15
|
+
* - Closed enums everywhere so renderers can switch exhaustively and the
|
|
16
|
+
* validator can reject typos.
|
|
17
|
+
* - Optional on `Theme`; `resolvePageStyle`/`defaultPageStyle` derive a
|
|
18
|
+
* complete style from existing theme fields when absent, so legacy and
|
|
19
|
+
* customizer themes keep working without edits.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// Section vocabulary
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The closed set of page section layouts. Every block template maps onto
|
|
28
|
+
* one of these kinds (see `doc/page/sectionExtractors.ts`); themes may
|
|
29
|
+
* remap via `ThemePageStyle.sections` / `.templates`.
|
|
30
|
+
*/
|
|
31
|
+
export const PAGE_SECTION_KINDS = [
|
|
32
|
+
'hero',
|
|
33
|
+
'banner',
|
|
34
|
+
'stat-band',
|
|
35
|
+
'quote-band',
|
|
36
|
+
'feature-split',
|
|
37
|
+
'media-figure',
|
|
38
|
+
'gallery',
|
|
39
|
+
'callout',
|
|
40
|
+
'card-grid',
|
|
41
|
+
'item-list',
|
|
42
|
+
'timeline-rail',
|
|
43
|
+
'table-section',
|
|
44
|
+
'canvas-embed',
|
|
45
|
+
'prose',
|
|
46
|
+
'footer',
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export type PageSectionKind = (typeof PAGE_SECTION_KINDS)[number];
|
|
50
|
+
|
|
51
|
+
/** Relative visual weight of a section within the page. */
|
|
52
|
+
export type PageEmphasis = 'lead' | 'strong' | 'standard' | 'quiet';
|
|
53
|
+
|
|
54
|
+
/** Which background treatment a section sits on. */
|
|
55
|
+
export type PageBackground = 'base' | 'alternate' | 'accent' | 'media';
|
|
56
|
+
|
|
57
|
+
// ============================================
|
|
58
|
+
// Design families + tokens
|
|
59
|
+
// ============================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Broad page design personalities. A family selects the structural
|
|
63
|
+
* skeleton (how sections compose); tokens parameterize it per theme.
|
|
64
|
+
*/
|
|
65
|
+
export const PAGE_DESIGN_FAMILIES = [
|
|
66
|
+
'clean',
|
|
67
|
+
'editorial',
|
|
68
|
+
'brutalist',
|
|
69
|
+
'terminal',
|
|
70
|
+
'cinematic',
|
|
71
|
+
'documentary',
|
|
72
|
+
'organic',
|
|
73
|
+
'soft',
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
export type PageDesignFamily = (typeof PAGE_DESIGN_FAMILIES)[number];
|
|
77
|
+
|
|
78
|
+
/** How headings announce themselves across the page. */
|
|
79
|
+
export interface PageHeadingTreatment {
|
|
80
|
+
/** Small label above headings: none, plain kicker, "01 —" numbering, or a mono tag. */
|
|
81
|
+
eyebrow: 'none' | 'kicker' | 'numbered' | 'mono-tag';
|
|
82
|
+
/** Overall heading size register. */
|
|
83
|
+
scale: 'regular' | 'display' | 'oversized';
|
|
84
|
+
/** Optional case transform for headings/eyebrows. */
|
|
85
|
+
case?: 'none' | 'uppercase';
|
|
86
|
+
/** Optional decoration under headings. */
|
|
87
|
+
underline?: 'none' | 'accent-bar' | 'full-rule';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Page-wide design tokens. Structural CSS reads these via
|
|
92
|
+
* `--squisq-page-*` custom properties (see `doc/pageCss.ts`).
|
|
93
|
+
*/
|
|
94
|
+
export interface PageTokens {
|
|
95
|
+
/** Reading-column max width in px (prose, callouts, quotes). */
|
|
96
|
+
contentMaxWidth: number;
|
|
97
|
+
/** Wide-band inner max width in px (figures, tables, feature splits). */
|
|
98
|
+
wideMaxWidth: number;
|
|
99
|
+
/** Vertical rhythm between sections. */
|
|
100
|
+
sectionSpacing: 'compact' | 'comfortable' | 'generous';
|
|
101
|
+
/** Corner radius for cards, media frames, panels (px). */
|
|
102
|
+
cornerRadius: number;
|
|
103
|
+
/** Divider treatment between sections. */
|
|
104
|
+
divider: 'none' | 'gap-only' | 'hairline' | 'thick-rule' | 'double-rule' | 'dotted';
|
|
105
|
+
/** How section backgrounds vary down the page. */
|
|
106
|
+
backgroundRhythm: 'flat' | 'alternate' | 'accent-bands' | 'tinted-panels';
|
|
107
|
+
/** Hero (cover/title) composition. */
|
|
108
|
+
heroStyle: 'stacked' | 'split' | 'full-bleed' | 'letterbox' | 'oversized-type';
|
|
109
|
+
/** Heading voice across the page. */
|
|
110
|
+
headingTreatment: PageHeadingTreatment;
|
|
111
|
+
/** Frame treatment for photographic media. */
|
|
112
|
+
imageFraming: 'flush' | 'rounded' | 'bordered' | 'polaroid' | 'letterboxed' | 'circle-accent';
|
|
113
|
+
/** Shadow language for elevated elements. */
|
|
114
|
+
shadow: 'none' | 'soft' | 'crisp' | 'heavy';
|
|
115
|
+
/** Decoration on quote sections. */
|
|
116
|
+
quoteMark: 'none' | 'oversized-glyph' | 'accent-bar';
|
|
117
|
+
/** Treatment of large stat numerals. */
|
|
118
|
+
numeralStyle: 'plain' | 'oversized' | 'boxed' | 'mono';
|
|
119
|
+
/** Optional page background pattern. */
|
|
120
|
+
pattern?: 'none' | 'dots' | 'grid' | 'diagonal' | 'noise';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================
|
|
124
|
+
// Overrides + accent rotation
|
|
125
|
+
// ============================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A theme's override for one section kind or one block template —
|
|
129
|
+
* remap the kind, force a variant, adjust emphasis/background, or pass
|
|
130
|
+
* freeform scalar hints the section renderer understands.
|
|
131
|
+
*/
|
|
132
|
+
export interface PageSectionOverride {
|
|
133
|
+
kind?: PageSectionKind;
|
|
134
|
+
variant?: string;
|
|
135
|
+
emphasis?: PageEmphasis;
|
|
136
|
+
background?: PageBackground;
|
|
137
|
+
/** Freeform scalar hints (e.g. `dropCap: true`, `frame: "terminal"`). */
|
|
138
|
+
hints?: Record<string, string | number | boolean>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** How named color schemes rotate across accent-bearing sections. */
|
|
142
|
+
export interface PageAccentRotation {
|
|
143
|
+
strategy: 'primary-only' | 'cycle' | 'alternate-two' | 'none';
|
|
144
|
+
/**
|
|
145
|
+
* ColorScheme names (keys of `theme.colorSchemes`) to rotate through.
|
|
146
|
+
* Defaults to all schemes in insertion order.
|
|
147
|
+
*/
|
|
148
|
+
schemes?: string[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================
|
|
152
|
+
// ThemePageStyle
|
|
153
|
+
// ============================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The complete page art direction for a theme. Optional on `Theme`;
|
|
157
|
+
* `defaultPageStyle(theme)` derives one from existing theme fields.
|
|
158
|
+
*/
|
|
159
|
+
export interface ThemePageStyle {
|
|
160
|
+
family: PageDesignFamily;
|
|
161
|
+
tokens: PageTokens;
|
|
162
|
+
/** Per-section-kind overrides. */
|
|
163
|
+
sections?: Partial<Record<PageSectionKind, PageSectionOverride>>;
|
|
164
|
+
/** Per-template overrides (canonical template ids); win over `sections`. */
|
|
165
|
+
templates?: Record<string, PageSectionOverride>;
|
|
166
|
+
accentRotation: PageAccentRotation;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================
|
|
170
|
+
// Enum sets (shared by the validator + defaults)
|
|
171
|
+
// ============================================
|
|
172
|
+
|
|
173
|
+
export const PAGE_SECTION_SPACINGS = ['compact', 'comfortable', 'generous'] as const;
|
|
174
|
+
export const PAGE_DIVIDERS = [
|
|
175
|
+
'none',
|
|
176
|
+
'gap-only',
|
|
177
|
+
'hairline',
|
|
178
|
+
'thick-rule',
|
|
179
|
+
'double-rule',
|
|
180
|
+
'dotted',
|
|
181
|
+
] as const;
|
|
182
|
+
export const PAGE_BACKGROUND_RHYTHMS = [
|
|
183
|
+
'flat',
|
|
184
|
+
'alternate',
|
|
185
|
+
'accent-bands',
|
|
186
|
+
'tinted-panels',
|
|
187
|
+
] as const;
|
|
188
|
+
export const PAGE_HERO_STYLES = [
|
|
189
|
+
'stacked',
|
|
190
|
+
'split',
|
|
191
|
+
'full-bleed',
|
|
192
|
+
'letterbox',
|
|
193
|
+
'oversized-type',
|
|
194
|
+
] as const;
|
|
195
|
+
export const PAGE_EYEBROWS = ['none', 'kicker', 'numbered', 'mono-tag'] as const;
|
|
196
|
+
export const PAGE_HEADING_SCALES = ['regular', 'display', 'oversized'] as const;
|
|
197
|
+
export const PAGE_HEADING_CASES = ['none', 'uppercase'] as const;
|
|
198
|
+
export const PAGE_HEADING_UNDERLINES = ['none', 'accent-bar', 'full-rule'] as const;
|
|
199
|
+
export const PAGE_IMAGE_FRAMINGS = [
|
|
200
|
+
'flush',
|
|
201
|
+
'rounded',
|
|
202
|
+
'bordered',
|
|
203
|
+
'polaroid',
|
|
204
|
+
'letterboxed',
|
|
205
|
+
'circle-accent',
|
|
206
|
+
] as const;
|
|
207
|
+
export const PAGE_SHADOWS = ['none', 'soft', 'crisp', 'heavy'] as const;
|
|
208
|
+
export const PAGE_QUOTE_MARKS = ['none', 'oversized-glyph', 'accent-bar'] as const;
|
|
209
|
+
export const PAGE_NUMERAL_STYLES = ['plain', 'oversized', 'boxed', 'mono'] as const;
|
|
210
|
+
export const PAGE_PATTERNS = ['none', 'dots', 'grid', 'diagonal', 'noise'] as const;
|
|
211
|
+
export const PAGE_EMPHASES = ['lead', 'strong', 'standard', 'quiet'] as const;
|
|
212
|
+
export const PAGE_BACKGROUNDS = ['base', 'alternate', 'accent', 'media'] as const;
|
|
213
|
+
export const PAGE_ACCENT_STRATEGIES = ['primary-only', 'cycle', 'alternate-two', 'none'] as const;
|
package/src/schemas/Theme.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import type { LayoutHints } from './LayoutStrategy.js';
|
|
24
24
|
import type { AnimationType, ImageTreatment, TransitionType } from './Doc.js';
|
|
25
25
|
import type { PersistentLayerConfig } from './BlockTemplates.js';
|
|
26
|
+
import type { ThemePageStyle } from './PageStyle.js';
|
|
26
27
|
import { THEME_SCHEMA_VERSION } from './themeConstants.js';
|
|
27
28
|
import { validateTheme } from './themeValidator.js';
|
|
28
29
|
import { cloneAndFreezeData } from '../internal/immutable.js';
|
|
@@ -236,6 +237,12 @@ export interface Theme {
|
|
|
236
237
|
colorSchemes: Record<string, ThemeColorScheme>;
|
|
237
238
|
/** Optional persistent layers baked into the theme */
|
|
238
239
|
persistentLayers?: PersistentLayerConfig;
|
|
240
|
+
/**
|
|
241
|
+
* Optional page art direction for Page (linear) mode. When absent,
|
|
242
|
+
* `resolvePageStyle`/`defaultPageStyle` derive one from the fields above,
|
|
243
|
+
* so legacy themes render pages without declaring this.
|
|
244
|
+
*/
|
|
245
|
+
pageStyle?: ThemePageStyle;
|
|
239
246
|
}
|
|
240
247
|
|
|
241
248
|
// ============================================
|
package/src/schemas/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export * from './Media.js';
|
|
|
5
5
|
export * from './BlockTemplates.js';
|
|
6
6
|
export * from './CustomTemplates.js';
|
|
7
7
|
export * from './Theme.js';
|
|
8
|
+
export * from './PageStyle.js';
|
|
9
|
+
export * from './pageStyleDefaults.js';
|
|
8
10
|
export * from './themeLibrary.js';
|
|
9
11
|
export * from './themeValidator.js';
|
|
10
12
|
export * from './themeCompile.js';
|