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