@neatico/visual-core 0.14.2 → 0.14.26

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.
@@ -1,126 +1,513 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DESIGN_RECIPES = exports.DESIGN_THEMES = void 0;
3
+ exports.DESIGN_RECIPES = exports.MANUAL_COMPOSITION_REASONS = exports.DESIGN_THEMES = void 0;
4
4
  exports.designTheme = designTheme;
5
+ exports.canonicalRecipeId = canonicalRecipeId;
5
6
  exports.pageEntryCount = pageEntryCount;
6
7
  exports.designCandidates = designCandidates;
7
8
  exports.structureFingerprint = structureFingerprint;
8
9
  const compositionScenes_js_1 = require("./compositionScenes.js");
9
- const layouts_js_1 = require("./layouts.js");
10
10
  const socialLayouts_js_1 = require("./socialLayouts.js");
11
11
  const content_js_1 = require("./content.js");
12
12
  /** Complete, deterministic token sets; decorative geometry belongs to individual compositions. */
13
13
  exports.DESIGN_THEMES = [
14
- { id: 'studio', palette: { background: '#F5F3EE', surface: '#FFFFFF', ink: '#20251F', muted: '#5F685C', accent: '#376346', line: '#D7DED3' }, typography: { heading: 'Inter', body: 'Inter', code: 'monospace', headingWeight: 700 }, geometry: { radius: 4, spacing: 1, rule: 'hairline' } },
15
- { id: 'ink', palette: { background: '#121B29', surface: '#1D2B3E', ink: '#F5F7FB', muted: '#B3C2D6', accent: '#85C8F3', line: '#3D526C' }, typography: { heading: 'Inter', body: 'Inter', code: 'monospace', headingWeight: 700 }, geometry: { radius: 12, spacing: 1, rule: 'accent' } },
16
- { id: 'editorial', palette: { background: '#FCF8F1', surface: '#FFFFFF', ink: '#30281F', muted: '#756657', accent: '#A74328', line: '#DED2C1' }, typography: { heading: 'Georgia', body: 'Inter', code: 'monospace', headingWeight: 700 }, geometry: { radius: 0, spacing: 1.1, rule: 'hairline' } },
17
- { id: 'electric', palette: { background: '#F3F4FF', surface: '#FFFFFF', ink: '#222047', muted: '#615D7B', accent: '#5340D9', line: '#DAD5F2' }, typography: { heading: 'Inter', body: 'Inter', code: 'monospace', headingWeight: 800 }, geometry: { radius: 16, spacing: 1, rule: 'none' } },
18
- { id: 'signal', palette: { background: '#FFE04B', surface: '#FFF5B7', ink: '#242418', muted: '#535039', accent: '#272B21', line: '#B8A233' }, typography: { heading: 'Inter', body: 'Inter', code: 'monospace', headingWeight: 800 }, geometry: { radius: 2, spacing: 1.05, rule: 'none' } },
14
+ {
15
+ id: "neatico",
16
+ palette: { background: "#FAF9F6", surface: "#FFFFFF", ink: "#202124", muted: "#62646B", accent: "#F4CE36", line: "#E4E1D9" },
17
+ typography: { heading: "Inter", body: "Inter", code: "monospace", headingWeight: 700 },
18
+ geometry: { radius: 12, spacing: 1, rule: "accent" },
19
+ },
20
+ {
21
+ id: "studio",
22
+ palette: {
23
+ background: "#F5F3EE",
24
+ surface: "#FFFFFF",
25
+ ink: "#20251F",
26
+ muted: "#5F685C",
27
+ accent: "#376346",
28
+ line: "#D7DED3",
29
+ },
30
+ typography: {
31
+ heading: "Inter",
32
+ body: "Inter",
33
+ code: "monospace",
34
+ headingWeight: 700,
35
+ },
36
+ geometry: { radius: 4, spacing: 1, rule: "hairline" },
37
+ },
38
+ {
39
+ id: "ink",
40
+ palette: {
41
+ background: "#121B29",
42
+ surface: "#1D2B3E",
43
+ ink: "#F5F7FB",
44
+ muted: "#B3C2D6",
45
+ accent: "#85C8F3",
46
+ line: "#3D526C",
47
+ },
48
+ typography: {
49
+ heading: "Inter",
50
+ body: "Inter",
51
+ code: "monospace",
52
+ headingWeight: 700,
53
+ },
54
+ geometry: { radius: 12, spacing: 1, rule: "accent" },
55
+ },
56
+ {
57
+ id: "editorial",
58
+ palette: {
59
+ background: "#FCF8F1",
60
+ surface: "#FFFFFF",
61
+ ink: "#30281F",
62
+ muted: "#756657",
63
+ accent: "#A74328",
64
+ line: "#DED2C1",
65
+ },
66
+ typography: {
67
+ heading: "Lora",
68
+ body: "Inter",
69
+ code: "monospace",
70
+ headingWeight: 700,
71
+ },
72
+ geometry: { radius: 0, spacing: 1.1, rule: "hairline" },
73
+ },
74
+ {
75
+ id: "electric",
76
+ palette: {
77
+ background: "#F3F4FF",
78
+ surface: "#FFFFFF",
79
+ ink: "#222047",
80
+ muted: "#615D7B",
81
+ accent: "#5340D9",
82
+ line: "#DAD5F2",
83
+ },
84
+ typography: {
85
+ heading: "Inter",
86
+ body: "Inter",
87
+ code: "monospace",
88
+ headingWeight: 800,
89
+ },
90
+ geometry: { radius: 16, spacing: 1, rule: "none" },
91
+ },
92
+ {
93
+ id: "signal",
94
+ palette: {
95
+ background: "#FFE04B",
96
+ surface: "#FFF5B7",
97
+ ink: "#242418",
98
+ muted: "#535039",
99
+ accent: "#272B21",
100
+ line: "#B8A233",
101
+ },
102
+ typography: {
103
+ heading: "Inter",
104
+ body: "Inter",
105
+ code: "monospace",
106
+ headingWeight: 800,
107
+ },
108
+ geometry: { radius: 2, spacing: 1.05, rule: "none" },
109
+ },
19
110
  ];
20
111
  function designTheme(id) {
21
- const alias = { neatico: 'electric', paper: 'studio', midnight: 'ink', ocean: 'ink', plum: 'electric' };
22
- return exports.DESIGN_THEMES.find(theme => theme.id === (alias[id] || id)) || exports.DESIGN_THEMES[0];
112
+ const alias = {
113
+ paper: "studio",
114
+ midnight: "ink",
115
+ ocean: "ink",
116
+ plum: "electric",
117
+ };
118
+ return (exports.DESIGN_THEMES.find((theme) => theme.id === (alias[id] || id)) ||
119
+ exports.DESIGN_THEMES[0]);
23
120
  }
24
121
  const family = (id, semantic) => {
25
122
  if (/code|terminal/.test(id))
26
- return 'code';
123
+ return "code";
27
124
  if (/comparison|compare|before-after|myth|problem-solution/.test(id))
28
- return 'comparison';
125
+ return "comparison";
29
126
  if (/process|steps|timeline|roadmap|journey|syllabus|workflow/.test(id))
30
- return 'steps';
127
+ return "steps";
31
128
  if (/chart|metric|stat|score|usage-breakdown/.test(id))
32
- return 'chart';
129
+ return "chart";
33
130
  if (/quote/.test(id))
34
- return 'quote';
131
+ return "quote";
35
132
  if (/table|specification/.test(id))
36
- return 'parameters';
133
+ return "parameters";
37
134
  if (/image|photo|gallery/.test(id))
38
- return 'image';
135
+ return "image";
39
136
  if (/cover|hero|banner|statement|closing|section/.test(id))
40
- return 'hero';
41
- if (semantic === 'steps')
42
- return 'steps';
43
- if (semantic === 'comparison')
44
- return 'comparison';
45
- return 'features';
137
+ return "hero";
138
+ if (semantic === "steps")
139
+ return "steps";
140
+ if (semantic === "comparison")
141
+ return "comparison";
142
+ return "features";
46
143
  };
47
- const accepted = (family) => ({ code: ['code'], comparison: ['comparison'], steps: ['steps'], chart: ['chart'], quote: ['quote'], parameters: ['parameters'], image: ['image'], hero: ['paragraph'], prose: ['paragraph'], features: ['features', 'paragraph'] }[family]);
144
+ const accepted = (family) => ({
145
+ code: ["code"],
146
+ comparison: ["comparison"],
147
+ steps: ["steps"],
148
+ chart: ["chart"],
149
+ quote: ["quote"],
150
+ parameters: ["parameters"],
151
+ image: ["image"],
152
+ hero: ["paragraph", "image"],
153
+ prose: ["paragraph"],
154
+ features: ["features", "paragraph"],
155
+ })[family];
48
156
  // Explicit semantic contracts, not name/scene guesses. A travel itinerary, calendar,
49
157
  // price promotion or fake UI cannot be inferred from an arbitrary paragraph.
50
158
  const socialContracts = {
51
- 'news-focus': ['paragraph'], manifesto: ['paragraph'], editorial: ['paragraph'], notice: ['paragraph'],
52
- 'block-banner': ['paragraph'], 'definition-card': ['paragraph'], 'concept-explainer': ['paragraph'],
53
- 'og-cover': ['paragraph'], 'gradient-cover': ['paragraph'], 'bold-diagonal-cover': ['paragraph'], 'split-cover': ['paragraph'],
54
- 'article-summary-card': ['paragraph'], 'paragraph-summary-card': ['paragraph'],
55
- bento: ['features'], spotlight: ['features'], report: ['features'], checklist: ['features'],
56
- 'contents-index': ['features'], 'contents-grid': ['features'], 'tips-card': ['features'],
57
- 'digest-card': ['features'], 'tips-grid': ['features'], 'feature-chip-hero': ['features'],
58
- steps: ['steps'], 'contents-chapters': ['steps'], 'illustration-process': ['steps'],
59
- comparison: ['comparison'], 'before-after': ['comparison'],
60
- 'code-snippet': ['code'], specification: ['parameters'],
61
- quote: ['quote'], 'editorial-quote': ['quote'], 'magazine-pull-quote': ['quote'],
159
+ "note-question": ["paragraph"], "note-marker": ["paragraph"], "note-editorial": ["paragraph"], "note-news": ["paragraph"], "note-checklist": ["features", "steps", "paragraph"], "note-photo": ["image", "paragraph"],
160
+ "news-focus": ["paragraph"],
161
+ manifesto: ["paragraph"],
162
+ editorial: ["paragraph"],
163
+ notice: ["paragraph"],
164
+ "block-banner": ["paragraph"],
165
+ "definition-card": ["paragraph"],
166
+ "concept-explainer": ["paragraph"],
167
+ "og-cover": ["paragraph"],
168
+ "gradient-cover": ["paragraph"],
169
+ "bold-diagonal-cover": ["paragraph"],
170
+ "split-cover": ["paragraph"],
171
+ "article-summary-card": ["paragraph"],
172
+ "paragraph-summary-card": ["paragraph"],
173
+ bento: ["features"],
174
+ spotlight: ["features"],
175
+ report: ["features"],
176
+ checklist: ["features"],
177
+ "contents-index": ["features"],
178
+ "contents-grid": ["features"],
179
+ "tips-card": ["features"],
180
+ "digest-card": ["features"],
181
+ "tips-grid": ["features"],
182
+ "feature-chip-hero": ["features"],
183
+ steps: ["steps"],
184
+ "contents-chapters": ["steps"],
185
+ "illustration-process": ["steps"],
186
+ comparison: ["comparison"],
187
+ "before-after": ["comparison"],
188
+ "code-snippet": ["code"],
189
+ specification: ["parameters"],
190
+ course: ["steps"],
191
+ "case-study": ["features"],
192
+ safety: ["features"],
193
+ faq: ["features"],
194
+ testimonial: ["quote"],
195
+ quote: ["quote"],
196
+ "editorial-quote": ["quote"],
197
+ "magazine-pull-quote": ["quote"],
198
+ };
199
+ /** Domain compositions require explicit semantic intent, never a keyword guess. */
200
+ const socialScenes = {
201
+ course: ["tutorial", "roadmap"],
202
+ safety: ["checklist"],
203
+ faq: ["questions"],
204
+ "case-study": ["case-study"],
205
+ testimonial: ["testimonial"],
206
+ };
207
+ exports.MANUAL_COMPOSITION_REASONS = {
208
+ launch: "Legacy artwork simulates product UI; use authored-feature-bento or authored-image-split with a resolved asset.",
209
+ metric: "Legacy value slot lacks a chart-to-value binding; use typed chart compositions.",
210
+ myth: "Fixed wrong/right semantics cannot be inferred from ordinary comparison columns.",
211
+ story: "Legacy artwork is illustrative; use authored-image-editorial with a resolved image.",
212
+ team: "Requires typed people and corresponding portrait assets.",
213
+ "split-bar-compare": "Requires paired measured series; generic comparison text is insufficient.",
214
+ "benchmark-bars": "Requires explicit benchmark values, not invented percentages.",
215
+ "usage-breakdown": "Requires typed parts and a validated total; current chart blocks support bar/line measurements, not inferred percentage shares.",
216
+ organization: "Requires an explicit parent/child relationship contract.",
217
+ "research-highlight": "Requires methodology and limitations mapped independently.",
218
+ "product-showcase": "Use authored-image-split/campaign with a resolved image, not legacy fake UI.",
219
+ };
220
+ const parameterScenes = {
221
+ "time-notice": ["event-details"],
222
+ ticket: ["event-details"],
223
+ webinar: ["event-details"],
224
+ community: ["event-details"],
225
+ schedule: ["event-details"],
226
+ pricing: ["pricing-details"],
227
+ offer: ["offer-details"],
228
+ hiring: ["recruitment-details"],
229
+ menu: ["menu-details"],
230
+ itinerary: ["travel-details"],
62
231
  };
63
232
  // Automatic presentation matching requires an explicit semantic contract too.
64
233
  // A feature paragraph must never become a price plan, employee or risk entry.
65
- const presentationContracts = {
66
- cover: ['paragraph'], section: ['paragraph'], statement: ['paragraph'], closing: ['paragraph'],
67
- agenda: ['features'], bullets: ['features'], 'two-column': ['features'],
68
- 'three-column': ['features'], cards: ['features'], checklist: ['features'],
69
- process: ['steps'], timeline: ['steps'], roadmap: ['steps'], 'progress-roadmap': ['steps'],
70
- comparison: ['comparison'], 'before-after': ['comparison'], 'problem-solution': ['comparison'],
71
- metrics: ['chart'], 'bar-chart': ['chart'], 'usage-breakdown': ['chart'],
72
- quote: ['quote'], 'editorial-quote': ['quote'], 'magazine-pull-quote': ['quote'],
73
- table: ['parameters'], specification: ['parameters'], 'code-snippet': ['code'],
74
- 'image-left': ['image'], 'image-right': ['image'], 'image-full': ['image'],
75
- 'product-showcase': ['image'],
76
- };
77
234
  const recipe = (id, kind, semantic) => {
78
- const contract = (kind === 'social' ? socialContracts : presentationContracts)[id];
235
+ if (parameterScenes[id])
236
+ return {
237
+ id,
238
+ kind,
239
+ renderer: "semantic",
240
+ automatic: true,
241
+ family: `domain-${id}`,
242
+ accepts: ["parameters"],
243
+ minEntries: 1,
244
+ maxEntries: 6,
245
+ requiresImage: false,
246
+ scenes: parameterScenes[id],
247
+ };
248
+ const contract = socialContracts[id];
79
249
  const group = contract?.[0] || family(id, semantic);
80
- return { id, kind, renderer: 'catalog', automatic: Boolean(contract), family: group, accepts: contract || accepted(group), minEntries: group === 'comparison' ? 2 : kind === 'social' && (group === 'features' || group === 'steps' || group === 'parameters') ? 1 : 0, maxEntries: group === 'hero' ? 0 : kind === 'presentation' ? 4 : 6, requiresImage: group === 'image' };
250
+ return {
251
+ id,
252
+ kind,
253
+ renderer: "catalog",
254
+ automatic: Boolean(contract),
255
+ scenes: socialScenes[id],
256
+ ...(!contract
257
+ ? {
258
+ manualReason: exports.MANUAL_COMPOSITION_REASONS[id] ||
259
+ "No verified semantic projection.",
260
+ }
261
+ : {}),
262
+ family: group,
263
+ accepts: contract || accepted(group),
264
+ minEntries: group === "comparison"
265
+ ? 2
266
+ : kind === "social" &&
267
+ (group === "features" ||
268
+ group === "steps" ||
269
+ group === "parameters")
270
+ ? 1
271
+ : 0,
272
+ maxEntries: group === "hero" ? 0 : kind === "presentation" ? 4 : 6,
273
+ requiresImage: group === "image",
274
+ };
81
275
  };
276
+ /** Stable aliases keep saved documents readable while excluding duplicate candidates. */
277
+ function canonicalRecipeId(id, kind) {
278
+ if (/^semantic-(code|parameters|comparison|chart)-editorial$/.test(id))
279
+ return canonicalRecipeId(id.replace(/-editorial$/, "-stack"), kind);
280
+ if (id === "semantic-code-stack")
281
+ return "authored-code-editorial";
282
+ if (id === "semantic-comparison-stack")
283
+ return "authored-comparison-panels";
284
+ if (kind === "social" && id === "semantic-hero-split")
285
+ return "semantic-hero-editorial";
286
+ if (id === "semantic-quote-stack")
287
+ return "authored-quote-poster";
288
+ if (id === "semantic-quote-editorial")
289
+ return "authored-quote-editorial";
290
+ if (kind === "presentation" && id === "semantic-hero-split")
291
+ return "bookend-cover-split";
292
+ if (kind === "presentation" &&
293
+ ["semantic-hero-stack", "semantic-hero-editorial"].includes(id))
294
+ return "bookend-cover-editorial";
295
+ if (kind === "social" && id === "semantic-hero-stack")
296
+ return "semantic-hero-editorial";
297
+ return id;
298
+ }
82
299
  /** Existing authored catalogs remain first-class candidates, rather than being discarded. */
83
- exports.DESIGN_RECIPES = [
84
- ...['cover', 'closing'].flatMap(role => ['editorial', 'panel', 'split'].map(style => ({ id: `bookend-${role}-${style}`, kind: 'presentation', renderer: 'semantic', family: `${role}-${style}`, accepts: ['paragraph'], minEntries: 0, maxEntries: 0, requiresImage: false }))),
85
- ...['presentation', 'social'].map(kind => ({ id: 'brand-collection-grid', kind, renderer: 'semantic', family: 'brand-collection', accepts: ['features'], minEntries: 2, maxEntries: 12, requiresImage: false })),
86
- ...layouts_js_1.PRESENTATION_LAYOUT_IDS.map(id => recipe(id, 'presentation')),
87
- ...socialLayouts_js_1.SOCIAL_LAYOUT_CATALOG.map(item => recipe(item.id, 'social', item.scene)),
88
- ...['presentation', 'social'].flatMap(kind => [
89
- ...['banner', 'split', 'editorial', 'campaign', 'inset'].map(style => ({ id: `authored-image-${style}`, kind, renderer: 'semantic', family: `image-${style}`, accepts: ['image'], minEntries: 0, maxEntries: 0, requiresImage: true })),
90
- { id: 'authored-prose-inset', kind, renderer: 'semantic', family: 'prose-inset', accepts: ['paragraph'], minEntries: 0, maxEntries: 0, requiresImage: false },
91
- ...compositionScenes_js_1.AUTHORED_COMPOSITIONS.map(composition => ({ id: `authored-${composition.id}`, kind, renderer: 'semantic', family: composition.id, accepts: [composition.kind], minEntries: composition.min, maxEntries: composition.max, requiresImage: false })),
92
- ...['code', 'parameters', 'steps', 'comparison', 'features', 'quote', 'chart', 'hero', 'prose'].flatMap(group => ['split', 'stack', 'editorial'].map(variant => ({ id: `semantic-${group}-${variant}`, kind, renderer: 'semantic', family: `${group}-${variant}`, accepts: accepted(group), minEntries: 0, maxEntries: 20, requiresImage: false }))),
93
- { id: 'semantic-mixed-stack', kind, renderer: 'semantic', family: 'mixed-stack', accepts: ['paragraph', 'steps', 'features', 'comparison', 'code', 'parameters', 'quote', 'chart', 'image'], minEntries: 0, maxEntries: 20, requiresImage: false },
300
+ const ALL_DESIGN_RECIPES = [
301
+ ...["cover", "closing"].flatMap((role) => ["editorial", "panel", "split"].map((style) => ({
302
+ id: `bookend-${role}-${style}`,
303
+ kind: "presentation",
304
+ renderer: "semantic",
305
+ family: `${role}-${style}`,
306
+ accepts: ["paragraph", "image"],
307
+ minEntries: 0,
308
+ maxEntries: 0,
309
+ requiresImage: false,
310
+ }))),
311
+ ...["presentation", "social"].map((kind) => ({
312
+ id: "brand-collection-grid",
313
+ kind,
314
+ renderer: "semantic",
315
+ family: "brand-collection",
316
+ accepts: ["features"],
317
+ minEntries: 2,
318
+ maxEntries: 12,
319
+ requiresImage: false,
320
+ })),
321
+ ...socialLayouts_js_1.SOCIAL_LAYOUT_CATALOG.map((item) => recipe(item.id, "social", item.scene)),
322
+ ...["presentation", "social"].flatMap((kind) => [
323
+ {
324
+ id: "authored-plan-cards",
325
+ kind,
326
+ renderer: "semantic",
327
+ family: "plan-cards",
328
+ accepts: ["comparison", "paragraph"],
329
+ minEntries: 2,
330
+ maxEntries: 4,
331
+ requiresImage: false,
332
+ },
333
+ ...["banner", "split", "editorial", "campaign", "inset"].map((style) => ({
334
+ id: `authored-image-${style}`,
335
+ kind,
336
+ renderer: "semantic",
337
+ family: `image-${style}`,
338
+ accepts: ["image", "paragraph", "features"],
339
+ minEntries: 0,
340
+ maxEntries: 3,
341
+ requiresImage: true,
342
+ })),
343
+ {
344
+ id: "authored-prose-inset",
345
+ kind,
346
+ renderer: "semantic",
347
+ family: "prose-inset",
348
+ accepts: ["paragraph"],
349
+ minEntries: 0,
350
+ maxEntries: 0,
351
+ requiresImage: false,
352
+ },
353
+ ...compositionScenes_js_1.AUTHORED_COMPOSITIONS.map((composition) => ({
354
+ id: `authored-${composition.id}`,
355
+ kind,
356
+ renderer: "semantic",
357
+ family: composition.id,
358
+ accepts: [composition.kind, "paragraph"],
359
+ minEntries: composition.min,
360
+ maxEntries: composition.max,
361
+ requiresImage: false,
362
+ })),
363
+ ...[
364
+ "code",
365
+ "parameters",
366
+ "steps",
367
+ "comparison",
368
+ "features",
369
+ "quote",
370
+ "chart",
371
+ "hero",
372
+ "prose",
373
+ ].flatMap((group) => (["code", "parameters", "comparison", "chart"].includes(group)
374
+ ? ["split", "stack"]
375
+ : ["split", "stack", "editorial"]).map((variant) => ({
376
+ id: `semantic-${group}-${variant}`,
377
+ kind,
378
+ renderer: "semantic",
379
+ family: `${group}-${variant}`,
380
+ accepts: group === "prose" && variant === "editorial"
381
+ ? ["paragraph", "image"]
382
+ : accepted(group),
383
+ minEntries: 0,
384
+ maxEntries: 20,
385
+ requiresImage: false,
386
+ }))),
387
+ {
388
+ id: "semantic-mixed-stack",
389
+ kind,
390
+ renderer: "semantic",
391
+ family: "mixed-stack",
392
+ accepts: [
393
+ "paragraph",
394
+ "steps",
395
+ "features",
396
+ "comparison",
397
+ "code",
398
+ "parameters",
399
+ "quote",
400
+ "chart",
401
+ "image",
402
+ ],
403
+ minEntries: 0,
404
+ maxEntries: 20,
405
+ requiresImage: false,
406
+ },
94
407
  ]),
95
408
  ];
409
+ exports.DESIGN_RECIPES = ALL_DESIGN_RECIPES.filter((r) => canonicalRecipeId(r.id, r.kind) === r.id);
96
410
  function pageEntryCount(page) {
97
- return page.blocks.reduce((sum, block) => sum + (block.kind === 'steps' || block.kind === 'features' ? block.entries.length : block.kind === 'comparison' ? block.columns.length : block.kind === 'parameters' ? block.rows.length : block.kind === 'chart' ? block.values.length : 0), 0);
411
+ return page.blocks.reduce((sum, block) => sum +
412
+ (block.kind === "steps" || block.kind === "features"
413
+ ? block.entries.length
414
+ : block.kind === "comparison"
415
+ ? block.columns.length
416
+ : block.kind === "parameters"
417
+ ? block.rows.length
418
+ : block.kind === "chart"
419
+ ? block.values.length
420
+ : 0), 0);
98
421
  }
99
422
  function designCandidates(page, context) {
100
- if (!Number.isFinite(context.ratio) || context.ratio < .25 || context.ratio > 4)
423
+ if (!Number.isFinite(context.ratio) ||
424
+ context.ratio < 0.25 ||
425
+ context.ratio > 4)
101
426
  return [];
102
- const primary = page.blocks.filter(b => b.kind !== 'paragraph' && !(b.kind === 'image' && b.status === 'missing'));
103
- const kinds = [...new Set((primary.length ? primary : page.blocks.filter(b => b.kind !== 'image')).map(b => b.kind))];
104
- const count = pageEntryCount(page), characters = page.blocks.flatMap(content_js_1.contentBlockText).join('').length;
105
- const available = page.blocks.some(b => b.kind === 'image' && b.status === 'available');
427
+ const primary = page.blocks.filter((b) => b.kind !== "paragraph" && !(b.kind === "image" && b.status === "missing"));
428
+ const kinds = [
429
+ ...new Set((primary.length
430
+ ? primary
431
+ : page.blocks.filter((b) => b.kind !== "image")).map((b) => b.kind)),
432
+ ];
433
+ const richComparison = page.blocks.some((b) => b.kind === "comparison" &&
434
+ b.columns.some((c) => c.price || c.values || c.statuses));
435
+ const count = pageEntryCount(page), characters = page.blocks.flatMap(content_js_1.contentBlockText).join("").length;
436
+ const available = page.blocks.some((b) => b.kind === "image" && b.status === "available");
106
437
  const exclude = new Set(context.excluded || []);
107
- const candidates = exports.DESIGN_RECIPES.filter(r => r.kind === context.kind && r.automatic !== false && (!(r.family.startsWith('hero') && page.purpose !== 'cover' && page.purpose !== 'conclude' && characters > 120)) && (!r.id.startsWith('bookend-') || (page.purpose === 'cover' ? r.id.startsWith('bookend-cover-') : page.purpose === 'conclude' && r.id.startsWith('bookend-closing-'))) && (r.id !== 'brand-collection-grid' || page.scene === 'brand-collection') && !exclude.has(r.id) && count >= r.minEntries && count <= r.maxEntries && (!r.requiresImage || available) && kinds.every(k => r.accepts.includes(k)) && !(r.renderer === 'catalog' && (primary.length > 1 || characters > 1000)));
438
+ const candidates = exports.DESIGN_RECIPES.filter((r) => r.kind === context.kind &&
439
+ r.automatic !== false &&
440
+ (!r.scenes || Boolean(page.scene && r.scenes.includes(page.scene))) &&
441
+ !(r.family.startsWith("hero") &&
442
+ page.purpose !== "cover" &&
443
+ page.purpose !== "conclude" &&
444
+ characters > 120) &&
445
+ (!r.id.startsWith("bookend-") ||
446
+ (page.purpose === "cover"
447
+ ? r.id.startsWith("bookend-cover-")
448
+ : page.purpose === "conclude" &&
449
+ r.id.startsWith("bookend-closing-"))) &&
450
+ (r.id !== "brand-collection-grid" || page.scene === "brand-collection") &&
451
+ !exclude.has(r.id) &&
452
+ count >= r.minEntries &&
453
+ count <= r.maxEntries &&
454
+ (!r.requiresImage || available) &&
455
+ kinds.every((k) => r.accepts.includes(k)) &&
456
+ !(r.renderer === "catalog" &&
457
+ (richComparison || primary.length > 1 || characters > 1000)));
108
458
  const score = (r) => {
109
- const repeats = (context.previousFamilies || []).filter(f => f === r.family).length;
110
- const intent = page.purpose === 'cover' || page.purpose === 'conclude' ? r.family.startsWith('hero') : page.purpose === 'instruct' ? r.family.startsWith('steps') || r.family.startsWith('code') : true;
111
- return (r.id.startsWith('bookend-') ? 80 : 0) + (page.scene === 'brand-collection' && r.id === 'brand-collection-grid' ? 80 : 0) + (0, compositionScenes_js_1.sceneRecipePreference)(page.scene, r.id) + (intent ? 30 : 0) + (r.renderer === 'catalog' ? 8 : 0) - (r.id.includes('mixed') ? 20 : 0) - repeats * 18 + (context.ratio < 1 && r.id.endsWith('stack') ? 6 : 0);
459
+ const repeats = (context.previousFamilies || []).filter((f) => f === r.family).length;
460
+ const intent = page.purpose === "cover" || page.purpose === "conclude"
461
+ ? r.family.startsWith("hero")
462
+ : page.purpose === "instruct"
463
+ ? r.family.startsWith("steps") || r.family.startsWith("code")
464
+ : true;
465
+ return ((r.id.startsWith("bookend-") ? 80 : 0) +
466
+ (page.scene === "brand-collection" && r.id === "brand-collection-grid"
467
+ ? 80
468
+ : 0) +
469
+ (0, compositionScenes_js_1.sceneRecipePreference)(page.scene, r.id) +
470
+ (intent ? 30 : 0) +
471
+ (r.renderer === "catalog" ? 8 : 0) -
472
+ (r.id.includes("mixed") ? 20 : 0) -
473
+ repeats * 18 +
474
+ (context.ratio < 1 && r.id.endsWith("stack") ? 6 : 0));
112
475
  };
113
- const ranked = candidates.map((r, index) => ({ r, index, score: score(r) })).sort((a, b) => b.score - a.score || a.index - b.index).map(e => e.r);
476
+ const ranked = candidates
477
+ .map((r, index) => ({ r, index, score: score(r) }))
478
+ .sort((a, b) => b.score - a.score || a.index - b.index)
479
+ .map((e) => e.r);
114
480
  const limit = Math.max(1, Math.min(32, context.limit ?? 12));
115
481
  // Catalog size must not prevent semantic renderers from reaching measurement.
116
- const semantic = ranked.filter(r => r.renderer === 'semantic').slice(0, Math.min(8, Math.ceil(limit / 2)));
117
- const reserved = new Set(semantic.map(r => r.id));
118
- const selected = new Set([...semantic, ...ranked.filter(r => !reserved.has(r.id)).slice(0, limit - semantic.length)].map(r => r.id));
119
- return ranked.filter(r => selected.has(r.id));
482
+ const semantic = ranked
483
+ .filter((r) => r.renderer === "semantic")
484
+ .slice(0, Math.min(8, Math.ceil(limit / 2)));
485
+ const reserved = new Set(semantic.map((r) => r.id));
486
+ const selected = new Set([
487
+ ...semantic,
488
+ ...ranked
489
+ .filter((r) => !reserved.has(r.id))
490
+ .slice(0, limit - semantic.length),
491
+ ].map((r) => r.id));
492
+ return ranked.filter((r) => selected.has(r.id));
120
493
  }
121
494
  /** Ignores color, copy and generated IDs; previews that only recolor are the same composition. */
122
495
  function structureFingerprint(items, width, height) {
123
- if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
124
- throw new Error('INVALID_CANVAS');
125
- return items.map(item => [item.role, ...[item.x / width, item.y / height, item.width / width, item.height / height].map(v => Math.round(v * 24))].join(':')).sort().join('|');
496
+ if (!Number.isFinite(width) ||
497
+ !Number.isFinite(height) ||
498
+ width <= 0 ||
499
+ height <= 0)
500
+ throw new Error("INVALID_CANVAS");
501
+ return items
502
+ .map((item) => [
503
+ item.role,
504
+ ...[
505
+ item.x / width,
506
+ item.y / height,
507
+ item.width / width,
508
+ item.height / height,
509
+ ].map((v) => Math.round(v * 24)),
510
+ ].join(":"))
511
+ .sort()
512
+ .join("|");
126
513
  }