@neatico/visual-core 0.14.26 → 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.
- package/dist/candidateRanking.d.ts +31 -0
- package/dist/candidateRanking.js +55 -0
- package/dist/cjs/candidateRanking.js +61 -0
- package/dist/cjs/compositionNumbers.js +111 -0
- package/dist/cjs/compositionUseCases.js +145 -0
- package/dist/cjs/content.js +46 -10
- package/dist/cjs/contentBrief.js +82 -0
- package/dist/cjs/designSystem.js +80 -8
- package/dist/cjs/fontCatalog.js +7 -1
- package/dist/cjs/formalCompositions.js +120 -0
- package/dist/cjs/index.js +13 -0
- package/dist/cjs/nativeSocialParameters.js +145 -0
- package/dist/cjs/pagination.js +14 -5
- package/dist/cjs/parameterDefaults.js +20 -0
- package/dist/cjs/recipeParameters.js +304 -0
- package/dist/cjs/styleManifest.js +107 -0
- package/dist/cjs/textPresentation.js +68 -0
- package/dist/compositionNumbers.d.ts +4 -0
- package/dist/compositionNumbers.js +108 -0
- package/dist/compositionUseCases.d.ts +1 -0
- package/dist/compositionUseCases.js +142 -0
- package/dist/content.d.ts +10 -0
- package/dist/content.js +46 -10
- package/dist/contentBrief.d.ts +16 -0
- package/dist/contentBrief.js +78 -0
- package/dist/designSystem.js +80 -8
- package/dist/fontCatalog.js +7 -1
- package/dist/formalCompositions.d.ts +26 -0
- package/dist/formalCompositions.js +116 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/nativeSocialParameters.d.ts +38 -0
- package/dist/nativeSocialParameters.js +139 -0
- package/dist/pagination.js +14 -5
- package/dist/parameterDefaults.d.ts +2 -0
- package/dist/parameterDefaults.js +17 -0
- package/dist/recipeParameters.d.ts +34 -0
- package/dist/recipeParameters.js +300 -0
- package/dist/styleManifest.d.ts +20 -0
- package/dist/styleManifest.js +103 -0
- package/dist/textPresentation.d.ts +29 -0
- package/dist/textPresentation.js +64 -0
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { compositionUseCase } from './compositionUseCases.js';
|
|
2
|
+
import { nativeSocialParameterContract } from './nativeSocialParameters.js';
|
|
3
|
+
import { DESIGN_RECIPES } from './designSystem.js';
|
|
4
|
+
import { SOCIAL_LAYOUT_CATALOG } from './socialLayouts.js';
|
|
5
|
+
import { recipeParameterContract } from './recipeParameters.js';
|
|
6
|
+
import { SEMANTIC_DISPLAY_NUMBERS, SOCIAL_DISPLAY_NUMBERS } from './compositionNumbers.js';
|
|
7
|
+
const both = ['presentation', 'social'];
|
|
8
|
+
const semantic = (id, accepts, minEntries = 0, maxEntries = 12, requiresImage = false) => ({ id, renderer: 'semantic', kind: 'social', family: id, accepts, minEntries, maxEntries, requiresImage });
|
|
9
|
+
function define(recipe, kinds, id, displayNumber, min = .25, max = 4) {
|
|
10
|
+
return { ...recipeParameterContract(recipe, kinds), semanticDescription: compositionUseCase(recipe.id), id, displayNumber, aspectRatios: { min, max }, recipe };
|
|
11
|
+
}
|
|
12
|
+
/** Structural reference contracts shared with the product's existing renderers.
|
|
13
|
+
* Colorways and historical aliases remain outside active discovery.
|
|
14
|
+
*/
|
|
15
|
+
export const FORMAL_REFERENCE_COMPOSITIONS = [
|
|
16
|
+
{ id: 'authored-reference-photo', reference: 1, layout: 'authored-photo-editorial', accepts: ['image', 'paragraph'] },
|
|
17
|
+
{ id: 'authored-reference-manifesto', reference: 2, layout: 'authored-type-manifesto', accepts: ['paragraph'] },
|
|
18
|
+
{ id: 'authored-reference-journey', reference: 3, layout: 'authored-editorial-flow', accepts: ['steps'] },
|
|
19
|
+
{ id: 'authored-reference-vision', reference: 4, layout: 'authored-editorial-features', accepts: ['features'] },
|
|
20
|
+
{ id: 'authored-reference-matrix', reference: 5, layout: 'authored-editorial-matrix', accepts: ['comparison', 'paragraph'] },
|
|
21
|
+
{ id: 'authored-reference-diagonal', reference: 6, layout: 'authored-diagonal-cover', accepts: ['paragraph'] },
|
|
22
|
+
{ id: 'authored-reference-list', reference: 8, layout: 'authored-editorial-index', accepts: ['features'] },
|
|
23
|
+
{ id: 'authored-reference-story', reference: 11, layout: 'authored-visual-poster', accepts: ['image', 'paragraph', 'features'] },
|
|
24
|
+
{ id: 'authored-reference-sustainable', reference: 13, layout: 'authored-editorial-metrics', accepts: ['parameters'] },
|
|
25
|
+
{ id: 'authored-reference-code', reference: 14, layout: 'authored-code-panel', accepts: ['code'] },
|
|
26
|
+
{ id: 'authored-reference-bars', reference: 18, layout: 'authored-editorial-bars', accepts: ['chart'] },
|
|
27
|
+
{ id: 'authored-reference-share', reference: 19, layout: 'authored-editorial-donut', accepts: ['chart'] },
|
|
28
|
+
{ id: 'authored-reference-opinion', reference: 20, layout: 'authored-editorial-quote', accepts: ['quote'] },
|
|
29
|
+
{ id: 'authored-reference-comparison', reference: 22, layout: 'authored-editorial-comparison', accepts: ['comparison'] },
|
|
30
|
+
{ id: 'authored-reference-faq', reference: 23, layout: 'authored-editorial-faq', accepts: ['features'] },
|
|
31
|
+
{ id: 'authored-reference-trend', reference: 25, layout: 'authored-editorial-trend', accepts: ['chart'] },
|
|
32
|
+
];
|
|
33
|
+
const semanticRecipes = DESIGN_RECIPES.filter(recipe => recipe.renderer === 'semantic');
|
|
34
|
+
const uniqueSemantic = semanticRecipes.filter((recipe, index) => semanticRecipes.findIndex(other => other.id === recipe.id) === index);
|
|
35
|
+
const campaignDefinitions = [
|
|
36
|
+
['ledger-chart', ['parameters', 'chart'], 2, 6, false, true],
|
|
37
|
+
['detail-sidebar', ['paragraph', 'parameters'], 1, 3, false, true],
|
|
38
|
+
['technical-log', ['code', 'paragraph'], 0, 12, false, false],
|
|
39
|
+
['architecture-stack', ['steps', 'paragraph'], 2, 4, false, false],
|
|
40
|
+
['speaker-spotlight', ['quote', 'parameters'], 1, 6, true, false],
|
|
41
|
+
['speaker-lineup', ['quote', 'parameters'], 1, 6, true, false],
|
|
42
|
+
];
|
|
43
|
+
const founderDefinitions = [
|
|
44
|
+
['authored-mechanism-flow', ['features', 'steps', 'paragraph'], 2, 4],
|
|
45
|
+
['authored-reference-capability-map', ['features', 'paragraph'], 2, 4],
|
|
46
|
+
['authored-founder-timeline', ['steps', 'paragraph'], 2, 4],
|
|
47
|
+
['authored-founder-comparison', ['comparison', 'paragraph'], 2, 2],
|
|
48
|
+
];
|
|
49
|
+
const articleKinds = ['features', 'process', 'comparison', 'summary', 'checklist', 'faq', 'chart'];
|
|
50
|
+
const articleAccepts = { features: ['features', 'paragraph'], process: ['steps', 'paragraph'], comparison: ['comparison', 'paragraph'], summary: ['paragraph', 'features'], checklist: ['features', 'paragraph'], faq: ['features', 'paragraph'], chart: ['chart', 'paragraph'] };
|
|
51
|
+
function article(id, kind, variant) {
|
|
52
|
+
const cover = kind === 'cover';
|
|
53
|
+
const recipe = semantic(id, cover ? ['paragraph', 'features', 'image'] : articleAccepts[kind], 0, kind === 'chart' ? 12 : 3);
|
|
54
|
+
const number = cover ? variant + 1 : kind === 'features' && variant >= 3 ? 22 + variant : kind === 'process' && variant === 3 ? 33 : 4 + articleKinds.indexOf(kind) * 3 + variant;
|
|
55
|
+
const definition = define(recipe, [cover ? 'article-cover' : 'article-illustration'], id, `A${String(number).padStart(3, '0')}`, .5, 2.4);
|
|
56
|
+
// A cover is a headline, a short deck and its authored hero. It is not a
|
|
57
|
+
// miniature article: the original source remains in the candidate pool.
|
|
58
|
+
const coverSchema = { type: 'object', additionalProperties: false, required: ['title', 'blocks'], properties: {
|
|
59
|
+
title: { type: 'string', minLength: 1, maxLength: 72, description: 'A concise editorial headline, preferably four to eight words.' },
|
|
60
|
+
blocks: { type: 'array', minItems: 1, maxItems: 1, items: { type: 'object', additionalProperties: false, required: ['id', 'kind', 'text', 'evidence'], properties: {
|
|
61
|
+
id: { type: 'string', minLength: 1, maxLength: 80 }, kind: { const: 'paragraph' }, text: { type: 'string', minLength: 1, maxLength: 140, description: 'One concise supporting sentence. Summarize the topic; do not repeat the full article or enumerate every feature.' },
|
|
62
|
+
evidence: { type: 'array', minItems: 1, maxItems: 32, items: { type: 'object', additionalProperties: false, required: ['sourceId'], properties: { sourceId: { type: 'string', maxLength: 160 }, quote: { type: 'string', maxLength: 2000 } } } },
|
|
63
|
+
} } },
|
|
64
|
+
} };
|
|
65
|
+
// The optional hero preserves compatibility with saved paragraph-only pools.
|
|
66
|
+
// New requests can reference a real asset; missing assets retain native artwork.
|
|
67
|
+
const coverWithHero = { ...coverSchema, properties: { ...coverSchema.properties, blocks: { ...coverSchema.properties.blocks, maxItems: 2,
|
|
68
|
+
description: 'One supporting paragraph and, when imagery helps explain the topic, one image hero. Use an available asset ID or a missing image request; never invent a photo URL.',
|
|
69
|
+
items: [coverSchema.properties.blocks.items, { type: 'object', additionalProperties: false, required: ['id', 'kind', 'sourceId', 'alt', 'treatment', 'status', 'evidence'], properties: {
|
|
70
|
+
id: { type: 'string', minLength: 1, maxLength: 80 }, kind: { const: 'image' }, sourceId: { type: 'string', maxLength: 160 },
|
|
71
|
+
alt: { type: 'string', minLength: 1, maxLength: 300, description: 'A concise description of the image subject, grounded in the article topic.' },
|
|
72
|
+
treatment: { enum: ['photograph', 'diagram', 'screenshot'] }, status: { enum: ['available', 'missing'] }, assetId: { type: 'string', maxLength: 200 },
|
|
73
|
+
evidence: coverSchema.properties.blocks.items.properties.evidence,
|
|
74
|
+
} }],
|
|
75
|
+
} } };
|
|
76
|
+
const illustrationSchema = JSON.parse(JSON.stringify(definition.parameterSchema));
|
|
77
|
+
if (!cover) {
|
|
78
|
+
const primaryKind = kind === 'summary' ? 'features' : articleAccepts[kind][0];
|
|
79
|
+
const blocks = illustrationSchema.properties.blocks;
|
|
80
|
+
const primary = blocks.items.find((block) => block.properties.kind.const === primaryKind);
|
|
81
|
+
blocks.items = [primary];
|
|
82
|
+
blocks.minItems = 1;
|
|
83
|
+
blocks.maxItems = 1;
|
|
84
|
+
if (kind === 'faq')
|
|
85
|
+
primary.properties.entries.items.properties.title.pattern = '^.+[??]$';
|
|
86
|
+
}
|
|
87
|
+
const coverDirections = ['An editorial document sculpture with layered planes and a strong headline.', 'A connected constellation with a single focal object and an editorial headline.', 'An architectural archive silhouette balanced against expressive editorial typography.'];
|
|
88
|
+
return { ...definition, ...(cover ? { parameterSchema: coverWithHero, capacity: { minEntries: 0, maxEntries: 0, requiresImage: false }, semanticDescription: coverDirections[variant] } : { parameterSchema: illustrationSchema }), recipe: undefined, rendererKey: `article-${kind}:${variant}`,
|
|
89
|
+
semanticDescription: compositionUseCase(id) + (cover ? ' ' + coverDirections[variant] : '') };
|
|
90
|
+
}
|
|
91
|
+
/** Matches /dev/compositions/all: 161 default previews plus two landscape-only
|
|
92
|
+
* reference entries. Output capabilities overlap and must never be summed.
|
|
93
|
+
*/
|
|
94
|
+
export const FORMAL_COMPOSITIONS = [
|
|
95
|
+
...campaignDefinitions.map(([id, accepts, min, max, image, landscape], index) => define(semantic(`authored-reference-${id}`, accepts, min, max, image), landscape ? ['presentation'] : both, `new-reference:${id}`, `N${index + 1}`, landscape ? 1.4 : .25)),
|
|
96
|
+
define(semantic('authored-event-poster', ['parameters', 'paragraph'], 1, 6), ['social'], 'event:provider', 'E1'),
|
|
97
|
+
define(semantic('authored-image-triptych', ['paragraph', 'features', 'image'], 3, 3, true), ['social'], 'campaign:triptych', 'C1', .5, 2),
|
|
98
|
+
...FORMAL_REFERENCE_COMPOSITIONS.map(spec => define(semantic(spec.id, spec.accepts, 0, 12, spec.accepts.includes('image')), both, `reference:${spec.id}`, `R${String(spec.reference).padStart(2, '0')}`)),
|
|
99
|
+
...SOCIAL_LAYOUT_CATALOG.map(preset => {
|
|
100
|
+
const recipe = DESIGN_RECIPES.find(item => item.id === preset.id && item.kind === 'social');
|
|
101
|
+
return { ...define({ ...recipe, renderer: 'catalog' }, ['social'], `social:${preset.id}`, SOCIAL_DISPLAY_NUMBERS[preset.id], .5, 2.4), ...nativeSocialParameterContract(preset.id) };
|
|
102
|
+
}),
|
|
103
|
+
...uniqueSemantic.map(recipe => define(recipe, semanticRecipes.filter(other => other.id === recipe.id).map(other => other.kind), `semantic:${recipe.kind}:${recipe.id}`, SEMANTIC_DISPLAY_NUMBERS[`semantic:${recipe.kind}:${recipe.id}`])),
|
|
104
|
+
...founderDefinitions.map(([id, accepts, min, max], index) => define(semantic(id, accepts, min, max), ['social'], `semantic:social:${id}`, `F${index + 1}`, 1.3)),
|
|
105
|
+
...[0, 1, 2].flatMap(variant => [article(`article-cover-${variant}`, 'cover', variant), ...articleKinds.map(kind => article(`article-${kind}-${variant}`, kind, variant))]),
|
|
106
|
+
...[3, 4, 5, 6, 7, 8, 9, 10].map(variant => article(`article-features-${variant}`, 'features', variant)),
|
|
107
|
+
article('article-process-3', 'process', 3),
|
|
108
|
+
];
|
|
109
|
+
/** Only output, ratio, and explicit IDs are hard gates; semantic fit is scored. */
|
|
110
|
+
export function applicableCompositions(kind, ratio, id) {
|
|
111
|
+
if (!Number.isFinite(ratio) || ratio <= 0)
|
|
112
|
+
throw new Error('COMPOSITION_INVALID_RATIO');
|
|
113
|
+
if (id && !FORMAL_COMPOSITIONS.some(item => item.id === id))
|
|
114
|
+
throw new Error('COMPOSITION_UNKNOWN_ID');
|
|
115
|
+
return FORMAL_COMPOSITIONS.filter(item => item.supportedKinds.includes(kind) && ratio >= item.aspectRatios.min && ratio <= item.aspectRatios.max && (!id || item.id === id));
|
|
116
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,3 +13,13 @@ export * from "./designBlueprint.js";
|
|
|
13
13
|
export * from './compositionScenes.js';
|
|
14
14
|
export * from './editorial.js';
|
|
15
15
|
export * from './fontCatalog.js';
|
|
16
|
+
export * from './candidateRanking.js';
|
|
17
|
+
export * from './styleManifest.js';
|
|
18
|
+
export * from './recipeParameters.js';
|
|
19
|
+
export * from './formalCompositions.js';
|
|
20
|
+
export * from './contentBrief.js';
|
|
21
|
+
export * from "./nativeSocialParameters.js";
|
|
22
|
+
export * from "./compositionUseCases.js";
|
|
23
|
+
export { compositionParameterDefaults } from './parameterDefaults.js';
|
|
24
|
+
export { SOCIAL_DISPLAY_NUMBERS } from './compositionNumbers.js';
|
|
25
|
+
export * from './textPresentation.js';
|
package/dist/index.js
CHANGED
|
@@ -13,3 +13,13 @@ export * from "./designBlueprint.js";
|
|
|
13
13
|
export * from './compositionScenes.js';
|
|
14
14
|
export * from './editorial.js';
|
|
15
15
|
export * from './fontCatalog.js';
|
|
16
|
+
export * from './candidateRanking.js';
|
|
17
|
+
export * from './styleManifest.js';
|
|
18
|
+
export * from './recipeParameters.js';
|
|
19
|
+
export * from './formalCompositions.js';
|
|
20
|
+
export * from './contentBrief.js';
|
|
21
|
+
export * from "./nativeSocialParameters.js";
|
|
22
|
+
export * from "./compositionUseCases.js";
|
|
23
|
+
export { compositionParameterDefaults } from './parameterDefaults.js';
|
|
24
|
+
export { SOCIAL_DISPLAY_NUMBERS } from './compositionNumbers.js';
|
|
25
|
+
export * from './textPresentation.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type TextPresentation } from './textPresentation.js';
|
|
2
|
+
import type { ParameterSchema } from './recipeParameters.js';
|
|
3
|
+
/** Native slots keep renderer-specific content separate from semantic page projection. */
|
|
4
|
+
export interface NativeSocialParameters {
|
|
5
|
+
textPresentation?: TextPresentation[];
|
|
6
|
+
decorationEmoji?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
body: string;
|
|
9
|
+
eyebrow?: string;
|
|
10
|
+
emphasis?: string[];
|
|
11
|
+
titleLines?: string[];
|
|
12
|
+
cta?: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
attribution?: string;
|
|
15
|
+
imageAssetId?: string;
|
|
16
|
+
comparisonLabels?: [string, string];
|
|
17
|
+
items?: {
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
assetId?: string;
|
|
21
|
+
illustration?: 'document' | 'collaboration' | 'review' | 'feedback' | 'sequence' | 'comparison' | 'structure';
|
|
22
|
+
primaryValue?: number;
|
|
23
|
+
secondaryValue?: number;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
export interface NativeSocialContract {
|
|
27
|
+
parameterModel: 'native-social';
|
|
28
|
+
parameterSchema: ParameterSchema;
|
|
29
|
+
importance: Readonly<Record<string, number>>;
|
|
30
|
+
generationPolicy: Readonly<Record<string, readonly string[]>>;
|
|
31
|
+
}
|
|
32
|
+
export declare const COMPOSITION_ILLUSTRATIONS: readonly ["document", "collaboration", "review", "feedback", "sequence", "comparison", "structure"];
|
|
33
|
+
/** Every field corresponds to an authored visible slot; no scene-based eligibility filter. */
|
|
34
|
+
export declare function nativeSocialParameterContract(id: string): NativeSocialContract;
|
|
35
|
+
/** Apply the small closed schema vocabulary used by native slots on every local render. */
|
|
36
|
+
export declare function validateNativeSocialParameters(id: string, raw: unknown): NativeSocialParameters;
|
|
37
|
+
/** Numeric and image identities remain available when a native design becomes editable content. */
|
|
38
|
+
export declare function nativeSocialContentPage(values: NativeSocialParameters, original: import('./content.js').ContentPage): import('./content.js').ContentPage;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { compositionParameterDefaults } from './parameterDefaults.js';
|
|
2
|
+
import { textPresentationSchema, validateTextPresentations } from './textPresentation.js';
|
|
3
|
+
import { SOCIAL_LAYOUT_CATALOG } from './socialLayouts.js';
|
|
4
|
+
const slots = {
|
|
5
|
+
'note-question': {}, 'note-marker': {}, 'note-editorial': {}, 'note-checklist': { items: 3 }, 'note-photo': { image: true }, 'note-news': { value: true },
|
|
6
|
+
'chat-messenger': { items: 3 }, 'chat-imessage': { items: 3 }, 'chat-telegram': { items: 3 }, 'chat-workplace': { items: 3 },
|
|
7
|
+
'time-notice': { value: true }, 'news-focus': {}, manifesto: {}, launch: { image: true }, bento: { items: 3 }, spotlight: { items: 3, value: true, image: true },
|
|
8
|
+
ticket: { items: 2, value: true }, webinar: { value: true, image: true }, quote: { attribution: true }, testimonial: { attribution: true }, metric: { value: true },
|
|
9
|
+
report: { items: 3 }, checklist: { items: 3 }, steps: { items: 3 }, comparison: { items: 2 }, myth: { items: 2 }, pricing: { items: 3, value: true }, offer: { value: true, image: true },
|
|
10
|
+
editorial: { image: true }, story: { image: true }, hiring: { items: 3 }, team: { items: 3, itemImages: true }, course: { items: 3 }, 'case-study': { items: 3 }, notice: {}, community: { value: true },
|
|
11
|
+
menu: { items: 3 }, specification: { items: 3 }, itinerary: { items: 3 }, schedule: { items: 3 }, safety: { items: 3 }, faq: { items: 3 },
|
|
12
|
+
'split-bar-compare': { items: 3, quantities: 'comparison' }, 'benchmark-bars': { items: 3, quantities: 'benchmark' }, 'usage-breakdown': { items: 3, quantities: 'share' },
|
|
13
|
+
organization: { items: 3 }, 'editorial-quote': { attribution: true }, 'research-highlight': { items: 2 }, 'product-showcase': { items: 3, image: true },
|
|
14
|
+
};
|
|
15
|
+
const copy = (maxLength, description, minLength = 1) => ({ type: 'string', minLength, maxLength, description });
|
|
16
|
+
const object = (properties) => ({ type: 'object', additionalProperties: false, properties, required: Object.keys(properties) });
|
|
17
|
+
const asset = () => copy(160, 'An existing supplied asset ID. Use neatico-editorial-placeholder when no matching source image exists.');
|
|
18
|
+
export const COMPOSITION_ILLUSTRATIONS = ['document', 'collaboration', 'review', 'feedback', 'sequence', 'comparison', 'structure'];
|
|
19
|
+
const illustratedSlots = new Set(['bento', 'course', 'case-study', 'report', 'product-showcase', 'comparison', 'research-highlight']);
|
|
20
|
+
const provenance = ['user-fact', 'derived', 'creative', 'simulated'];
|
|
21
|
+
// These authored slots have been measured with complete provider sentences at
|
|
22
|
+
// square, landscape, and portrait ratios. Compact labels keep their own limits.
|
|
23
|
+
const sentenceBodySlots = new Set(['note-question', 'note-marker', 'note-editorial', 'chat-imessage', 'chat-telegram', 'news-focus', 'manifesto', 'launch', 'spotlight', 'comparison', 'myth', 'editorial', 'story', 'notice']);
|
|
24
|
+
const supportingItemSlots = new Set(['team', 'course', 'specification', 'schedule', 'research-highlight']);
|
|
25
|
+
/** Every field corresponds to an authored visible slot; no scene-based eligibility filter. */
|
|
26
|
+
export function nativeSocialParameterContract(id) {
|
|
27
|
+
const spec = slots[id];
|
|
28
|
+
if (!spec || !SOCIAL_LAYOUT_CATALOG.some(item => item.id === id))
|
|
29
|
+
throw new Error('COMPOSITION_UNKNOWN_NATIVE_SOCIAL');
|
|
30
|
+
const properties = { title: copy(id === 'note-question' ? 44 : 40, 'Concise audience-facing headline.'), body: copy(id === 'organization' ? 32 : id === 'case-study' ? 64 : ['quote', 'testimonial', 'editorial-quote'].includes(id) ? 140 : sentenceBodySlots.has(id) ? 96 : 80, id === 'organization' ? 'A short label for the parent or coordinating role; never invent a real person.' : 'One supporting sentence, or the quoted passage for a quote composition.') };
|
|
31
|
+
if (!id.startsWith('chat-') && !['note-photo', 'note-checklist'].includes(id))
|
|
32
|
+
properties.eyebrow = { ...copy(20, 'Optional concise section label. Use an empty string when no label improves the message.', 0), default: '' };
|
|
33
|
+
if (!id.startsWith('chat-') && !['note-photo', 'note-checklist'].includes(id))
|
|
34
|
+
properties.cta = { ...copy(24, 'Optional short call to action appropriate to the input. Use an empty string when this is purely informational.', 0), default: '' };
|
|
35
|
+
if (['note-marker', 'note-editorial', 'note-question'].includes(id))
|
|
36
|
+
properties.titleLines = { type: 'array', minItems: 0, maxItems: 4, default: [], items: copy(40, 'One complete phrase from the title, preserving its original language.'), description: 'Preferred title lines: one item for a single line, two to four for a multiline heading, or empty for automatic fitting. Concatenating the phrases must preserve the complete title and word boundaries. The renderer may reflow only when the requested lines cannot fit legibly.' };
|
|
37
|
+
if (id === 'note-marker')
|
|
38
|
+
properties.emphasis = { type: 'array', minItems: 0, maxItems: 2, default: [], items: copy(40, 'An exact contiguous phrase from title to highlight.'), description: 'Choose one or two important exact title phrases. Empty means the renderer highlights the final title line.' };
|
|
39
|
+
if (spec.value)
|
|
40
|
+
properties.value = copy(24, 'The focal quantity, price, time, or short value statement. Label fictional quantities as simulated.');
|
|
41
|
+
if (spec.attribution)
|
|
42
|
+
properties.attribution = copy(48, 'Exact supplied attribution, or an empty string. Never invent an endorsement.', 0);
|
|
43
|
+
if (spec.image)
|
|
44
|
+
properties.imageAssetId = asset();
|
|
45
|
+
if (spec.items) {
|
|
46
|
+
const item = { title: copy(id === 'faq' ? 40 : id === 'case-study' ? 18 : 20, id === 'faq' ? 'A concise audience question ending in a question mark.' : 'A short item label.'), description: copy(id === 'faq' ? 64 : spec.quantities || id === 'case-study' ? 32 : ['comparison', 'myth'].includes(id) ? 80 : supportingItemSlots.has(id) ? 48 : 40, 'Concise supporting copy.', spec.quantities ? 0 : 1) };
|
|
47
|
+
if (id === 'faq')
|
|
48
|
+
item.title.pattern = '^.+[??]$';
|
|
49
|
+
if (illustratedSlots.has(id))
|
|
50
|
+
item.illustration = { type: 'string', enum: COMPOSITION_ILLUSTRATIONS, default: 'structure', description: 'An editable native illustration that explains this item. Choose document for material, review for checking, collaboration for people, feedback for revision, sequence for ordered actions, comparison for alternatives, or structure for organization. Never omit the visual slot.' };
|
|
51
|
+
if (spec.itemImages)
|
|
52
|
+
item.assetId = asset();
|
|
53
|
+
if (spec.quantities) {
|
|
54
|
+
item.primaryValue = { type: 'number', minimum: 0, maximum: 100, description: spec.quantities === 'comparison' ? 'A percentage from zero to one hundred. primaryValue plus secondaryValue must equal exactly 100 in this item.' : 'A percentage from zero to one hundred. Never encode numbers inside descriptive copy.' };
|
|
55
|
+
if (spec.quantities !== 'share')
|
|
56
|
+
item.secondaryValue = { type: 'number', minimum: 0, maximum: 100, description: 'The comparison or benchmark percentage.' };
|
|
57
|
+
if (spec.quantities === 'share')
|
|
58
|
+
item.description = { const: '' };
|
|
59
|
+
}
|
|
60
|
+
properties.items = { type: 'array', minItems: spec.items, maxItems: spec.items, items: object(item), ...(spec.quantities === 'share' ? { description: 'Exactly three nonnegative percentage shares totaling at most one hundred.' } : {}) };
|
|
61
|
+
}
|
|
62
|
+
if (spec.quantities === 'comparison' || spec.quantities === 'benchmark')
|
|
63
|
+
properties.comparisonLabels = { type: 'array', minItems: 2, maxItems: 2, items: copy(20, 'A supplied or explicitly illustrative series label.') };
|
|
64
|
+
if (['note-question', 'note-marker', 'note-editorial'].includes(id))
|
|
65
|
+
properties.decorationEmoji = { type: 'string', enum: ['', '✨', '💡', '🌱', '💬', '🎯', '📚'], default: '', description: 'Optional built-in decorative emoji for informal social notes. Choose one relevant symbol, or empty for the authored illustration. Never use it as factual evidence.' };
|
|
66
|
+
properties.textPresentation = textPresentationSchema();
|
|
67
|
+
const contentKeys = Object.keys(properties).filter(key => !['textPresentation', 'decorationEmoji'].includes(key));
|
|
68
|
+
const importance = Object.fromEntries(contentKeys.map(key => [key, key === 'title' ? 3 : key === 'items' ? 7 : ['eyebrow', 'cta'].includes(key) ? .5 : 2]));
|
|
69
|
+
return { parameterModel: 'native-social', parameterSchema: object(properties), importance, generationPolicy: Object.fromEntries(contentKeys.map(key => [key, provenance])) };
|
|
70
|
+
}
|
|
71
|
+
/** Apply the small closed schema vocabulary used by native slots on every local render. */
|
|
72
|
+
export function validateNativeSocialParameters(id, raw) {
|
|
73
|
+
const check = (value, schema) => {
|
|
74
|
+
if (Object.hasOwn(schema, 'const'))
|
|
75
|
+
return value === schema.const;
|
|
76
|
+
if (Array.isArray(schema.enum))
|
|
77
|
+
return schema.enum.includes(value);
|
|
78
|
+
if (schema.type === 'string')
|
|
79
|
+
return typeof value === 'string' && Array.from(value).length >= Number(schema.minLength || 0) && Array.from(value).length <= Number(schema.maxLength) && (!schema.pattern || new RegExp(String(schema.pattern), 'u').test(value));
|
|
80
|
+
if (schema.type === 'boolean')
|
|
81
|
+
return typeof value === 'boolean';
|
|
82
|
+
if (schema.type === 'number')
|
|
83
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= Number(schema.minimum) && value <= Number(schema.maximum);
|
|
84
|
+
if (schema.type === 'array')
|
|
85
|
+
return Array.isArray(value) && value.length >= Number(schema.minItems) && value.length <= Number(schema.maxItems) && value.every(item => check(item, schema.items));
|
|
86
|
+
if (schema.type === 'object') {
|
|
87
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
88
|
+
return false;
|
|
89
|
+
const properties = schema.properties, object = value;
|
|
90
|
+
return Object.keys(object).every(key => Object.hasOwn(properties, key) && check(object[key], properties[key])) && schema.required.every(key => Object.hasOwn(object, key) && check(object[key], properties[key]));
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
};
|
|
94
|
+
const schema = nativeSocialParameterContract(id).parameterSchema;
|
|
95
|
+
raw = compositionParameterDefaults(schema, raw);
|
|
96
|
+
if (!check(raw, schema))
|
|
97
|
+
throw new Error('CANDIDATE_INVALID_PARAMETERS');
|
|
98
|
+
const values = raw;
|
|
99
|
+
validateTextPresentations(values.textPresentation, values);
|
|
100
|
+
if (values.titleLines?.length) {
|
|
101
|
+
const normalize = (copy) => copy.normalize('NFC').replace(/\s+/gu, ' ').trim();
|
|
102
|
+
const title = normalize(values.title);
|
|
103
|
+
let cursor = 0;
|
|
104
|
+
for (const phrase of values.titleLines) {
|
|
105
|
+
const line = normalize(phrase), hadSpace = title[cursor] === ' ';
|
|
106
|
+
if (hadSpace)
|
|
107
|
+
cursor++;
|
|
108
|
+
// CJK phrases may meet without spaces, but Latin words cannot be split.
|
|
109
|
+
const brokenWord = cursor > 0 && !hadSpace && /[\p{Script=Latin}\p{Mark}\d’'-]/u.test(title[cursor - 1]) && /^[\p{Script=Latin}\p{Mark}\d’'-]/u.test(line);
|
|
110
|
+
if (!line || brokenWord || !title.startsWith(line, cursor))
|
|
111
|
+
throw new Error('CANDIDATE_INVALID_TITLE_LINES');
|
|
112
|
+
cursor += line.length;
|
|
113
|
+
}
|
|
114
|
+
if (cursor !== title.length)
|
|
115
|
+
throw new Error('CANDIDATE_INVALID_TITLE_LINES');
|
|
116
|
+
}
|
|
117
|
+
if (values.emphasis?.some(phrase => !values.title.includes(phrase)))
|
|
118
|
+
throw new Error('CANDIDATE_INVALID_EMPHASIS');
|
|
119
|
+
if (id === 'usage-breakdown' && values.items.reduce((sum, item) => sum + item.primaryValue, 0) > 100.0001)
|
|
120
|
+
throw new Error('CANDIDATE_INVALID_PERCENTAGES');
|
|
121
|
+
if (id === 'split-bar-compare' && values.items.some(item => Math.abs(item.primaryValue + item.secondaryValue - 100) > .001))
|
|
122
|
+
throw new Error('CANDIDATE_INVALID_PERCENTAGES');
|
|
123
|
+
return JSON.parse(JSON.stringify(values));
|
|
124
|
+
}
|
|
125
|
+
/** Numeric and image identities remain available when a native design becomes editable content. */
|
|
126
|
+
export function nativeSocialContentPage(values, original) {
|
|
127
|
+
const proof = original.evidence, blocks = [{ id: 'native-body', kind: 'paragraph', text: values.body, evidence: proof }];
|
|
128
|
+
if (values.items?.length)
|
|
129
|
+
blocks.push({ id: 'native-items', kind: 'features', evidence: proof, entries: values.items.map(item => ({ title: item.title, ...(item.illustration ? { illustration: item.illustration } : {}), description: [item.description, ...[item.primaryValue, item.secondaryValue].filter(value => value !== undefined).map(value => `${value}%`)].filter(Boolean).join(' · ') })) });
|
|
130
|
+
for (const field of ['value', 'attribution', 'eyebrow', 'cta'])
|
|
131
|
+
if (values[field])
|
|
132
|
+
blocks.push({ id: `native-${field}`, kind: 'paragraph', text: values[field], evidence: proof });
|
|
133
|
+
if (values.comparisonLabels)
|
|
134
|
+
blocks.push({ id: 'native-series', kind: 'paragraph', evidence: proof, text: values.comparisonLabels.join(' / ') });
|
|
135
|
+
const images = [values.imageAssetId, ...(values.items || []).map(item => item.assetId)].filter((id) => !!id);
|
|
136
|
+
for (const [index, assetId] of images.entries())
|
|
137
|
+
blocks.push({ id: `native-image-${index}`, kind: 'image', evidence: [{ sourceId: assetId }], sourceId: assetId, assetId, alt: values.title, treatment: assetId === 'neatico-editorial-placeholder' ? 'diagram' : 'photograph', status: 'available' });
|
|
138
|
+
return { ...original, title: values.title, blocks };
|
|
139
|
+
}
|
package/dist/pagination.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/** Split only at explicit word/sentence boundaries. Never cut identifiers,
|
|
2
2
|
* decimal values, or unbroken URLs to force them onto a smaller canvas. */
|
|
3
3
|
export function splitCopy(copy) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
let at;
|
|
5
|
+
let distance = Infinity;
|
|
6
|
+
const midpoint = copy.length / 2;
|
|
7
|
+
for (const match of copy.matchAll(/\s+|[。!?;]|[.!?;](?=\s|$)/gu)) {
|
|
8
|
+
const index = match.index + match[0].length;
|
|
9
|
+
if (index <= 0 || index >= copy.length)
|
|
10
|
+
continue;
|
|
11
|
+
const candidateDistance = Math.abs(index - midpoint);
|
|
12
|
+
if (candidateDistance < distance) {
|
|
13
|
+
at = index;
|
|
14
|
+
distance = candidateDistance;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (at === undefined)
|
|
8
18
|
return;
|
|
9
|
-
const at = breaks.sort((a, b) => Math.abs(a - copy.length / 2) - Math.abs(b - copy.length / 2))[0];
|
|
10
19
|
return [copy.slice(0, at), copy.slice(at)];
|
|
11
20
|
}
|
|
12
21
|
/** No paraphrasing, invented continuation labels or removed constraints. The
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Apply only renderer-authored defaults. Never rewrite supplied copy or values. */
|
|
2
|
+
export function compositionParameterDefaults(schema, value) {
|
|
3
|
+
if (value === undefined && Object.hasOwn(schema, 'default'))
|
|
4
|
+
return JSON.parse(JSON.stringify(schema.default));
|
|
5
|
+
if (schema.type === 'object' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
6
|
+
const next = { ...value };
|
|
7
|
+
for (const [key, child] of Object.entries(schema.properties || {})) {
|
|
8
|
+
const resolved = compositionParameterDefaults(child, next[key]);
|
|
9
|
+
if (resolved !== undefined)
|
|
10
|
+
next[key] = resolved;
|
|
11
|
+
}
|
|
12
|
+
return next;
|
|
13
|
+
}
|
|
14
|
+
if (schema.type === 'array' && Array.isArray(value))
|
|
15
|
+
return value.map((item, index) => compositionParameterDefaults((Array.isArray(schema.items) ? schema.items[index] : schema.items) || {}, item));
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type DesignRecipe } from "./designSystem.js";
|
|
2
|
+
export type ParameterSchema = Readonly<Record<string, unknown>>;
|
|
3
|
+
export type CompositionKind = "presentation" | "social" | "article-cover" | "article-illustration";
|
|
4
|
+
export interface RecipeParameterContract {
|
|
5
|
+
parameterModel?: "semantic" | "native-social";
|
|
6
|
+
id: string;
|
|
7
|
+
schemaVersion: 1;
|
|
8
|
+
supportedKinds: readonly CompositionKind[];
|
|
9
|
+
semanticDescription: string;
|
|
10
|
+
scenes: readonly string[];
|
|
11
|
+
rendererKey: string;
|
|
12
|
+
capacity: {
|
|
13
|
+
minEntries: number;
|
|
14
|
+
maxEntries: number;
|
|
15
|
+
requiresImage: boolean;
|
|
16
|
+
};
|
|
17
|
+
parameterSchema: ParameterSchema;
|
|
18
|
+
importance: Readonly<Record<string, number>>;
|
|
19
|
+
generationPolicy: Readonly<Record<string, readonly string[]>>;
|
|
20
|
+
themeBindings: readonly string[];
|
|
21
|
+
fallbackPolicy: {
|
|
22
|
+
missingContent: "derive-or-labeled-example";
|
|
23
|
+
missingAsset: "search-or-native-placeholder";
|
|
24
|
+
omitCandidate: false;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Fixed renderer parameters, not executable layout trees. The complete schema
|
|
28
|
+
* travels with each formal generation batch; summaries are scheduling aids only.
|
|
29
|
+
*/
|
|
30
|
+
export declare function recipeParameterContract(recipe: DesignRecipe, supportedKinds?: RecipeParameterContract["supportedKinds"]): RecipeParameterContract;
|
|
31
|
+
/** Shared recipes are one part of the formal catalog; host-specific renderer
|
|
32
|
+
* definitions register through the same contract constructor during migration.
|
|
33
|
+
*/
|
|
34
|
+
export declare const RECIPE_PARAMETER_CONTRACTS: readonly RecipeParameterContract[];
|