brand-intent 0.14.0 → 0.15.0
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/package.json
CHANGED
package/parser/brandParser.ts
CHANGED
|
@@ -112,6 +112,27 @@ export interface ParsedBrand {
|
|
|
112
112
|
icons?: ParsedIconDef[];
|
|
113
113
|
/** List counter component (`counter` block). */
|
|
114
114
|
counter?: ParsedCounterDef;
|
|
115
|
+
/** Photo framing modules (`framing NAME` blocks), in declaration order. Absent → no camera guides. */
|
|
116
|
+
framings?: ParsedFramingDef[];
|
|
117
|
+
/** Camera defaults (`camera` block). Absent → rear camera, no grid. */
|
|
118
|
+
camera?: ParsedCameraDef;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ParsedCameraDef {
|
|
122
|
+
/** Which camera opens first. */
|
|
123
|
+
facing: 'front' | 'rear';
|
|
124
|
+
/** Viewfinder grid as rows × columns of cells. Absent → no grid. */
|
|
125
|
+
grid?: { rows: number; cols: number };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ParsedFramingDef {
|
|
129
|
+
id: string;
|
|
130
|
+
/** Display name. Defaults to the id. */
|
|
131
|
+
label: string;
|
|
132
|
+
/** Horizon position as % of image height, measured from the top. null → no horizon (close-ups). */
|
|
133
|
+
horizon: number | null;
|
|
134
|
+
/** The photographic task, one line. */
|
|
135
|
+
description?: string;
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
export type CounterFormat = '1' | '01' | '1.' | '(1)' | 'i';
|
|
@@ -351,7 +372,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
351
372
|
tokensSource: 'figma',
|
|
352
373
|
};
|
|
353
374
|
|
|
354
|
-
type ActiveBlock = 'none' | 'brand-colors' | 'state-colors' | 'color-bounds' | 'color-adherence' | 'color-rules' | 'color-families' | 'theme' | 'spacing' | 'divider' | 'typography' | 'font' | 'logo' | 'photo' | 'video' | 'illustration' | 'label' | 'cta' | 'icons' | 'counter';
|
|
375
|
+
type ActiveBlock = 'none' | 'brand-colors' | 'state-colors' | 'color-bounds' | 'color-adherence' | 'color-rules' | 'color-families' | 'theme' | 'spacing' | 'divider' | 'typography' | 'font' | 'logo' | 'photo' | 'video' | 'illustration' | 'label' | 'cta' | 'icons' | 'counter' | 'framing' | 'camera';
|
|
355
376
|
let activeBlock: ActiveBlock = 'none';
|
|
356
377
|
let currentThemeName = '';
|
|
357
378
|
let currentDividerName = '';
|
|
@@ -398,6 +419,9 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
398
419
|
let iconsDeclared = false;
|
|
399
420
|
const counterRaw: Record<string, { value: string; line: number }> = {};
|
|
400
421
|
let counterDeclared = false;
|
|
422
|
+
const cameraRaw: Record<string, { value: string; line: number }> = {};
|
|
423
|
+
let cameraDeclared = false;
|
|
424
|
+
const framingsRaw: Array<{ id: string; line: number; props: Record<string, { value: string; line: number }> }> = [];
|
|
401
425
|
|
|
402
426
|
for (let i = 0; i < lines.length; i++) {
|
|
403
427
|
const raw = lines[i];
|
|
@@ -767,6 +791,24 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
767
791
|
counterRaw[entry.slice(0, colonIdx).trim()] = { value: entry.slice(colonIdx + 1).trim(), line: lineNum };
|
|
768
792
|
continue;
|
|
769
793
|
}
|
|
794
|
+
if (activeBlock === 'camera') {
|
|
795
|
+
const trimmed = raw.trim();
|
|
796
|
+
if (trimmed.startsWith('#')) continue;
|
|
797
|
+
const commentIdx = trimmed.search(/\s+#/);
|
|
798
|
+
const entry = commentIdx !== -1 ? trimmed.slice(0, commentIdx) : trimmed;
|
|
799
|
+
const colonIdx = entry.indexOf(':');
|
|
800
|
+
if (colonIdx === -1) throw new Error(`${fileName}:${lineNum}: camera property must be "KEY: VALUE", got "${trimmed}"`);
|
|
801
|
+
cameraRaw[entry.slice(0, colonIdx).trim()] = { value: entry.slice(colonIdx + 1).trim(), line: lineNum };
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (activeBlock === 'framing') {
|
|
805
|
+
const trimmed = raw.trim();
|
|
806
|
+
if (trimmed.startsWith('#')) continue;
|
|
807
|
+
const colonIdx = trimmed.indexOf(':');
|
|
808
|
+
if (colonIdx === -1) throw new Error(`${fileName}:${lineNum}: framing property must be "KEY: VALUE", got "${trimmed}"`);
|
|
809
|
+
framingsRaw[framingsRaw.length - 1].props[trimmed.slice(0, colonIdx).trim()] = { value: trimmed.slice(colonIdx + 1).trim(), line: lineNum };
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
770
812
|
// Indented line with no active block — ignore
|
|
771
813
|
continue;
|
|
772
814
|
}
|
|
@@ -831,6 +873,24 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
831
873
|
continue;
|
|
832
874
|
}
|
|
833
875
|
|
|
876
|
+
// camera — camera defaults (facing, viewfinder grid)
|
|
877
|
+
if (raw.trim() === 'camera') {
|
|
878
|
+
if (cameraDeclared) throw new Error(`${fileName}:${lineNum}: duplicate camera block`);
|
|
879
|
+
cameraDeclared = true;
|
|
880
|
+
activeBlock = 'camera';
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// framing <id> — photo framing module (horizon position)
|
|
885
|
+
const framingMatch = raw.match(/^framing\s+(\S+)\s*$/);
|
|
886
|
+
if (framingMatch) {
|
|
887
|
+
const id = framingMatch[1];
|
|
888
|
+
if (framingsRaw.some(f => f.id === id)) throw new Error(`${fileName}:${lineNum}: duplicate framing "${id}"`);
|
|
889
|
+
framingsRaw.push({ id, line: lineNum, props: {} });
|
|
890
|
+
activeBlock = 'framing';
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
|
|
834
894
|
// theme ThemeName — starts a theme block
|
|
835
895
|
const themeMatch = raw.match(/^theme\s+(\S+)/);
|
|
836
896
|
if (themeMatch) {
|
|
@@ -1473,6 +1533,52 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1473
1533
|
result.counter = counter;
|
|
1474
1534
|
}
|
|
1475
1535
|
|
|
1536
|
+
// Finalise camera defaults
|
|
1537
|
+
if (cameraDeclared) {
|
|
1538
|
+
const known = new Set(['facing', 'grid']);
|
|
1539
|
+
for (const [key, { line }] of Object.entries(cameraRaw)) {
|
|
1540
|
+
if (!known.has(key)) throw new Error(`${fileName}:${line}: unknown camera property "${key}"`);
|
|
1541
|
+
}
|
|
1542
|
+
const facing = cameraRaw['facing']?.value ?? 'rear';
|
|
1543
|
+
if (facing !== 'front' && facing !== 'rear') {
|
|
1544
|
+
throw new Error(`${fileName}:${cameraRaw['facing'].line}: camera facing must be front | rear, got "${facing}"`);
|
|
1545
|
+
}
|
|
1546
|
+
const camera: ParsedCameraDef = { facing };
|
|
1547
|
+
const g = cameraRaw['grid'];
|
|
1548
|
+
if (g) {
|
|
1549
|
+
// "3" → 3 × 3, "3 4" → 3 rows × 4 columns
|
|
1550
|
+
const nums = g.value.split(/\s+/).map(Number);
|
|
1551
|
+
const [rows, cols = rows] = nums;
|
|
1552
|
+
if (nums.length > 2 || ![rows, cols].every(n => Number.isInteger(n) && n >= 2 && n <= 12)) {
|
|
1553
|
+
throw new Error(`${fileName}:${g.line}: camera grid must be "N" or "ROWS COLS" (2–12), got "${g.value}"`);
|
|
1554
|
+
}
|
|
1555
|
+
camera.grid = { rows, cols };
|
|
1556
|
+
}
|
|
1557
|
+
result.camera = camera;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Finalise framing modules
|
|
1561
|
+
if (framingsRaw.length > 0) {
|
|
1562
|
+
const known = new Set(['label', 'horizon', 'description']);
|
|
1563
|
+
result.framings = framingsRaw.map(({ id, line, props }) => {
|
|
1564
|
+
for (const [key, { line: l }] of Object.entries(props)) {
|
|
1565
|
+
if (!known.has(key)) throw new Error(`${fileName}:${l}: unknown framing property "${key}"`);
|
|
1566
|
+
}
|
|
1567
|
+
const h = props['horizon'];
|
|
1568
|
+
if (!h) throw new Error(`${fileName}:${line}: framing "${id}" needs "horizon: N%" or "horizon: none"`);
|
|
1569
|
+
let horizon: number | null = null;
|
|
1570
|
+
if (h.value !== 'none') {
|
|
1571
|
+
const m = h.value.match(/^(\d+(?:\.\d+)?)%$/);
|
|
1572
|
+
const n = m ? parseFloat(m[1]) : NaN;
|
|
1573
|
+
if (!(n > 0 && n < 100)) throw new Error(`${fileName}:${h.line}: framing horizon must be a percentage between 0% and 100% or "none", got "${h.value}"`);
|
|
1574
|
+
horizon = n;
|
|
1575
|
+
}
|
|
1576
|
+
const def: ParsedFramingDef = { id, label: props['label']?.value || id, horizon };
|
|
1577
|
+
if (props['description']?.value) def.description = props['description'].value;
|
|
1578
|
+
return def;
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1476
1582
|
// Validate required fields
|
|
1477
1583
|
const required: (keyof ParsedBrand)[] = ['id', 'name', 'language', 'locale'];
|
|
1478
1584
|
for (const field of required) {
|
package/parser/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export {
|
|
|
23
23
|
DEFAULT_COLOR_RULES,
|
|
24
24
|
} from './colorSystem';
|
|
25
25
|
export type { ReachConfig } from './colorSystem';
|
|
26
|
-
export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef, BrandColorPrintSpec, ParsedAssetDef, ParsedBadgeStyle, LogoAlign, ParsedCounterDef, CounterFormat, CounterShape } from './brandParser';
|
|
26
|
+
export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef, BrandColorPrintSpec, ParsedAssetDef, ParsedBadgeStyle, LogoAlign, ParsedCounterDef, CounterFormat, CounterShape, ParsedFramingDef, ParsedCameraDef } from './brandParser';
|
|
27
27
|
|
|
28
28
|
export { parseIconRef, parseIconSource, resolveIconRef, parseListItem, phosphorName, PHOSPHOR_WEIGHTS } from './icons';
|
|
29
29
|
export type { IconRef, IconSource, ParsedIconDef, ResolvedIcon, PhosphorWeight } from './icons';
|
package/parser/purposeParser.ts
CHANGED
|
@@ -38,6 +38,10 @@ export interface ParsedPurpose {
|
|
|
38
38
|
defaultVideo?: string;
|
|
39
39
|
/** When false, hides Take Photo / Upload buttons for this purpose. Default: true. */
|
|
40
40
|
camera?: boolean;
|
|
41
|
+
/** `camera: front | rear` — overrides the brand's camera facing for this purpose. */
|
|
42
|
+
cameraFacing?: 'front' | 'rear';
|
|
43
|
+
/** `framing: <id>` — the brand framing module photos for this purpose are shot in. */
|
|
44
|
+
framing?: string;
|
|
41
45
|
/** When false, photo/video analysis skips this purpose (no AI text generation). Default: true. */
|
|
42
46
|
analyze?: boolean;
|
|
43
47
|
/** Default icon for the icon slot. Resolves in the brand set first, then Phosphor (see resolveIconRef). */
|
|
@@ -132,6 +136,8 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
132
136
|
let defaultPhoto: string | undefined;
|
|
133
137
|
let defaultVideo: string | undefined;
|
|
134
138
|
let camera: boolean | undefined;
|
|
139
|
+
let cameraFacing: 'front' | 'rear' | undefined;
|
|
140
|
+
let framing: string | undefined;
|
|
135
141
|
let analyze: boolean | undefined;
|
|
136
142
|
let defaultIcon: IconRef | undefined;
|
|
137
143
|
let bgPrompt: string | undefined;
|
|
@@ -315,7 +321,15 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
315
321
|
|
|
316
322
|
// Camera control
|
|
317
323
|
if (line.startsWith('camera:')) {
|
|
318
|
-
|
|
324
|
+
const value = line.slice(7).trim();
|
|
325
|
+
camera = value !== 'false';
|
|
326
|
+
if (value === 'front' || value === 'rear') cameraFacing = value;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Framing module: one of the brand's `framing NAME` blocks
|
|
331
|
+
if (line.startsWith('framing:')) {
|
|
332
|
+
framing = line.slice(8).trim() || undefined;
|
|
319
333
|
continue;
|
|
320
334
|
}
|
|
321
335
|
|
|
@@ -652,5 +666,5 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
652
666
|
context = contextLines.join('\n').trim();
|
|
653
667
|
}
|
|
654
668
|
|
|
655
|
-
return { id, name, description, preferredCompositions: compositions, slots, density, palette, scope, context, voice, defaultLogo, defaultPhoto, defaultVideo, camera, analyze, defaultIcon, bgPrompt };
|
|
669
|
+
return { id, name, description, preferredCompositions: compositions, slots, density, palette, scope, context, voice, defaultLogo, defaultPhoto, defaultVideo, camera, cameraFacing, framing, analyze, defaultIcon, bgPrompt };
|
|
656
670
|
}
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
{ "include": "#icons-counter-header" },
|
|
32
32
|
{ "include": "#icon-entry" },
|
|
33
33
|
{ "include": "#counter-value" },
|
|
34
|
+
{ "include": "#framing-horizon" },
|
|
34
35
|
{ "include": "#theme-header" },
|
|
35
36
|
{ "include": "#brand-color-entry" },
|
|
36
37
|
{ "include": "#brand-color-print-sub" },
|
|
@@ -366,7 +367,7 @@
|
|
|
366
367
|
}
|
|
367
368
|
},
|
|
368
369
|
"asset-header": {
|
|
369
|
-
"match": "^(logo|photo|video|illustration)\\s+(\\S+)",
|
|
370
|
+
"match": "^(logo|photo|video|illustration|framing)\\s+(\\S+)",
|
|
370
371
|
"captures": {
|
|
371
372
|
"1": { "name": "keyword.control.brand-intent-brand" },
|
|
372
373
|
"2": { "name": "entity.name.type.brand-intent-brand" }
|
|
@@ -410,6 +411,16 @@
|
|
|
410
411
|
"11": { "name": "comment.line.number-sign.brand-intent-brand" }
|
|
411
412
|
}
|
|
412
413
|
},
|
|
414
|
+
"framing-horizon": {
|
|
415
|
+
"comment": "framing block horizon: N% | none",
|
|
416
|
+
"match": "^(\\s{2,})(horizon)(:\\s*)(\\d+(?:\\.\\d+)?%|none)\\s*(#.*)?$",
|
|
417
|
+
"captures": {
|
|
418
|
+
"2": { "name": "support.type.property-name.brand-intent-brand" },
|
|
419
|
+
"3": { "name": "punctuation.separator.key-value.brand-intent-brand" },
|
|
420
|
+
"4": { "name": "constant.numeric.brand-intent-brand" },
|
|
421
|
+
"5": { "name": "comment.line.number-sign.brand-intent-brand" }
|
|
422
|
+
}
|
|
423
|
+
},
|
|
413
424
|
"counter-value": {
|
|
414
425
|
"comment": "counter block properties (typography: is covered by typography-value)",
|
|
415
426
|
"match": "^(\\s{2,})(format|shape|size|padding)(:\\s*)(\\S+)\\s*(#.*)?$",
|
|
@@ -445,7 +456,7 @@
|
|
|
445
456
|
},
|
|
446
457
|
"keyword-partial": {
|
|
447
458
|
"comment": "Fallback: color the keyword token on incomplete lines (while typing)",
|
|
448
|
-
"match": "^(id:|name:|language:|locale:|domain:|app-icon:|favicon:|og-image:|tokens:|figma-file-key:|figma-collection:|tokens-file:|spacing:|label:|cta:|color-mode:|color-strategy:|color-adherence:|brand-colors|state-colors|color-bounds|color-rules|color-families|theme|divider|font|typography|logo|photo|video|illustration|icons|counter)(?=\\s|$)",
|
|
459
|
+
"match": "^(id:|name:|language:|locale:|domain:|app-icon:|favicon:|og-image:|tokens:|figma-file-key:|figma-collection:|tokens-file:|spacing:|label:|cta:|color-mode:|color-strategy:|color-adherence:|brand-colors|state-colors|color-bounds|color-rules|color-families|theme|divider|font|typography|logo|photo|video|illustration|icons|counter|framing)(?=\\s|$)",
|
|
449
460
|
"captures": {
|
|
450
461
|
"1": { "name": "keyword.control.brand-intent-brand" }
|
|
451
462
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
{ "include": "#palette-header" },
|
|
15
15
|
{ "include": "#asset-field" },
|
|
16
16
|
{ "include": "#camera-field" },
|
|
17
|
+
{ "include": "#framing-field" },
|
|
17
18
|
{ "include": "#analyze-field" },
|
|
18
19
|
{ "include": "#bg-prompt-field" },
|
|
19
20
|
{ "include": "#context-header" },
|
|
@@ -114,12 +115,19 @@
|
|
|
114
115
|
}
|
|
115
116
|
},
|
|
116
117
|
"camera-field": {
|
|
117
|
-
"match": "^(camera:)\\s+(true|false)",
|
|
118
|
+
"match": "^(camera:)\\s+(true|false|front|rear)",
|
|
118
119
|
"captures": {
|
|
119
120
|
"1": { "name": "keyword.control.brand-intent-purpose" },
|
|
120
121
|
"2": { "name": "constant.language.brand-intent-purpose" }
|
|
121
122
|
}
|
|
122
123
|
},
|
|
124
|
+
"framing-field": {
|
|
125
|
+
"match": "^(framing:)\\s+([\\w-]+)",
|
|
126
|
+
"captures": {
|
|
127
|
+
"1": { "name": "keyword.control.brand-intent-purpose" },
|
|
128
|
+
"2": { "name": "entity.name.type.brand-intent-purpose" }
|
|
129
|
+
}
|
|
130
|
+
},
|
|
123
131
|
"analyze-field": {
|
|
124
132
|
"match": "^(analyze:)\\s+(true|false)",
|
|
125
133
|
"captures": {
|
|
@@ -242,7 +250,7 @@
|
|
|
242
250
|
"comment": "Fallback: color the keyword token on incomplete lines (while typing)",
|
|
243
251
|
"patterns": [
|
|
244
252
|
{
|
|
245
|
-
"match": "^(id:|name:|compositions:|density:|defaultIcon:|scope|voice|palette:|logo:|photo:|video:|camera:|analyze:|bg-prompt:|context|slot)(?=\\s|$)",
|
|
253
|
+
"match": "^(id:|name:|compositions:|density:|defaultIcon:|scope|voice|palette:|logo:|photo:|video:|camera:|framing:|analyze:|bg-prompt:|context|slot)(?=\\s|$)",
|
|
246
254
|
"captures": {
|
|
247
255
|
"1": { "name": "keyword.control.brand-intent-purpose" }
|
|
248
256
|
}
|