@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.
Files changed (85) 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-6PJRB2CF.js → chunk-JHZWYRSW.js} +570 -2
  6. package/dist/chunk-JHZWYRSW.js.map +1 -0
  7. package/dist/{chunk-GF3BVNLU.js → chunk-JI77MGAK.js} +195 -27
  8. package/dist/chunk-JI77MGAK.js.map +1 -0
  9. package/dist/{chunk-QVANWDPH.js → chunk-OTKVFVL6.js} +1403 -41
  10. package/dist/chunk-OTKVFVL6.js.map +1 -0
  11. package/dist/{chunk-LYNJEOAC.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-QTSJOY54.js → chunk-VYFRI7R6.js} +2 -2
  16. package/dist/{chunk-T3TI7GSZ.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__/customThemesFrontmatter.test.ts +1 -0
  35. package/src/__tests__/markdown.test.ts +13 -4
  36. package/src/__tests__/markdownSanitize.test.ts +7 -5
  37. package/src/__tests__/materializeBlockLayers.test.ts +32 -0
  38. package/src/__tests__/materializePageSections.test.ts +404 -0
  39. package/src/__tests__/pageStyle.test.ts +258 -0
  40. package/src/__tests__/templateAnnotationParse.test.ts +13 -0
  41. package/src/doc/index.ts +34 -0
  42. package/src/doc/markdownToDoc.ts +3 -1
  43. package/src/doc/materializeBlockLayers.ts +170 -9
  44. package/src/doc/page/PageSection.ts +174 -0
  45. package/src/doc/page/materializePageSection.ts +616 -0
  46. package/src/doc/page/resolvePageBlock.ts +108 -0
  47. package/src/doc/page/sectionExtractors.ts +466 -0
  48. package/src/doc/pageCss.ts +628 -0
  49. package/src/doc/templateInputs.ts +1 -0
  50. package/src/doc/templates/__tests__/listBlock.test.ts +64 -0
  51. package/src/doc/templates/listBlock.ts +88 -30
  52. package/src/schemas/Doc.ts +22 -1
  53. package/src/schemas/PageStyle.ts +213 -0
  54. package/src/schemas/Theme.ts +7 -0
  55. package/src/schemas/index.ts +2 -0
  56. package/src/schemas/pageStyleDefaults.ts +204 -0
  57. package/src/schemas/themeCompile.ts +7 -0
  58. package/src/schemas/themeValidator.ts +183 -0
  59. package/src/schemas/themes/bold.json +29 -0
  60. package/src/schemas/themes/cinematic.json +32 -0
  61. package/src/schemas/themes/documentary.json +29 -0
  62. package/src/schemas/themes/gezellig.json +31 -1
  63. package/src/schemas/themes/magazine.json +37 -0
  64. package/src/schemas/themes/minimalist.json +29 -0
  65. package/src/schemas/themes/morning-light.json +32 -0
  66. package/src/schemas/themes/standard-dark.json +24 -0
  67. package/src/schemas/themes/standard.json +24 -0
  68. package/src/schemas/themes/tech-dark.json +42 -0
  69. package/src/schemas/themes/warm-earth.json +24 -0
  70. package/src/transform/registry.ts +19 -0
  71. package/src/transform/styles/dataDriven.ts +1 -0
  72. package/src/transform/styles/documentary.ts +1 -0
  73. package/src/transform/styles/magazine.ts +1 -0
  74. package/src/transform/styles/minimal.ts +1 -0
  75. package/src/transform/styles/narrative.ts +1 -0
  76. package/src/transform/types.ts +7 -0
  77. package/dist/chunk-6PJRB2CF.js.map +0 -1
  78. package/dist/chunk-FVJUUTEM.js +0 -74
  79. package/dist/chunk-FVJUUTEM.js.map +0 -1
  80. package/dist/chunk-GF3BVNLU.js.map +0 -1
  81. package/dist/chunk-LYNJEOAC.js.map +0 -1
  82. package/dist/chunk-PXLVXTLW.js.map +0 -1
  83. package/dist/chunk-QVANWDPH.js.map +0 -1
  84. /package/dist/{chunk-QTSJOY54.js.map → chunk-VYFRI7R6.js.map} +0 -0
  85. /package/dist/{chunk-T3TI7GSZ.js.map → chunk-XNLB57UD.js.map} +0 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Page Style Defaults
3
+ *
4
+ * Derives a complete `ThemePageStyle` from a theme's existing fields so
5
+ * legacy themes, customizer themes, and file-inferred themes get a
6
+ * coherent page art direction without declaring one. Built-in themes
7
+ * ship explicit `pageStyle` blocks in their JSON; this derivation is the
8
+ * fallback used by `compileTheme` and by `resolvePageStyle` at read time.
9
+ *
10
+ * Derivation signals:
11
+ * - `renderStyle.name` → design family (documentary → documentary,
12
+ * magazine → editorial, bold → brutalist, tech-dark → terminal, …)
13
+ * - `style.borderRadius` → cornerRadius + rounded vs flush image framing
14
+ * - `style.imageTreatment` (mono/duotone) → letterboxed framing
15
+ * - `style.textShadow` → shadow language
16
+ * - `persistentLayers` patternBackground → page pattern
17
+ */
18
+
19
+ import type { Theme } from './Theme.js';
20
+ import type {
21
+ PageDesignFamily,
22
+ PageTokens,
23
+ ThemePageStyle,
24
+ PageHeadingTreatment,
25
+ } from './PageStyle.js';
26
+
27
+ /** renderStyle.name → design family. Unknown names fall back to 'clean'. */
28
+ const FAMILY_BY_RENDER_STYLE: Record<string, PageDesignFamily> = {
29
+ standard: 'clean',
30
+ minimalist: 'clean',
31
+ documentary: 'documentary',
32
+ magazine: 'editorial',
33
+ bold: 'brutalist',
34
+ 'tech-dark': 'terminal',
35
+ cinematic: 'cinematic',
36
+ 'warm-earth': 'organic',
37
+ 'morning-light': 'soft',
38
+ gezellig: 'soft',
39
+ };
40
+
41
+ interface FamilyDefaults {
42
+ sectionSpacing: PageTokens['sectionSpacing'];
43
+ divider: PageTokens['divider'];
44
+ backgroundRhythm: PageTokens['backgroundRhythm'];
45
+ heroStyle: PageTokens['heroStyle'];
46
+ headingTreatment: PageHeadingTreatment;
47
+ quoteMark: PageTokens['quoteMark'];
48
+ numeralStyle: PageTokens['numeralStyle'];
49
+ accentStrategy: ThemePageStyle['accentRotation']['strategy'];
50
+ }
51
+
52
+ const FAMILY_DEFAULTS: Record<PageDesignFamily, FamilyDefaults> = {
53
+ clean: {
54
+ sectionSpacing: 'comfortable',
55
+ divider: 'hairline',
56
+ backgroundRhythm: 'alternate',
57
+ heroStyle: 'stacked',
58
+ headingTreatment: { eyebrow: 'kicker', scale: 'regular', underline: 'none' },
59
+ quoteMark: 'accent-bar',
60
+ numeralStyle: 'plain',
61
+ accentStrategy: 'cycle',
62
+ },
63
+ editorial: {
64
+ sectionSpacing: 'comfortable',
65
+ divider: 'double-rule',
66
+ backgroundRhythm: 'flat',
67
+ heroStyle: 'split',
68
+ headingTreatment: { eyebrow: 'kicker', scale: 'display', underline: 'none' },
69
+ quoteMark: 'oversized-glyph',
70
+ numeralStyle: 'oversized',
71
+ accentStrategy: 'alternate-two',
72
+ },
73
+ brutalist: {
74
+ sectionSpacing: 'compact',
75
+ divider: 'thick-rule',
76
+ backgroundRhythm: 'accent-bands',
77
+ heroStyle: 'oversized-type',
78
+ headingTreatment: { eyebrow: 'kicker', scale: 'oversized', case: 'uppercase' },
79
+ quoteMark: 'none',
80
+ numeralStyle: 'oversized',
81
+ accentStrategy: 'cycle',
82
+ },
83
+ terminal: {
84
+ sectionSpacing: 'compact',
85
+ divider: 'hairline',
86
+ backgroundRhythm: 'tinted-panels',
87
+ heroStyle: 'stacked',
88
+ headingTreatment: { eyebrow: 'mono-tag', scale: 'regular', underline: 'none' },
89
+ quoteMark: 'none',
90
+ numeralStyle: 'mono',
91
+ accentStrategy: 'primary-only',
92
+ },
93
+ cinematic: {
94
+ sectionSpacing: 'generous',
95
+ divider: 'gap-only',
96
+ backgroundRhythm: 'flat',
97
+ heroStyle: 'full-bleed',
98
+ headingTreatment: { eyebrow: 'kicker', scale: 'display', case: 'uppercase' },
99
+ quoteMark: 'none',
100
+ numeralStyle: 'oversized',
101
+ accentStrategy: 'primary-only',
102
+ },
103
+ documentary: {
104
+ sectionSpacing: 'generous',
105
+ divider: 'thick-rule',
106
+ backgroundRhythm: 'flat',
107
+ heroStyle: 'letterbox',
108
+ headingTreatment: { eyebrow: 'numbered', scale: 'display', underline: 'none' },
109
+ quoteMark: 'accent-bar',
110
+ numeralStyle: 'boxed',
111
+ accentStrategy: 'primary-only',
112
+ },
113
+ organic: {
114
+ sectionSpacing: 'comfortable',
115
+ divider: 'thick-rule',
116
+ backgroundRhythm: 'tinted-panels',
117
+ heroStyle: 'stacked',
118
+ headingTreatment: { eyebrow: 'kicker', scale: 'regular', underline: 'accent-bar' },
119
+ quoteMark: 'oversized-glyph',
120
+ numeralStyle: 'boxed',
121
+ accentStrategy: 'alternate-two',
122
+ },
123
+ soft: {
124
+ sectionSpacing: 'comfortable',
125
+ divider: 'dotted',
126
+ backgroundRhythm: 'tinted-panels',
127
+ heroStyle: 'split',
128
+ headingTreatment: { eyebrow: 'kicker', scale: 'regular', underline: 'none' },
129
+ quoteMark: 'oversized-glyph',
130
+ numeralStyle: 'plain',
131
+ accentStrategy: 'alternate-two',
132
+ },
133
+ };
134
+
135
+ /** Read the first patternBackground pattern from a theme's persistent layers. */
136
+ function derivePattern(theme: Theme): PageTokens['pattern'] {
137
+ const layers = [
138
+ ...(theme.persistentLayers?.bottomLayers ?? []),
139
+ ...(theme.persistentLayers?.topLayers ?? []),
140
+ ];
141
+ for (const layer of layers) {
142
+ if (
143
+ typeof layer === 'object' &&
144
+ layer !== null &&
145
+ 'template' in layer &&
146
+ (layer as { template?: unknown }).template === 'patternBackground'
147
+ ) {
148
+ const config = (layer as { config?: { pattern?: unknown } }).config;
149
+ const pattern = config?.pattern;
150
+ if (
151
+ pattern === 'dots' ||
152
+ pattern === 'grid' ||
153
+ pattern === 'diagonal' ||
154
+ pattern === 'noise'
155
+ ) {
156
+ return pattern;
157
+ }
158
+ }
159
+ }
160
+ return undefined;
161
+ }
162
+
163
+ /**
164
+ * Derive a complete `ThemePageStyle` from a theme's existing fields.
165
+ * Deterministic and read-only; explicit `theme.pageStyle` always wins
166
+ * (callers check before deriving — see `resolvePageStyle`).
167
+ */
168
+ export function defaultPageStyle(theme: Theme): ThemePageStyle {
169
+ const family = FAMILY_BY_RENDER_STYLE[theme.renderStyle?.name ?? ''] ?? 'clean';
170
+ const defaults = FAMILY_DEFAULTS[family];
171
+
172
+ const cornerRadius = theme.style?.borderRadius ?? 6;
173
+ const treatmentType = theme.style?.imageTreatment?.type;
174
+ const imageFraming: PageTokens['imageFraming'] =
175
+ treatmentType === 'mono' || treatmentType === 'duotone'
176
+ ? 'letterboxed'
177
+ : cornerRadius > 0
178
+ ? 'rounded'
179
+ : 'flush';
180
+
181
+ const tokens: PageTokens = {
182
+ contentMaxWidth: 760,
183
+ wideMaxWidth: 1100,
184
+ sectionSpacing: defaults.sectionSpacing,
185
+ cornerRadius,
186
+ divider: defaults.divider,
187
+ backgroundRhythm: defaults.backgroundRhythm,
188
+ heroStyle: defaults.heroStyle,
189
+ headingTreatment: { ...defaults.headingTreatment },
190
+ imageFraming,
191
+ shadow: theme.style?.textShadow ? 'soft' : 'none',
192
+ quoteMark: defaults.quoteMark,
193
+ numeralStyle: defaults.numeralStyle,
194
+ };
195
+
196
+ const pattern = derivePattern(theme);
197
+ if (pattern) tokens.pattern = pattern;
198
+
199
+ return {
200
+ family,
201
+ tokens,
202
+ accentRotation: { strategy: defaults.accentStrategy },
203
+ };
204
+ }
@@ -22,6 +22,7 @@ import type {
22
22
  } from './Theme.js';
23
23
  import { THEME_SCHEMA_VERSION, createTheme } from './Theme.js';
24
24
  import { assertTheme } from './themeValidator.js';
25
+ import { defaultPageStyle } from './pageStyleDefaults.js';
25
26
  import {
26
27
  oklchLighten,
27
28
  oklchDarken,
@@ -210,6 +211,12 @@ export function compileTheme(partial: DeepPartial<Theme>, opts: CompileOptions =
210
211
  });
211
212
  }
212
213
 
214
+ // Step 2b: fill page art direction from existing fields when absent, so
215
+ // every compiled theme carries an explicit, serializable pageStyle.
216
+ if (!merged.pageStyle) {
217
+ merged.pageStyle = defaultPageStyle(merged);
218
+ }
219
+
213
220
  // Step 3: validate
214
221
  return assertTheme(merged, `compiled theme "${merged.id}"`);
215
222
  }
@@ -14,6 +14,26 @@ import type { Theme, ThemeColorPalette } from './Theme.js';
14
14
  import { THEME_SCHEMA_VERSION } from './themeConstants.js';
15
15
  import { isHex } from './colorUtils.js';
16
16
  import { isTransitionType } from './Transitions.js';
17
+ import {
18
+ PAGE_SECTION_KINDS,
19
+ PAGE_DESIGN_FAMILIES,
20
+ PAGE_SECTION_SPACINGS,
21
+ PAGE_DIVIDERS,
22
+ PAGE_BACKGROUND_RHYTHMS,
23
+ PAGE_HERO_STYLES,
24
+ PAGE_EYEBROWS,
25
+ PAGE_HEADING_SCALES,
26
+ PAGE_HEADING_CASES,
27
+ PAGE_HEADING_UNDERLINES,
28
+ PAGE_IMAGE_FRAMINGS,
29
+ PAGE_SHADOWS,
30
+ PAGE_QUOTE_MARKS,
31
+ PAGE_NUMERAL_STYLES,
32
+ PAGE_PATTERNS,
33
+ PAGE_EMPHASES,
34
+ PAGE_BACKGROUNDS,
35
+ PAGE_ACCENT_STRATEGIES,
36
+ } from './PageStyle.js';
17
37
 
18
38
  export interface ValidationError {
19
39
  path: string;
@@ -28,6 +48,28 @@ export interface ValidationResult {
28
48
  }
29
49
 
30
50
  const VALID_FALLBACK = new Set(['serif', 'sans-serif', 'monospace', 'system-ui']);
51
+ const VALID_SECTION_KINDS = new Set<string>(PAGE_SECTION_KINDS);
52
+ const VALID_DESIGN_FAMILIES = new Set<string>(PAGE_DESIGN_FAMILIES);
53
+ const VALID_PAGE_EMPHASES = new Set<string>(PAGE_EMPHASES);
54
+ const VALID_PAGE_BACKGROUNDS = new Set<string>(PAGE_BACKGROUNDS);
55
+ const VALID_ACCENT_STRATEGIES = new Set<string>(PAGE_ACCENT_STRATEGIES);
56
+ /** PageTokens enum fields → their allowed values. */
57
+ const PAGE_TOKEN_ENUMS: Record<string, Set<string>> = {
58
+ sectionSpacing: new Set<string>(PAGE_SECTION_SPACINGS),
59
+ divider: new Set<string>(PAGE_DIVIDERS),
60
+ backgroundRhythm: new Set<string>(PAGE_BACKGROUND_RHYTHMS),
61
+ heroStyle: new Set<string>(PAGE_HERO_STYLES),
62
+ imageFraming: new Set<string>(PAGE_IMAGE_FRAMINGS),
63
+ shadow: new Set<string>(PAGE_SHADOWS),
64
+ quoteMark: new Set<string>(PAGE_QUOTE_MARKS),
65
+ numeralStyle: new Set<string>(PAGE_NUMERAL_STYLES),
66
+ };
67
+ const HEADING_TREATMENT_ENUMS: Record<string, { values: Set<string>; required: boolean }> = {
68
+ eyebrow: { values: new Set<string>(PAGE_EYEBROWS), required: true },
69
+ scale: { values: new Set<string>(PAGE_HEADING_SCALES), required: true },
70
+ case: { values: new Set<string>(PAGE_HEADING_CASES), required: false },
71
+ underline: { values: new Set<string>(PAGE_HEADING_UNDERLINES), required: false },
72
+ };
31
73
  const VALID_TITLE_WEIGHT = new Set(['normal', 'bold']);
32
74
  const VALID_GRADIENT_PRESETS = new Set([
33
75
  'dark-vignette',
@@ -340,6 +382,143 @@ class V {
340
382
  arr.forEach((entry, i) => this.persistentLayer(`${path}.${side}[${i}]`, entry));
341
383
  }
342
384
  }
385
+
386
+ scalarHints(path: string, v: unknown): void {
387
+ if (!this.isObject(v)) {
388
+ this.err(path, 'expected object');
389
+ return;
390
+ }
391
+ for (const key of Object.keys(v)) {
392
+ const t = typeof v[key];
393
+ if (t !== 'string' && t !== 'number' && t !== 'boolean') {
394
+ this.err(`${path}.${key}`, `expected string|number|boolean, got ${t}`);
395
+ }
396
+ }
397
+ }
398
+
399
+ pageSectionOverride(path: string, v: unknown): void {
400
+ if (!this.isObject(v)) {
401
+ this.err(path, 'expected object');
402
+ return;
403
+ }
404
+ if (v.kind !== undefined && (!this.isString(v.kind) || !VALID_SECTION_KINDS.has(v.kind))) {
405
+ this.err(`${path}.kind`, `expected one of: ${PAGE_SECTION_KINDS.join(', ')}`);
406
+ }
407
+ if (v.variant !== undefined && !this.isString(v.variant)) {
408
+ this.err(`${path}.variant`, 'expected string');
409
+ }
410
+ if (
411
+ v.emphasis !== undefined &&
412
+ (!this.isString(v.emphasis) || !VALID_PAGE_EMPHASES.has(v.emphasis))
413
+ ) {
414
+ this.err(`${path}.emphasis`, `expected one of: ${PAGE_EMPHASES.join(', ')}`);
415
+ }
416
+ if (
417
+ v.background !== undefined &&
418
+ (!this.isString(v.background) || !VALID_PAGE_BACKGROUNDS.has(v.background))
419
+ ) {
420
+ this.err(`${path}.background`, `expected one of: ${PAGE_BACKGROUNDS.join(', ')}`);
421
+ }
422
+ if (v.hints !== undefined) this.scalarHints(`${path}.hints`, v.hints);
423
+ }
424
+
425
+ pageTokens(path: string, v: unknown): void {
426
+ if (!this.isObject(v)) {
427
+ this.err(path, 'expected object');
428
+ return;
429
+ }
430
+ for (const k of ['contentMaxWidth', 'wideMaxWidth', 'cornerRadius']) {
431
+ if (!this.isNumber(v[k])) {
432
+ this.err(`${path}.${k}`, 'expected number');
433
+ }
434
+ }
435
+ for (const [k, values] of Object.entries(PAGE_TOKEN_ENUMS)) {
436
+ if (!this.isString(v[k]) || !values.has(v[k] as string)) {
437
+ this.err(`${path}.${k}`, `expected one of: ${Array.from(values).join(', ')}`);
438
+ }
439
+ }
440
+ if (v.pattern !== undefined) {
441
+ if (!this.isString(v.pattern) || !(PAGE_PATTERNS as readonly string[]).includes(v.pattern)) {
442
+ this.err(`${path}.pattern`, `expected one of: ${PAGE_PATTERNS.join(', ')}`);
443
+ }
444
+ }
445
+ const ht = v.headingTreatment;
446
+ if (!this.isObject(ht)) {
447
+ this.err(`${path}.headingTreatment`, 'expected object');
448
+ return;
449
+ }
450
+ for (const [k, spec] of Object.entries(HEADING_TREATMENT_ENUMS)) {
451
+ const val = ht[k];
452
+ if (val === undefined) {
453
+ if (spec.required) this.err(`${path}.headingTreatment.${k}`, 'required');
454
+ continue;
455
+ }
456
+ if (!this.isString(val) || !spec.values.has(val)) {
457
+ this.err(
458
+ `${path}.headingTreatment.${k}`,
459
+ `expected one of: ${Array.from(spec.values).join(', ')}`,
460
+ );
461
+ }
462
+ }
463
+ }
464
+
465
+ pageStyle(path: string, v: unknown): void {
466
+ if (!this.isObject(v)) {
467
+ this.err(path, 'expected object');
468
+ return;
469
+ }
470
+ if (!this.isString(v.family) || !VALID_DESIGN_FAMILIES.has(v.family)) {
471
+ this.err(`${path}.family`, `expected one of: ${PAGE_DESIGN_FAMILIES.join(', ')}`);
472
+ }
473
+ if (v.tokens === undefined) {
474
+ this.err(`${path}.tokens`, 'required');
475
+ } else {
476
+ this.pageTokens(`${path}.tokens`, v.tokens);
477
+ }
478
+ if (v.sections !== undefined) {
479
+ if (!this.isObject(v.sections)) {
480
+ this.err(`${path}.sections`, 'expected object map');
481
+ } else {
482
+ for (const key of Object.keys(v.sections)) {
483
+ if (!VALID_SECTION_KINDS.has(key)) {
484
+ this.err(`${path}.sections.${key}`, `unknown section kind`);
485
+ continue;
486
+ }
487
+ this.pageSectionOverride(`${path}.sections.${key}`, v.sections[key]);
488
+ }
489
+ }
490
+ }
491
+ if (v.templates !== undefined) {
492
+ if (!this.isObject(v.templates)) {
493
+ this.err(`${path}.templates`, 'expected object map');
494
+ } else {
495
+ for (const key of Object.keys(v.templates)) {
496
+ this.pageSectionOverride(`${path}.templates.${key}`, v.templates[key]);
497
+ }
498
+ }
499
+ }
500
+ if (v.accentRotation === undefined) {
501
+ this.err(`${path}.accentRotation`, 'required');
502
+ } else if (!this.isObject(v.accentRotation)) {
503
+ this.err(`${path}.accentRotation`, 'expected object');
504
+ } else {
505
+ const rotation = v.accentRotation;
506
+ if (!this.isString(rotation.strategy) || !VALID_ACCENT_STRATEGIES.has(rotation.strategy)) {
507
+ this.err(
508
+ `${path}.accentRotation.strategy`,
509
+ `expected one of: ${PAGE_ACCENT_STRATEGIES.join(', ')}`,
510
+ );
511
+ }
512
+ if (rotation.schemes !== undefined) {
513
+ if (
514
+ !Array.isArray(rotation.schemes) ||
515
+ rotation.schemes.some((s) => typeof s !== 'string')
516
+ ) {
517
+ this.err(`${path}.accentRotation.schemes`, 'expected array of strings');
518
+ }
519
+ }
520
+ }
521
+ }
343
522
  }
344
523
 
345
524
  export function validateTheme(input: unknown): ValidationResult {
@@ -403,6 +582,10 @@ export function validateTheme(input: unknown): ValidationResult {
403
582
  v.persistentLayers('persistentLayers', input.persistentLayers);
404
583
  }
405
584
 
585
+ if (input.pageStyle !== undefined) {
586
+ v.pageStyle('pageStyle', input.pageStyle);
587
+ }
588
+
406
589
  if (v.errors.length === 0) {
407
590
  return { valid: true, errors: [], theme: input as unknown as Theme };
408
591
  }
@@ -44,5 +44,34 @@
44
44
  "red": { "bg": "#1a202c", "text": "#fc8181", "accent": "#feb2b2" },
45
45
  "orange": { "bg": "#1a202c", "text": "#f6ad55", "accent": "#fbd38d" },
46
46
  "teal": { "bg": "#1a202c", "text": "#4fd1c5", "accent": "#81e6d9" }
47
+ },
48
+ "pageStyle": {
49
+ "family": "brutalist",
50
+ "tokens": {
51
+ "contentMaxWidth": 760,
52
+ "wideMaxWidth": 1200,
53
+ "sectionSpacing": "compact",
54
+ "cornerRadius": 0,
55
+ "divider": "thick-rule",
56
+ "backgroundRhythm": "accent-bands",
57
+ "heroStyle": "oversized-type",
58
+ "headingTreatment": {
59
+ "eyebrow": "kicker",
60
+ "scale": "oversized",
61
+ "case": "uppercase"
62
+ },
63
+ "imageFraming": "bordered",
64
+ "shadow": "heavy",
65
+ "quoteMark": "none",
66
+ "numeralStyle": "oversized"
67
+ },
68
+ "sections": {
69
+ "quote-band": {
70
+ "background": "accent"
71
+ }
72
+ },
73
+ "accentRotation": {
74
+ "strategy": "cycle"
75
+ }
47
76
  }
48
77
  }
@@ -107,5 +107,37 @@
107
107
  }
108
108
  }
109
109
  ]
110
+ },
111
+ "pageStyle": {
112
+ "family": "cinematic",
113
+ "tokens": {
114
+ "contentMaxWidth": 760,
115
+ "wideMaxWidth": 1100,
116
+ "sectionSpacing": "generous",
117
+ "cornerRadius": 0,
118
+ "divider": "gap-only",
119
+ "backgroundRhythm": "flat",
120
+ "heroStyle": "full-bleed",
121
+ "headingTreatment": {
122
+ "eyebrow": "kicker",
123
+ "scale": "display",
124
+ "case": "uppercase"
125
+ },
126
+ "imageFraming": "letterboxed",
127
+ "shadow": "none",
128
+ "quoteMark": "none",
129
+ "numeralStyle": "oversized",
130
+ "pattern": "noise"
131
+ },
132
+ "sections": {
133
+ "quote-band": {
134
+ "hints": {
135
+ "vignette": true
136
+ }
137
+ }
138
+ },
139
+ "accentRotation": {
140
+ "strategy": "primary-only"
141
+ }
110
142
  }
111
143
  }
@@ -92,5 +92,34 @@
92
92
  }
93
93
  }
94
94
  ]
95
+ },
96
+ "pageStyle": {
97
+ "family": "documentary",
98
+ "tokens": {
99
+ "contentMaxWidth": 680,
100
+ "wideMaxWidth": 1000,
101
+ "sectionSpacing": "generous",
102
+ "cornerRadius": 2,
103
+ "divider": "thick-rule",
104
+ "backgroundRhythm": "flat",
105
+ "heroStyle": "letterbox",
106
+ "headingTreatment": {
107
+ "eyebrow": "numbered",
108
+ "scale": "display",
109
+ "underline": "none"
110
+ },
111
+ "imageFraming": "letterboxed",
112
+ "shadow": "none",
113
+ "quoteMark": "accent-bar",
114
+ "numeralStyle": "boxed"
115
+ },
116
+ "sections": {
117
+ "quote-band": {
118
+ "variant": "editorial"
119
+ }
120
+ },
121
+ "accentRotation": {
122
+ "strategy": "primary-only"
123
+ }
95
124
  }
96
125
  }
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": "1",
3
3
  "id": "gezellig",
4
4
  "name": "Gezellig",
5
- "description": "Warm sage + terracotta palette inspired by the Gezel desktop app. Subtle orange-tinted dark background, cream text, sage and terracotta accents.",
5
+ "description": "Cozy, warm-hued palette with a subtle orange-tinted dark background, cream text, and sage and terracotta accents.",
6
6
  "colors": {
7
7
  "primary": "#b0724c",
8
8
  "secondary": "#667f62",
@@ -89,5 +89,35 @@
89
89
  }
90
90
  }
91
91
  ]
92
+ },
93
+ "pageStyle": {
94
+ "family": "soft",
95
+ "tokens": {
96
+ "contentMaxWidth": 740,
97
+ "wideMaxWidth": 1060,
98
+ "sectionSpacing": "comfortable",
99
+ "cornerRadius": 16,
100
+ "divider": "dotted",
101
+ "backgroundRhythm": "tinted-panels",
102
+ "heroStyle": "stacked",
103
+ "headingTreatment": {
104
+ "eyebrow": "kicker",
105
+ "scale": "regular",
106
+ "case": "none"
107
+ },
108
+ "imageFraming": "circle-accent",
109
+ "shadow": "soft",
110
+ "quoteMark": "oversized-glyph",
111
+ "numeralStyle": "plain",
112
+ "pattern": "noise"
113
+ },
114
+ "sections": {
115
+ "quote-band": {
116
+ "background": "alternate"
117
+ }
118
+ },
119
+ "accentRotation": {
120
+ "strategy": "alternate-two"
121
+ }
92
122
  }
93
123
  }
@@ -85,5 +85,42 @@
85
85
  }
86
86
  }
87
87
  ]
88
+ },
89
+ "pageStyle": {
90
+ "family": "editorial",
91
+ "tokens": {
92
+ "contentMaxWidth": 720,
93
+ "wideMaxWidth": 1240,
94
+ "sectionSpacing": "comfortable",
95
+ "cornerRadius": 6,
96
+ "divider": "double-rule",
97
+ "backgroundRhythm": "flat",
98
+ "heroStyle": "split",
99
+ "headingTreatment": {
100
+ "eyebrow": "kicker",
101
+ "scale": "display",
102
+ "underline": "none"
103
+ },
104
+ "imageFraming": "flush",
105
+ "shadow": "none",
106
+ "quoteMark": "oversized-glyph",
107
+ "numeralStyle": "oversized",
108
+ "pattern": "diagonal"
109
+ },
110
+ "sections": {
111
+ "prose": {
112
+ "hints": {
113
+ "dropCap": true
114
+ }
115
+ },
116
+ "feature-split": {
117
+ "hints": {
118
+ "alternate": true
119
+ }
120
+ }
121
+ },
122
+ "accentRotation": {
123
+ "strategy": "alternate-two"
124
+ }
88
125
  }
89
126
  }
@@ -41,5 +41,34 @@
41
41
  "red": { "bg": "#fff5f5", "text": "#c53030", "accent": "#e53e3e" },
42
42
  "orange": { "bg": "#fffaf0", "text": "#c05621", "accent": "#dd6b20" },
43
43
  "teal": { "bg": "#e6fffa", "text": "#285e61", "accent": "#319795" }
44
+ },
45
+ "pageStyle": {
46
+ "family": "clean",
47
+ "tokens": {
48
+ "contentMaxWidth": 720,
49
+ "wideMaxWidth": 1000,
50
+ "sectionSpacing": "generous",
51
+ "cornerRadius": 8,
52
+ "divider": "gap-only",
53
+ "backgroundRhythm": "flat",
54
+ "heroStyle": "stacked",
55
+ "headingTreatment": {
56
+ "eyebrow": "none",
57
+ "scale": "display",
58
+ "underline": "none"
59
+ },
60
+ "imageFraming": "flush",
61
+ "shadow": "none",
62
+ "quoteMark": "none",
63
+ "numeralStyle": "plain"
64
+ },
65
+ "sections": {
66
+ "quote-band": {
67
+ "emphasis": "quiet"
68
+ }
69
+ },
70
+ "accentRotation": {
71
+ "strategy": "primary-only"
72
+ }
44
73
  }
45
74
  }