@neatico/visual-core 0.14.3 → 0.14.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/candidateRanking.d.ts +31 -0
  2. package/dist/candidateRanking.js +55 -0
  3. package/dist/cjs/candidateRanking.js +61 -0
  4. package/dist/cjs/composition.js +62 -36
  5. package/dist/cjs/compositionNumbers.js +111 -0
  6. package/dist/cjs/compositionScenes.js +91 -38
  7. package/dist/cjs/compositionUseCases.js +145 -0
  8. package/dist/cjs/content.js +574 -120
  9. package/dist/cjs/contentBrief.js +82 -0
  10. package/dist/cjs/designBlueprint.js +94 -19
  11. package/dist/cjs/designSystem.js +536 -77
  12. package/dist/cjs/editorial.js +76 -20
  13. package/dist/cjs/fontCatalog.js +81 -23
  14. package/dist/cjs/formalCompositions.js +120 -0
  15. package/dist/cjs/index.js +13 -0
  16. package/dist/cjs/layoutContracts.js +57 -11
  17. package/dist/cjs/layouts.js +5 -92
  18. package/dist/cjs/nativeSocialParameters.js +145 -0
  19. package/dist/cjs/pagination.js +27 -7
  20. package/dist/cjs/parameterDefaults.js +20 -0
  21. package/dist/cjs/recipeParameters.js +304 -0
  22. package/dist/cjs/socialLayouts.js +127 -387
  23. package/dist/cjs/styleManifest.js +107 -0
  24. package/dist/cjs/text.js +12 -2
  25. package/dist/cjs/textPresentation.js +68 -0
  26. package/dist/composition.d.ts +4 -4
  27. package/dist/composition.js +62 -36
  28. package/dist/compositionNumbers.d.ts +4 -0
  29. package/dist/compositionNumbers.js +108 -0
  30. package/dist/compositionScenes.d.ts +2 -2
  31. package/dist/compositionScenes.js +91 -38
  32. package/dist/compositionUseCases.d.ts +1 -0
  33. package/dist/compositionUseCases.js +142 -0
  34. package/dist/content.d.ts +65 -20
  35. package/dist/content.js +575 -121
  36. package/dist/contentBrief.d.ts +16 -0
  37. package/dist/contentBrief.js +78 -0
  38. package/dist/designBlueprint.d.ts +4 -4
  39. package/dist/designBlueprint.js +95 -20
  40. package/dist/designSystem.d.ts +11 -7
  41. package/dist/designSystem.js +537 -79
  42. package/dist/editorial.d.ts +1 -1
  43. package/dist/editorial.js +77 -21
  44. package/dist/fontCatalog.d.ts +1 -1
  45. package/dist/fontCatalog.js +81 -23
  46. package/dist/formalCompositions.d.ts +26 -0
  47. package/dist/formalCompositions.js +116 -0
  48. package/dist/index.d.ts +10 -0
  49. package/dist/index.js +10 -0
  50. package/dist/layoutContracts.d.ts +3 -3
  51. package/dist/layoutContracts.js +58 -12
  52. package/dist/layouts.d.ts +2 -2
  53. package/dist/layouts.js +5 -92
  54. package/dist/nativeSocialParameters.d.ts +38 -0
  55. package/dist/nativeSocialParameters.js +139 -0
  56. package/dist/pagination.js +27 -7
  57. package/dist/parameterDefaults.d.ts +2 -0
  58. package/dist/parameterDefaults.js +17 -0
  59. package/dist/recipeParameters.d.ts +34 -0
  60. package/dist/recipeParameters.js +300 -0
  61. package/dist/socialLayouts.d.ts +40 -232
  62. package/dist/socialLayouts.js +127 -387
  63. package/dist/styleManifest.d.ts +20 -0
  64. package/dist/styleManifest.js +103 -0
  65. package/dist/text.js +12 -2
  66. package/dist/textPresentation.d.ts +29 -0
  67. package/dist/textPresentation.js +64 -0
  68. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ import { DESIGN_THEMES, designTheme, } from "./designSystem.js";
2
+ import { languageFont } from "./fontCatalog.js";
3
+ export function createStyleManifest(themeId, copy = "", imagery) {
4
+ const theme = designTheme(themeId);
5
+ if (!DESIGN_THEMES.some((candidate) => candidate.id === themeId) &&
6
+ !["paper", "midnight", "ocean", "plum"].includes(themeId))
7
+ throw new Error("STYLE_UNKNOWN_THEME");
8
+ const manifest = {
9
+ version: 1,
10
+ themeId: theme.id,
11
+ themeVersion: 1,
12
+ theme: {
13
+ ...theme,
14
+ palette: { ...theme.palette },
15
+ typography: { ...theme.typography },
16
+ geometry: { ...theme.geometry },
17
+ },
18
+ fonts: {
19
+ heading: languageFont(copy, theme.typography.heading),
20
+ body: languageFont(copy, theme.typography.body),
21
+ code: theme.typography.code === "monospace"
22
+ ? "JetBrains Mono"
23
+ : theme.typography.code,
24
+ },
25
+ imagery: {
26
+ medium: "native",
27
+ referenceAssetIds: [],
28
+ direction: "",
29
+ ...imagery,
30
+ },
31
+ };
32
+ return validateStyleManifest(manifest);
33
+ }
34
+ export function validateStyleManifest(value) {
35
+ const manifest = value;
36
+ const fail = () => {
37
+ throw new Error("STYLE_INVALID_MANIFEST");
38
+ };
39
+ if (!manifest ||
40
+ manifest.version !== 1 ||
41
+ manifest.themeVersion !== 1 ||
42
+ typeof manifest.themeId !== "string" ||
43
+ !manifest.theme ||
44
+ manifest.theme.id !== manifest.themeId ||
45
+ !DESIGN_THEMES.some((theme) => theme.id === manifest.themeId))
46
+ return fail();
47
+ const { palette, typography, geometry } = manifest.theme;
48
+ if (!palette ||
49
+ ["background", "surface", "ink", "muted", "accent", "line"].some((key) => !/^#[\da-f]{6}$/i.test(palette[key] || "")))
50
+ return fail();
51
+ if (!typography ||
52
+ ["heading", "body", "code"].some((key) => typeof typography[key] !==
53
+ "string") ||
54
+ !Number.isInteger(typography.headingWeight) ||
55
+ typography.headingWeight < 100 ||
56
+ typography.headingWeight > 900)
57
+ return fail();
58
+ if (!geometry ||
59
+ !Number.isFinite(geometry.radius) ||
60
+ geometry.radius < 0 ||
61
+ geometry.radius > 100 ||
62
+ !Number.isFinite(geometry.spacing) ||
63
+ geometry.spacing <= 0 ||
64
+ geometry.spacing > 4 ||
65
+ !["none", "hairline", "accent"].includes(geometry.rule))
66
+ return fail();
67
+ if (!manifest.fonts ||
68
+ Object.keys(manifest.fonts).length !== 3 ||
69
+ ["heading", "body", "code"].some((key) => {
70
+ const font = manifest.fonts[key];
71
+ return typeof font !== "string" || !font.trim() || font.length > 100;
72
+ }))
73
+ return fail();
74
+ if (!manifest.imagery ||
75
+ !["native", "photography", "illustration", "3d"].includes(manifest.imagery.medium) ||
76
+ typeof manifest.imagery.direction !== "string" ||
77
+ manifest.imagery.direction.length > 2000 ||
78
+ !Array.isArray(manifest.imagery.referenceAssetIds) ||
79
+ manifest.imagery.referenceAssetIds.length > 16 ||
80
+ manifest.imagery.referenceAssetIds.some((id) => typeof id !== "string" || !id || id.length > 160))
81
+ return fail();
82
+ return {
83
+ version: 1,
84
+ themeId: manifest.themeId,
85
+ themeVersion: 1,
86
+ theme: {
87
+ id: manifest.themeId,
88
+ palette: { ...palette },
89
+ typography: { ...typography },
90
+ geometry: { ...geometry },
91
+ },
92
+ fonts: {
93
+ heading: manifest.fonts.heading,
94
+ body: manifest.fonts.body,
95
+ code: manifest.fonts.code,
96
+ },
97
+ imagery: {
98
+ medium: manifest.imagery.medium,
99
+ direction: manifest.imagery.direction,
100
+ referenceAssetIds: [...manifest.imagery.referenceAssetIds],
101
+ },
102
+ };
103
+ }
package/dist/text.js CHANGED
@@ -22,7 +22,7 @@ export function wrapCopy(text, width, size, bold, measure, options = {}) {
22
22
  }
23
23
  const ranges = options.keepWords && typeof Intl.Segmenter === 'function' ? [...new Intl.Segmenter(undefined, { granularity: 'word' }).segment(paragraph)].filter(part => part.isWordLike).map(part => ({ start: part.index, end: part.index + part.segment.length, text: part.segment })) : [];
24
24
  // Keep a fitting URL/API token whole even where UAX permits a slash break.
25
- for (const match of paragraph.matchAll(/[A-Za-z0-9]+(?:[._/@+\-][A-Za-z0-9]+)+/g))
25
+ for (const match of paragraph.matchAll(/[A-Za-z0-9]+(?:[.:_/@+\-][A-Za-z0-9]+)+/g))
26
26
  ranges.push({ start: match.index, end: match.index + match[0].length, text: match[0] });
27
27
  const wrapAt = (available) => {
28
28
  const breaker = new LineBreaker(paragraph), tokens = [];
@@ -50,7 +50,17 @@ export function wrapCopy(text, width, size, bold, measure, options = {}) {
50
50
  line = token.trimStart();
51
51
  continue;
52
52
  }
53
- const glyphs = typeof Intl.Segmenter === 'function' ? [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(token)].map(part => part.segment) : Array.from(token);
53
+ const graphemes = (value) => typeof Intl.Segmenter === 'function' ? [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(value)].map(part => part.segment) : Array.from(value);
54
+ // A range may be wider than a column, but a fitting clock must stay intact.
55
+ // Keep each clock as an atomic fallback unit instead of splitting its minutes.
56
+ const glyphs = [];
57
+ let cursor = 0;
58
+ for (const match of token.matchAll(/\d{1,2}:\d{2}(?::\d{2})?/g)) {
59
+ glyphs.push(...graphemes(token.slice(cursor, match.index)));
60
+ glyphs.push(...(measured(match[0]) <= available ? [match[0]] : graphemes(match[0])));
61
+ cursor = match.index + match[0].length;
62
+ }
63
+ glyphs.push(...graphemes(token.slice(cursor)));
54
64
  for (const glyph of glyphs) {
55
65
  if (glyph.trim() && measured(glyph) > available)
56
66
  throw new RangeError('A character exceeds the available width');
@@ -0,0 +1,29 @@
1
+ /** Closed presentation intent. Content stays in its original field, never in CSS. */
2
+ export interface TextPresentationLine {
3
+ text: string;
4
+ font: 'display' | 'body' | 'mono';
5
+ scale: number;
6
+ tone: 'ink' | 'accent' | 'secondary';
7
+ decoration: 'none' | 'marker' | 'underline';
8
+ italic: boolean;
9
+ }
10
+ export interface TextPresentation {
11
+ path: string;
12
+ fit: 'natural' | 'equal-width';
13
+ lines: TextPresentationLine[];
14
+ }
15
+ export declare function textPresentationSchema(): {
16
+ type: string;
17
+ minItems: number;
18
+ maxItems: number;
19
+ default: never[];
20
+ description: string;
21
+ items: {
22
+ type: string;
23
+ additionalProperties: boolean;
24
+ properties: Record<string, unknown>;
25
+ required: string[];
26
+ };
27
+ };
28
+ /** Validate exact source reconstruction; script-aware boundaries permit CJK breaks. */
29
+ export declare function validateTextPresentations(value: unknown, content: Record<string, unknown>): TextPresentation[];
@@ -0,0 +1,64 @@
1
+ const object = (properties) => ({
2
+ type: 'object', additionalProperties: false, properties, required: Object.keys(properties),
3
+ });
4
+ export function textPresentationSchema() {
5
+ return {
6
+ type: 'array', minItems: 0, maxItems: 8, default: [],
7
+ description: 'Optional typography for visible text fields, including title, body, item descriptions and quotations. Use a few deliberate contrasts within the shared suite. Every line must preserve the original field text and word boundaries. Empty uses the authored typography. Never style code, evidence, IDs or asset metadata.',
8
+ items: object({
9
+ path: { type: 'string', minLength: 1, maxLength: 120, description: 'Exact dot path of the visible text field, such as body, items.0.description or blocks.0.text.' },
10
+ fit: { type: 'string', enum: ['natural', 'equal-width'], description: 'Equal-width adjusts each line independently within readable size limits; it never stretches glyphs.' },
11
+ lines: { type: 'array', minItems: 1, maxItems: 8, items: object({
12
+ text: { type: 'string', minLength: 1, maxLength: 220, description: 'One complete phrase copied exactly from the target field. All lines together must preserve that entire field.' },
13
+ font: { type: 'string', enum: ['display', 'body', 'mono'] },
14
+ scale: { type: 'number', minimum: 0.8, maximum: 1.35 },
15
+ tone: { type: 'string', enum: ['ink', 'accent', 'secondary'] },
16
+ decoration: { type: 'string', enum: ['none', 'marker', 'underline'] },
17
+ italic: { type: 'boolean' },
18
+ }) },
19
+ }),
20
+ };
21
+ }
22
+ const normalize = (text) => text.normalize('NFC').replace(/\s+/gu, ' ').trim();
23
+ const word = (character) => /[\p{Script=Latin}\p{Script=Cyrillic}\p{Script=Greek}\p{Number}]/u.test(character);
24
+ /** Validate exact source reconstruction; script-aware boundaries permit CJK breaks. */
25
+ export function validateTextPresentations(value, content) {
26
+ if (value === undefined)
27
+ return [];
28
+ if (!Array.isArray(value) || value.length > 8)
29
+ throw new Error('TEXT_PRESENTATION_INVALID');
30
+ const paths = new Set();
31
+ for (const entry of value) {
32
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
33
+ throw new Error('TEXT_PRESENTATION_INVALID');
34
+ if (Object.keys(entry).some(key => !['path', 'fit', 'lines'].includes(key)))
35
+ throw new Error('TEXT_PRESENTATION_INVALID');
36
+ if (typeof entry.path !== 'string' || !/^(?:title|body|eyebrow|cta|value|attribution|(?:items|blocks)\.\d+\.(?:(?:entries|rows|columns)\.\d+\.)?(?:title|heading|text|description|name|value|attribution|points\.\d+))$/.test(entry.path) || paths.has(entry.path))
37
+ throw new Error('TEXT_PRESENTATION_PATH');
38
+ paths.add(entry.path);
39
+ const copy = entry.path.split('.').reduce((node, key) => node?.[key], content);
40
+ if (typeof copy !== 'string' || !copy.trim())
41
+ throw new Error('TEXT_PRESENTATION_PATH');
42
+ if (!['natural', 'equal-width'].includes(entry.fit) || !Array.isArray(entry.lines) || entry.lines.length < 1 || entry.lines.length > 8)
43
+ throw new Error('TEXT_PRESENTATION_INVALID');
44
+ const source = normalize(copy);
45
+ let cursor = 0;
46
+ for (const line of entry.lines) {
47
+ if (!line || typeof line !== 'object' || Array.isArray(line) || Object.keys(line).some(key => !['text', 'font', 'scale', 'tone', 'decoration', 'italic'].includes(key)))
48
+ throw new Error('TEXT_PRESENTATION_INVALID');
49
+ if (typeof line.text !== 'string' || !normalize(line.text) || Array.from(line.text).length > 220 || !['display', 'body', 'mono'].includes(line.font) || !Number.isFinite(line.scale) || line.scale < 0.8 || line.scale > 1.35 || !['ink', 'accent', 'secondary'].includes(line.tone) || !['none', 'marker', 'underline'].includes(line.decoration) || typeof line.italic !== 'boolean')
50
+ throw new Error('TEXT_PRESENTATION_INVALID');
51
+ const phrase = normalize(line.text);
52
+ if (source[cursor] === ' ')
53
+ cursor++;
54
+ if (!source.startsWith(phrase, cursor))
55
+ throw new Error('TEXT_PRESENTATION_CONTENT');
56
+ if (cursor && source[cursor - 1] !== ' ' && word(source[cursor - 1]) && word(phrase[0]))
57
+ throw new Error('TEXT_PRESENTATION_WORD_BOUNDARY');
58
+ cursor += phrase.length;
59
+ }
60
+ if (cursor !== source.length)
61
+ throw new Error('TEXT_PRESENTATION_CONTENT');
62
+ }
63
+ return value;
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neatico/visual-core",
3
- "version": "0.14.3",
3
+ "version": "0.14.64",
4
4
  "description": "Framework-independent geometry, measured text wrapping, and extensible scene registries for visual editors.",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",