@neatico/visual-core 0.0.0-stage → 0.14.1
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/LICENSE +21 -0
- package/README.md +244 -2
- package/dist/blueprints.d.ts +20 -0
- package/dist/blueprints.js +43 -0
- package/dist/cjs/blueprints.js +48 -0
- package/dist/cjs/composition.js +68 -0
- package/dist/cjs/compositionScenes.js +51 -0
- package/dist/cjs/content.js +242 -0
- package/dist/cjs/designBlueprint.js +34 -0
- package/dist/cjs/designSystem.js +126 -0
- package/dist/cjs/editorial.js +41 -0
- package/dist/cjs/fontCatalog.js +42 -0
- package/dist/cjs/geometry.js +89 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/layoutContracts.js +27 -0
- package/dist/cjs/layouts.js +103 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pagination.js +36 -0
- package/dist/cjs/scenes.js +31 -0
- package/dist/cjs/socialLayouts.js +471 -0
- package/dist/cjs/text.js +87 -0
- package/dist/composition.d.ts +17 -0
- package/dist/composition.js +63 -0
- package/dist/compositionScenes.d.ts +19 -0
- package/dist/compositionScenes.js +47 -0
- package/dist/content.d.ts +104 -0
- package/dist/content.js +234 -0
- package/dist/designBlueprint.d.ts +26 -0
- package/dist/designBlueprint.js +29 -0
- package/dist/designSystem.d.ts +58 -0
- package/dist/designSystem.js +119 -0
- package/dist/editorial.d.ts +6 -0
- package/dist/editorial.js +35 -0
- package/dist/fontCatalog.d.ts +11 -0
- package/dist/fontCatalog.js +36 -0
- package/dist/geometry.d.ts +23 -0
- package/dist/geometry.js +83 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/layoutContracts.d.ts +15 -0
- package/dist/layoutContracts.js +22 -0
- package/dist/layouts.d.ts +2 -0
- package/dist/layouts.js +100 -0
- package/dist/pagination.d.ts +14 -0
- package/dist/pagination.js +32 -0
- package/dist/scenes.d.ts +7 -0
- package/dist/scenes.js +28 -0
- package/dist/socialLayouts.d.ts +390 -0
- package/dist/socialLayouts.js +468 -0
- package/dist/text.d.ts +8 -0
- package/dist/text.js +81 -0
- package/package.json +55 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 PowerDuckie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,245 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://www.neatico.com/?utm_source=npm&utm_medium=readme&utm_campaign=visual-core">
|
|
3
|
+
<img src="https://www.neatico.com/brand/neatico-logo.svg" width="240" alt="Neatico" />
|
|
4
|
+
</a>
|
|
5
|
+
<h1>@neatico/visual-core</h1>
|
|
6
|
+
<p>Framework-independent geometry, measured text wrapping, and extensible scene registries for visual editors.</p>
|
|
7
|
+
<p>
|
|
8
|
+
<a href="https://www.npmjs.com/package/@neatico/visual-core"><img src="https://img.shields.io/npm/v/@neatico/visual-core.svg?style=flat-square&label=npm" alt="npm version" /></a>
|
|
9
|
+
<a href="https://github.com/neatico/visual-core"><img src="https://img.shields.io/badge/source-github-111827?style=flat-square" alt="source" /></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/license-MIT-10b981?style=flat-square" alt="license: MIT" />
|
|
11
|
+
<img src="https://img.shields.io/badge/node-%3E%3D20-6366f1?style=flat-square" alt="node >=20" />
|
|
12
|
+
</p>
|
|
13
|
+
</div>
|
|
2
14
|
|
|
3
|
-
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
`@neatico/visual-core` is the layout engine that powers Neatico's AI design canvas. It is pure, data-first, and free of React, the DOM, a database, and any AI provider — so you can run it on a server, in a worker, or inside your own editor.
|
|
18
|
+
|
|
19
|
+
It provides the pieces a serious visual editor needs:
|
|
20
|
+
|
|
21
|
+
- **Affine geometry** — group/child transforms that survive ungrouping.
|
|
22
|
+
- **Measured text wrapping** — UAX #14 line breaks driven by *your* font metrics.
|
|
23
|
+
- **Source-backed content model** — validate untrusted AI output without trusting generated code or asset URLs.
|
|
24
|
+
- **Data-only compositions & blueprints** — portable, versioned layout descriptions with no executable markup.
|
|
25
|
+
- **Layout eligibility** — 97 presentation and 97 social layouts with semantic contracts.
|
|
26
|
+
- **Themes, fonts, and pagination** — deterministic token sets and lossless content splitting.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install @neatico/visual-core
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Ships as native ES modules, CommonJS, and TypeScript declarations. Requires Node.js 20+ (the only runtime dependency is the MIT-licensed [`linebreak`](https://github.com/foliojs/linebreak) for Unicode line opportunities).
|
|
35
|
+
|
|
36
|
+
## Contents
|
|
37
|
+
|
|
38
|
+
- [Preserve transforms when ungrouping](#preserve-transforms-when-ungrouping)
|
|
39
|
+
- [Wrap text with your font measurements](#wrap-text-with-your-font-measurements)
|
|
40
|
+
- [Add custom scenes](#add-custom-scenes)
|
|
41
|
+
- [Validate AI-authored content](#validate-ai-authored-content)
|
|
42
|
+
- [Validate compositions and blueprints](#validate-compositions-and-blueprints)
|
|
43
|
+
- [Select layout candidates](#select-layout-candidates)
|
|
44
|
+
- [Paginate without losing content](#paginate-without-losing-content)
|
|
45
|
+
- [Themes, fonts, and fingerprints](#themes-fonts-and-fingerprints)
|
|
46
|
+
- [API surface](#api-surface)
|
|
47
|
+
|
|
48
|
+
## Preserve transforms when ungrouping
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { flattenGroupLayout } from "@neatico/visual-core";
|
|
52
|
+
|
|
53
|
+
const group = { x: 100, y: 50, width: 400, height: 300, rotation: 30 };
|
|
54
|
+
const child = { x: 20, y: 30, width: 120, height: 60, rotation: 15 };
|
|
55
|
+
const ungrouped = flattenGroupLayout(group, child);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Coordinates are in logical canvas units; rotations and skew are in degrees with the element center as the origin. `flattenGroupLayout` preserves additional child properties and returns a new object. It supports translation, rotation, scaling, skew, and horizontal/vertical flips. Nonfinite values, negative dimensions, and degenerate transforms fail explicitly.
|
|
59
|
+
|
|
60
|
+
Also exported: `layoutMatrix`, `multiplyMatrix`, `layoutBounds`, plus the `Layout` and `Matrix` types. Matrices use the six-value CSS affine format `[a, b, c, d, e, f]`.
|
|
61
|
+
|
|
62
|
+
## Wrap text with your font measurements
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { wrapCopy } from "@neatico/visual-core";
|
|
66
|
+
|
|
67
|
+
const ctx = document.createElement("canvas").getContext("2d")!;
|
|
68
|
+
await document.fonts.load("400 24px Inter");
|
|
69
|
+
|
|
70
|
+
const lines = wrapCopy(
|
|
71
|
+
"One idea. Every format.",
|
|
72
|
+
240,
|
|
73
|
+
24,
|
|
74
|
+
false,
|
|
75
|
+
(text, size, bold) => {
|
|
76
|
+
ctx.font = `${bold ? 700 : 400} ${size}px Inter`;
|
|
77
|
+
return ctx.measureText(text).width;
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
You supply measurements for the actual font the renderer uses. Explicit line breaks are preserved; opportunities follow UAX #14 (Unicode 13), oversized tokens use grapheme boundaries via `Intl.Segmenter`, and a grapheme wider than the line fails rather than silently clipping. Tiny trailing lines are balanced unless you pass `{ balance: false }`. Titles can use `{ keepWords: true, preferClauseBreaks: true }`. Returned lines are plain text — escape them before inserting into HTML.
|
|
83
|
+
|
|
84
|
+
## Add custom scenes
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createSceneRegistry } from "@neatico/visual-core";
|
|
88
|
+
|
|
89
|
+
type Brief = { headline: string };
|
|
90
|
+
type Scene = { text: string; accent: string };
|
|
91
|
+
|
|
92
|
+
const scenes = createSceneRegistry<Brief, Scene>();
|
|
93
|
+
const unregister = scenes.register("product-launch", (brief) => ({
|
|
94
|
+
text: brief.headline,
|
|
95
|
+
accent: "#6950e8",
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
const scene = scenes.render("product-launch", { headline: "Meet your next idea." });
|
|
99
|
+
unregister();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each registry is isolated; duplicate names and unknown scenes throw. Renderers own their input validation and output schema, so async renderers and custom canvas/SVG representations are supported. No global registry is created.
|
|
103
|
+
|
|
104
|
+
## Validate AI-authored content
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { parseSource, validateContentPlan } from "@neatico/visual-core";
|
|
108
|
+
|
|
109
|
+
const source = parseSource(rawInputText);
|
|
110
|
+
const plan = validateContentPlan(aiJson, source, "presentation");
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`validateContentPlan` is a hard boundary for untrusted model output. It strips unknown properties, never trusts generated code or asset URLs, and verifies content against the source ranges. File-name placeholders are treated as references, not fabricated assets. Use `validateEditableContentPage` for editor-authored pages (pass `{ allowIncomplete: true }` while the user is still editing).
|
|
114
|
+
|
|
115
|
+
## Validate compositions and blueprints
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import {
|
|
119
|
+
validateComposition,
|
|
120
|
+
compositionFields,
|
|
121
|
+
createBlueprintRegistry,
|
|
122
|
+
blueprintLabel,
|
|
123
|
+
} from "@neatico/visual-core";
|
|
124
|
+
|
|
125
|
+
const fields = compositionFields(aiContent);
|
|
126
|
+
const composition = validateComposition(aiComposition, fields);
|
|
127
|
+
|
|
128
|
+
const catalog = createBlueprintRegistry();
|
|
129
|
+
const unregister = catalog.register({
|
|
130
|
+
version: 1,
|
|
131
|
+
id: "editorial-introduction",
|
|
132
|
+
labels: { en: "Editorial introduction", zh: "编辑式导言" },
|
|
133
|
+
fields: ["title", "body"],
|
|
134
|
+
ratios: [1, 16 / 9],
|
|
135
|
+
intents: ["cover"],
|
|
136
|
+
composition: {
|
|
137
|
+
version: 1,
|
|
138
|
+
root: {
|
|
139
|
+
type: "column",
|
|
140
|
+
children: [
|
|
141
|
+
{ type: "text", bind: "title", role: "hero" },
|
|
142
|
+
{ type: "text", bind: "body", role: "body" },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const choices = catalog.find(["title", "body"], 16 / 9, "cover");
|
|
149
|
+
const name = blueprintLabel(choices[0], "zh-CN");
|
|
150
|
+
unregister();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Compositions and blueprints are **data-only** (`version: 1`). There are no coordinates, CSS, HTML, or URLs in the tree; every text node binds exactly one existing, nonempty content field. Blueprints can declare semantic `intents`; a cover-only template will never be picked for a closing slide. Field matching is exact, so a template cannot silently drop extra user content. Locale lookup tries the exact locale, then the language, then English.
|
|
154
|
+
|
|
155
|
+
Structural validation is not visual approval. Before registering AI-authored templates in production, render representative samples at every supported ratio with real font metrics and check bounds, collisions, readability, and content preservation. The host owns persistence, permissions, review, and migration.
|
|
156
|
+
|
|
157
|
+
## Select layout candidates
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { eligibleLayouts, layoutCandidates } from "@neatico/visual-core";
|
|
161
|
+
|
|
162
|
+
const eligible = eligibleLayouts("presentation", contentPage, 16 / 9);
|
|
163
|
+
const shortlist = layoutCandidates("presentation", "cover", contentPage, 16 / 9, 5);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`LAYOUT_CONTRACTS` covers all 97 presentation and 97 social layout IDs with semantic intent, item capacity, required fields, and ratio bounds. Eligibility is a first filter only — it never replaces measured fit. The host must still measure emitted geometry, confirm every source field is visible, and reject or paginate unreadable output.
|
|
167
|
+
|
|
168
|
+
## Paginate without losing content
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { splitCopy, splitContent } from "@neatico/visual-core";
|
|
172
|
+
|
|
173
|
+
const copySplit = splitCopy(longParagraph); // [head, tail] | undefined
|
|
174
|
+
const pageSplit = splitContent(overflowingPage); // [pageA, pageB] | undefined
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Splits happen only at explicit word/sentence boundaries. Identifiers, decimal values, and unbroken URLs are never cut to force them onto a smaller canvas. The host keeps the title and context on each emitted page and remeasures.
|
|
178
|
+
|
|
179
|
+
## Themes, fonts, and fingerprints
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import {
|
|
183
|
+
designTheme,
|
|
184
|
+
designCandidates,
|
|
185
|
+
structureFingerprint,
|
|
186
|
+
recommendedFonts,
|
|
187
|
+
} from "@neatico/visual-core";
|
|
188
|
+
|
|
189
|
+
const theme = designTheme("ink");
|
|
190
|
+
const recipes = designCandidates(page, { kind: "social", ratio: 1, limit: 5 });
|
|
191
|
+
|
|
192
|
+
const fp = structureFingerprint(placedItems, canvasWidth, canvasHeight);
|
|
193
|
+
const fonts = recommendedFonts("ja-JP", japaneseHeadline);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- `DESIGN_THEMES` / `designTheme` expose complete, deterministic palette, typography, and geometry tokens.
|
|
197
|
+
- `designCandidates` ranks authored catalog recipes against a page, excluding already-used families.
|
|
198
|
+
- `structureFingerprint` ignores color, copy, and generated IDs — a preview that only recolors has the same fingerprint.
|
|
199
|
+
- `recommendedFonts`, `contentScript`, and `languageFont` resolve the curated Google Fonts catalog to scripts and supported weights.
|
|
200
|
+
|
|
201
|
+
## API surface
|
|
202
|
+
|
|
203
|
+
| Module | Key exports |
|
|
204
|
+
|---|---|
|
|
205
|
+
| Geometry | `layoutMatrix`, `multiplyMatrix`, `flattenGroupLayout`, `layoutBounds`, `Layout`, `Matrix` |
|
|
206
|
+
| Text | `wrapCopy`, `Measure`, `WrapOptions` |
|
|
207
|
+
| Scenes | `createSceneRegistry`, `SceneRenderer` |
|
|
208
|
+
| Content | `parseSource`, `validateContentPlan`, `validateEditableContentPage`, `contentBlockText`, `ContentPlan`, `ContentPage`, `ContentBlock` |
|
|
209
|
+
| Composition | `validateComposition`, `compositionFields`, `COMPOSITION_PROMPT`, `Composition` |
|
|
210
|
+
| Blueprints | `validateBlueprint`, `createBlueprintRegistry`, `blueprintLabel` |
|
|
211
|
+
| Design blueprints | `validateDesignBlueprint`, `blueprintFitsContent`, `DesignBlueprint` |
|
|
212
|
+
| Layout contracts | `LAYOUT_CONTRACTS`, `eligibleLayouts`, `layoutCandidates` |
|
|
213
|
+
| Layout catalogs | `PRESENTATION_LAYOUT_IDS`, `SOCIAL_LAYOUT_CATALOG`, `COMPOSITION_SCENES` |
|
|
214
|
+
| Pagination | `splitCopy`, `splitContent` |
|
|
215
|
+
| Design system | `DESIGN_THEMES`, `designTheme`, `DESIGN_RECIPES`, `designCandidates`, `structureFingerprint` |
|
|
216
|
+
| Editorial | `readingUnits`, `needsEditorialPagination`, `ensurePresentationBookends` |
|
|
217
|
+
| Fonts | `GOOGLE_FONT_CATALOG`, `contentScript`, `recommendedFonts`, `languageFont` |
|
|
218
|
+
|
|
219
|
+
## Design philosophy
|
|
220
|
+
|
|
221
|
+
Neatico generates genuinely different designs for different stories, not the same template recolored. That principle is encoded here: eligibility is structural but measured fit stays mandatory, compositions are data-only, and registries are isolated so an application can bring its own renderers and approve its own templates. Every validator fails explicitly rather than guessing.
|
|
222
|
+
|
|
223
|
+
## Development
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
npm install
|
|
227
|
+
npm test # builds ESM/CJS, then runs node:test
|
|
228
|
+
npm pack --dry-run
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Tests cover transform equivalence, invalid geometry, text wrapping, content validation, registry isolation, and pagination. `npm pack` runs the build and tests before assembling the package; only compiled output, this README, and the license are published.
|
|
232
|
+
|
|
233
|
+
## Meet Neatico
|
|
234
|
+
|
|
235
|
+
This package is the open-source layout core of **[Neatico](https://www.neatico.com/?utm_source=npm&utm_medium=readme&utm_campaign=visual-core)** — the AI workspace that turns a single idea into pixel-perfect, fully editable visuals. Describe it once and Neatico designs social posts, presentations, and marketing graphics with real layouts, measured typography, your brand kit, AI imagery and background removal, and one-click PPTX/PNG export.
|
|
236
|
+
|
|
237
|
+
The `@neatico/*` packages are the framework-independent pieces extracted from that product so you can build your own editor, generate-and-go pipeline, or scene extension. Want the finished experience instead of the primitives? **[Start creating free on neatico.com →](https://www.neatico.com/?utm_source=npm&utm_medium=readme&utm_campaign=visual-core)**
|
|
238
|
+
|
|
239
|
+
## Contributing
|
|
240
|
+
|
|
241
|
+
Open an issue or pull request at [neatico/visual-core](https://github.com/neatico/visual-core) with a minimal reproduction and a regression test. Keep APIs independent of application state and use American English in documentation and code comments.
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Composition } from './composition.js';
|
|
2
|
+
/** Portable AI-authored template. Applications own approval and visual checks. */
|
|
3
|
+
export interface Blueprint {
|
|
4
|
+
version: 1;
|
|
5
|
+
id: string;
|
|
6
|
+
labels: Record<string, string>;
|
|
7
|
+
fields: string[];
|
|
8
|
+
ratios: number[];
|
|
9
|
+
/** Optional semantic layout/scene IDs. Omit only for intentionally generic layouts. */
|
|
10
|
+
intents?: string[];
|
|
11
|
+
composition: Composition;
|
|
12
|
+
}
|
|
13
|
+
export declare function validateBlueprint(raw: unknown): Blueprint;
|
|
14
|
+
/** Register only after the application has approved representative rendered samples. */
|
|
15
|
+
export declare function createBlueprintRegistry(): {
|
|
16
|
+
register(raw: unknown): () => void;
|
|
17
|
+
list(): Blueprint[];
|
|
18
|
+
find(fields: string[], ratio: number, intent?: string): Blueprint[];
|
|
19
|
+
};
|
|
20
|
+
export declare function blueprintLabel(blueprint: Blueprint, locale: string): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { validateComposition } from './composition.js';
|
|
2
|
+
export function validateBlueprint(raw) {
|
|
3
|
+
const value = raw;
|
|
4
|
+
if (!value || value.version !== 1 || typeof value.id !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(value.id))
|
|
5
|
+
throw new Error('Invalid blueprint identity');
|
|
6
|
+
if (!value.labels || typeof value.labels !== 'object' || Array.isArray(value.labels) || !value.labels.en)
|
|
7
|
+
throw new Error('Missing blueprint labels');
|
|
8
|
+
const labels = {};
|
|
9
|
+
for (const [locale, label] of Object.entries(value.labels)) {
|
|
10
|
+
if (!/^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(locale) || typeof label !== 'string' || !label.trim() || label.length > 120)
|
|
11
|
+
throw new Error('Invalid blueprint label');
|
|
12
|
+
labels[locale] = label.trim();
|
|
13
|
+
}
|
|
14
|
+
if (!Array.isArray(value.fields) || !value.fields.length || value.fields.length > 40 || new Set(value.fields).size !== value.fields.length || value.fields.some(field => typeof field !== 'string' || !/^(title|headline|body|eyebrow|cta|value|attribution|items\.\d{1,2}\.(title|description))$/.test(field)))
|
|
15
|
+
throw new Error('Invalid blueprint fields');
|
|
16
|
+
if (!Array.isArray(value.ratios) || !value.ratios.length || value.ratios.length > 16 || value.ratios.some(r => !Number.isFinite(r) || r < .25 || r > 4))
|
|
17
|
+
throw new Error('Invalid blueprint ratios');
|
|
18
|
+
if (value.intents !== undefined && (!Array.isArray(value.intents) || !value.intents.length || value.intents.length > 32 || value.intents.some(intent => typeof intent !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(intent))))
|
|
19
|
+
throw new Error('Invalid blueprint intents');
|
|
20
|
+
return { version: 1, id: value.id, labels, ...(value.intents ? { intents: [...new Set(value.intents)] } : {}), fields: [...value.fields], ratios: [...new Set(value.ratios)], composition: validateComposition(value.composition, Object.fromEntries(value.fields.map(field => [field, 'content']))) };
|
|
21
|
+
}
|
|
22
|
+
/** Register only after the application has approved representative rendered samples. */
|
|
23
|
+
export function createBlueprintRegistry() {
|
|
24
|
+
const entries = new Map();
|
|
25
|
+
const copy = (entry) => validateBlueprint(entry);
|
|
26
|
+
return {
|
|
27
|
+
register(raw) {
|
|
28
|
+
const entry = validateBlueprint(raw);
|
|
29
|
+
if (entries.has(entry.id))
|
|
30
|
+
throw new Error('Blueprint already registered');
|
|
31
|
+
entries.set(entry.id, entry);
|
|
32
|
+
return () => { if (entries.get(entry.id) === entry)
|
|
33
|
+
entries.delete(entry.id); };
|
|
34
|
+
},
|
|
35
|
+
list() { return [...entries.values()].map(copy); },
|
|
36
|
+
find(fields, ratio, intent) {
|
|
37
|
+
return [...entries.values()].filter(entry => (!entry.intents || (intent !== undefined && entry.intents.includes(intent))) && entry.fields.length === fields.length && entry.fields.every(field => fields.includes(field)) && entry.ratios.some(r => Math.abs(r - ratio) < .001)).map(copy);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function blueprintLabel(blueprint, locale) {
|
|
42
|
+
return blueprint.labels[locale] || blueprint.labels[locale.split('-')[0]] || blueprint.labels.en;
|
|
43
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateBlueprint = validateBlueprint;
|
|
4
|
+
exports.createBlueprintRegistry = createBlueprintRegistry;
|
|
5
|
+
exports.blueprintLabel = blueprintLabel;
|
|
6
|
+
const composition_js_1 = require("./composition.js");
|
|
7
|
+
function validateBlueprint(raw) {
|
|
8
|
+
const value = raw;
|
|
9
|
+
if (!value || value.version !== 1 || typeof value.id !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(value.id))
|
|
10
|
+
throw new Error('Invalid blueprint identity');
|
|
11
|
+
if (!value.labels || typeof value.labels !== 'object' || Array.isArray(value.labels) || !value.labels.en)
|
|
12
|
+
throw new Error('Missing blueprint labels');
|
|
13
|
+
const labels = {};
|
|
14
|
+
for (const [locale, label] of Object.entries(value.labels)) {
|
|
15
|
+
if (!/^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(locale) || typeof label !== 'string' || !label.trim() || label.length > 120)
|
|
16
|
+
throw new Error('Invalid blueprint label');
|
|
17
|
+
labels[locale] = label.trim();
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(value.fields) || !value.fields.length || value.fields.length > 40 || new Set(value.fields).size !== value.fields.length || value.fields.some(field => typeof field !== 'string' || !/^(title|headline|body|eyebrow|cta|value|attribution|items\.\d{1,2}\.(title|description))$/.test(field)))
|
|
20
|
+
throw new Error('Invalid blueprint fields');
|
|
21
|
+
if (!Array.isArray(value.ratios) || !value.ratios.length || value.ratios.length > 16 || value.ratios.some(r => !Number.isFinite(r) || r < .25 || r > 4))
|
|
22
|
+
throw new Error('Invalid blueprint ratios');
|
|
23
|
+
if (value.intents !== undefined && (!Array.isArray(value.intents) || !value.intents.length || value.intents.length > 32 || value.intents.some(intent => typeof intent !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(intent))))
|
|
24
|
+
throw new Error('Invalid blueprint intents');
|
|
25
|
+
return { version: 1, id: value.id, labels, ...(value.intents ? { intents: [...new Set(value.intents)] } : {}), fields: [...value.fields], ratios: [...new Set(value.ratios)], composition: (0, composition_js_1.validateComposition)(value.composition, Object.fromEntries(value.fields.map(field => [field, 'content']))) };
|
|
26
|
+
}
|
|
27
|
+
/** Register only after the application has approved representative rendered samples. */
|
|
28
|
+
function createBlueprintRegistry() {
|
|
29
|
+
const entries = new Map();
|
|
30
|
+
const copy = (entry) => validateBlueprint(entry);
|
|
31
|
+
return {
|
|
32
|
+
register(raw) {
|
|
33
|
+
const entry = validateBlueprint(raw);
|
|
34
|
+
if (entries.has(entry.id))
|
|
35
|
+
throw new Error('Blueprint already registered');
|
|
36
|
+
entries.set(entry.id, entry);
|
|
37
|
+
return () => { if (entries.get(entry.id) === entry)
|
|
38
|
+
entries.delete(entry.id); };
|
|
39
|
+
},
|
|
40
|
+
list() { return [...entries.values()].map(copy); },
|
|
41
|
+
find(fields, ratio, intent) {
|
|
42
|
+
return [...entries.values()].filter(entry => (!entry.intents || (intent !== undefined && entry.intents.includes(intent))) && entry.fields.length === fields.length && entry.fields.every(field => fields.includes(field)) && entry.ratios.some(r => Math.abs(r - ratio) < .001)).map(copy);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function blueprintLabel(blueprint, locale) {
|
|
47
|
+
return blueprint.labels[locale] || blueprint.labels[locale.split('-')[0]] || blueprint.labels.en;
|
|
48
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COMPOSITION_PROMPT = void 0;
|
|
4
|
+
exports.compositionFields = compositionFields;
|
|
5
|
+
exports.validateComposition = validateComposition;
|
|
6
|
+
exports.COMPOSITION_PROMPT = `Also return composition:{version:1,root:node}. Design a hierarchy from the content, not a fixed template. A node is {type,weight?,tone?,role?,bind?,icon?,children?}. row and column contain 2-4 children; nest up to 5 levels, at most 40 nodes. Weight is relative space 0.5-4. Tone plain/card/contrast applies to groups. Text binds exactly one existing nonempty content field (title or headline, body, eyebrow, cta, value, attribution, items.0.title, items.0.description, etc); never bind notes. Include EVERY nonempty content field exactly once. Text role hero/heading/body/caption controls typography. Image is an optional original conceptual illustration, not invented evidence or a photo. Icon optionally uses idea/check/chart/people/shield/arrow. Divider separates related content. No coordinates, CSS, HTML, URLs or text in the tree. Use one strong hero, purposeful contrast, asymmetric columns, grouped evidence, and breathing room. Rows reflow for narrow formats. Different stories should have different trees, not just different colors.`;
|
|
7
|
+
function compositionFields(value) {
|
|
8
|
+
const fields = {};
|
|
9
|
+
for (const key of ['title', 'headline', 'body', 'eyebrow', 'cta', 'value', 'attribution'])
|
|
10
|
+
if (typeof value[key] === 'string' && value[key].trim())
|
|
11
|
+
fields[key] = value[key];
|
|
12
|
+
if (Array.isArray(value.items))
|
|
13
|
+
value.items.forEach((item, i) => { if (!item || typeof item !== 'object')
|
|
14
|
+
return; for (const key of ['title', 'description']) {
|
|
15
|
+
if (!Object.prototype.hasOwnProperty.call(item, key))
|
|
16
|
+
continue;
|
|
17
|
+
const v = item[key];
|
|
18
|
+
if (typeof v === 'string' && v.trim())
|
|
19
|
+
fields[`items.${i}.${key}`] = v;
|
|
20
|
+
} });
|
|
21
|
+
return fields;
|
|
22
|
+
}
|
|
23
|
+
function validateComposition(raw, fields) {
|
|
24
|
+
const value = raw;
|
|
25
|
+
let count = 0, images = 0;
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
if (!value || value.version !== 1)
|
|
28
|
+
throw new Error('Unsupported composition');
|
|
29
|
+
function visit(n, depth) {
|
|
30
|
+
if (!n || typeof n !== 'object' || ++count > 40 || depth > 5 || !['row', 'column', 'text', 'image', 'divider', 'icon'].includes(n.type))
|
|
31
|
+
throw new Error('Invalid composition node');
|
|
32
|
+
if (n.weight !== undefined && (!Number.isFinite(n.weight) || n.weight < .5 || n.weight > 4))
|
|
33
|
+
throw new Error('Invalid composition weight');
|
|
34
|
+
if (n.tone !== undefined && !['plain', 'card', 'contrast'].includes(n.tone))
|
|
35
|
+
throw new Error('Invalid composition tone');
|
|
36
|
+
const out = { type: n.type, weight: n.weight, tone: n.tone };
|
|
37
|
+
if (n.type === 'row' || n.type === 'column') {
|
|
38
|
+
if (!Array.isArray(n.children) || n.children.length < 2 || n.children.length > 4)
|
|
39
|
+
throw new Error('Invalid composition group');
|
|
40
|
+
out.children = n.children.map(c => visit(c, depth + 1));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (n.children !== undefined)
|
|
44
|
+
throw new Error('Leaf cannot have children');
|
|
45
|
+
if (n.type === 'text') {
|
|
46
|
+
if (!n.bind || !Object.prototype.hasOwnProperty.call(fields, n.bind) || seen.has(n.bind))
|
|
47
|
+
throw new Error('Invalid or duplicated binding');
|
|
48
|
+
if (n.role !== undefined && !['hero', 'heading', 'body', 'caption'].includes(n.role))
|
|
49
|
+
throw new Error('Invalid text role');
|
|
50
|
+
seen.add(n.bind);
|
|
51
|
+
out.bind = n.bind;
|
|
52
|
+
out.role = n.role || 'body';
|
|
53
|
+
}
|
|
54
|
+
if (n.type === 'image' && ++images > 1)
|
|
55
|
+
throw new Error('Only one illustration per composition');
|
|
56
|
+
if (n.type === 'icon') {
|
|
57
|
+
if (!n.icon || !['idea', 'check', 'chart', 'people', 'shield', 'arrow'].includes(n.icon))
|
|
58
|
+
throw new Error('Invalid icon');
|
|
59
|
+
out.icon = n.icon;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
const root = visit(value.root, 0);
|
|
65
|
+
if (Object.keys(fields).some(key => !seen.has(key)))
|
|
66
|
+
throw new Error('Composition omits content');
|
|
67
|
+
return { version: 1, root };
|
|
68
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AUTHORED_COMPOSITIONS = exports.COMPOSITION_SCENES = void 0;
|
|
4
|
+
exports.sceneRecipePreference = sceneRecipePreference;
|
|
5
|
+
const both = ['presentation', 'social'];
|
|
6
|
+
exports.COMPOSITION_SCENES = [
|
|
7
|
+
{ id: 'brand-collection', kinds: ['features'], outputs: both },
|
|
8
|
+
{ id: 'hero', kinds: ['paragraph'], outputs: both }, { id: 'chapter', kinds: ['paragraph'], outputs: ['presentation'] },
|
|
9
|
+
{ id: 'statement', kinds: ['paragraph'], outputs: both }, { id: 'closing', kinds: ['paragraph'], outputs: both },
|
|
10
|
+
{ id: 'product-launch', kinds: ['features'], outputs: both }, { id: 'feature-spotlight', kinds: ['features'], outputs: both },
|
|
11
|
+
{ id: 'feature-overview', kinds: ['features'], outputs: both }, { id: 'benefits', kinds: ['features'], outputs: both },
|
|
12
|
+
{ id: 'comparison', kinds: ['comparison'], outputs: both }, { id: 'before-after', kinds: ['comparison'], outputs: both },
|
|
13
|
+
{ id: 'plan-options', kinds: ['comparison'], outputs: both }, { id: 'process', kinds: ['steps'], outputs: both },
|
|
14
|
+
{ id: 'roadmap', kinds: ['steps'], outputs: both }, { id: 'tutorial', kinds: ['steps'], outputs: both },
|
|
15
|
+
{ id: 'checklist', kinds: ['features'], outputs: both }, { id: 'metrics', kinds: ['chart'], outputs: both },
|
|
16
|
+
{ id: 'trend', kinds: ['chart'], outputs: both }, { id: 'results', kinds: ['chart'], outputs: both },
|
|
17
|
+
{ id: 'quotation', kinds: ['quote'], outputs: both }, { id: 'testimonial', kinds: ['quote'], outputs: both },
|
|
18
|
+
{ id: 'code-example', kinds: ['code'], outputs: both }, { id: 'specifications', kinds: ['parameters'], outputs: both },
|
|
19
|
+
{ id: 'pricing-details', kinds: ['parameters'], outputs: both }, { id: 'event-details', kinds: ['parameters'], outputs: both },
|
|
20
|
+
{ id: 'recruitment-details', kinds: ['parameters'], outputs: both },
|
|
21
|
+
{ id: 'product-demonstration', kinds: ['image'], outputs: both, requiresAsset: true },
|
|
22
|
+
{ id: 'photograph', kinds: ['image'], outputs: both, requiresAsset: true },
|
|
23
|
+
...['banner-hero', 'product-showcase', 'editorial-cover', 'image-campaign'].map(id => ({ id, kinds: ['image'], outputs: both, requiresAsset: true })),
|
|
24
|
+
];
|
|
25
|
+
exports.AUTHORED_COMPOSITIONS = [
|
|
26
|
+
{ id: 'feature-focus', kind: 'features', min: 1, max: 1 },
|
|
27
|
+
{ id: 'feature-bento', kind: 'features', min: 2, max: 6 },
|
|
28
|
+
{ id: 'feature-index', kind: 'features', min: 2, max: 6 },
|
|
29
|
+
{ id: 'steps-journey', kind: 'steps', min: 2, max: 6 },
|
|
30
|
+
{ id: 'steps-ribbon', kind: 'steps', min: 2, max: 4 },
|
|
31
|
+
{ id: 'comparison-panels', kind: 'comparison', min: 2, max: 3 },
|
|
32
|
+
{ id: 'quote-editorial', kind: 'quote', min: 0, max: 0 },
|
|
33
|
+
{ id: 'quote-poster', kind: 'quote', min: 0, max: 0 },
|
|
34
|
+
{ id: 'code-editorial', kind: 'code', min: 0, max: 0 },
|
|
35
|
+
{ id: 'parameter-ledger', kind: 'parameters', min: 1, max: 8 },
|
|
36
|
+
{ id: 'parameter-cards', kind: 'parameters', min: 1, max: 4 },
|
|
37
|
+
];
|
|
38
|
+
/** Soft art-direction preferences; capacity and measured quality always remain mandatory. */
|
|
39
|
+
function sceneRecipePreference(scene, recipeId) {
|
|
40
|
+
const preferred = {
|
|
41
|
+
'banner-hero': ['authored-image-banner'], 'product-showcase': ['authored-image-split'], 'editorial-cover': ['authored-image-editorial'], 'image-campaign': ['authored-image-campaign'],
|
|
42
|
+
hero: ['cover', 'semantic-hero-split'], chapter: ['section', 'semantic-hero-editorial'], statement: ['statement', 'semantic-hero-stack'], closing: ['closing'],
|
|
43
|
+
'product-launch': ['authored-feature-bento'], 'feature-spotlight': ['authored-feature-focus'], 'feature-overview': ['authored-feature-bento', 'three-column'], benefits: ['authored-feature-index'],
|
|
44
|
+
comparison: ['authored-comparison-panels'], 'before-after': ['before-after', 'semantic-comparison-split'], 'plan-options': ['authored-comparison-panels'],
|
|
45
|
+
process: ['authored-steps-ribbon'], roadmap: ['authored-steps-ribbon', 'roadmap'], tutorial: ['authored-steps-journey'], checklist: ['authored-feature-index', 'checklist'],
|
|
46
|
+
quotation: ['authored-quote-editorial'], testimonial: ['authored-quote-poster'],
|
|
47
|
+
'code-example': ['authored-code-editorial'], specifications: ['authored-parameter-ledger'],
|
|
48
|
+
'pricing-details': ['authored-parameter-cards'], 'event-details': ['authored-parameter-cards'], 'recruitment-details': ['authored-parameter-ledger'],
|
|
49
|
+
};
|
|
50
|
+
return scene && preferred[scene]?.some(id => recipeId === id || recipeId.startsWith(`${id}--`)) ? 6 : 0;
|
|
51
|
+
}
|