@neatico/visual-core 0.14.3 → 0.14.64

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