brand-intent 0.3.2 → 0.4.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/package.json +1 -1
- package/parser/brandParser.ts +13 -11
- package/parser/compositionParser.ts +41 -1
- package/parser/index.ts +2 -0
package/package.json
CHANGED
package/parser/brandParser.ts
CHANGED
|
@@ -45,8 +45,8 @@ export interface ParsedBrand {
|
|
|
45
45
|
dividers?: Record<string, ParsedDividerDef>;
|
|
46
46
|
/** Named typography styles: name → style definition */
|
|
47
47
|
typographies?: Record<string, ParsedTypographyDef>;
|
|
48
|
-
/** Brand-declared assets: curated logos, photos, videos with labels and metadata */
|
|
49
|
-
assets?: { logos?: ParsedAssetDef[]; photos?: ParsedAssetDef[]; videos?: ParsedAssetDef[] };
|
|
48
|
+
/** Brand-declared assets: curated logos, photos, videos, illustrations with labels and metadata */
|
|
49
|
+
assets?: { logos?: ParsedAssetDef[]; photos?: ParsedAssetDef[]; videos?: ParsedAssetDef[]; illustrations?: ParsedAssetDef[] };
|
|
50
50
|
/** Label badge style — padding (top/right/bottom/left in cqh) and corner radius (cqh) */
|
|
51
51
|
label?: ParsedBadgeStyle;
|
|
52
52
|
/** CTA pill style — padding (top/right/bottom/left in cqh) and corner radius (cqh, or 999 for full pill) */
|
|
@@ -146,7 +146,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
146
146
|
tokensSource: 'figma',
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
-
type ActiveBlock = 'none' | 'brand-colors' | 'theme' | 'spacing' | 'divider' | 'typography' | 'font' | 'logo' | 'photo' | 'video' | 'label' | 'cta';
|
|
149
|
+
type ActiveBlock = 'none' | 'brand-colors' | 'theme' | 'spacing' | 'divider' | 'typography' | 'font' | 'logo' | 'photo' | 'video' | 'illustration' | 'label' | 'cta';
|
|
150
150
|
let activeBlock: ActiveBlock = 'none';
|
|
151
151
|
let currentThemeName = '';
|
|
152
152
|
let currentDividerName = '';
|
|
@@ -164,6 +164,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
164
164
|
const logosRaw: Array<{ id: string; props: Record<string, string> }> = [];
|
|
165
165
|
const photosRaw: Array<{ id: string; props: Record<string, string> }> = [];
|
|
166
166
|
const videosRaw: Array<{ id: string; props: Record<string, string> }> = [];
|
|
167
|
+
const illustrationsRaw: Array<{ id: string; props: Record<string, string> }> = [];
|
|
167
168
|
let currentAssetId = '';
|
|
168
169
|
const labelRaw: Record<string, string> = {};
|
|
169
170
|
const ctaRaw: Record<string, string> = {};
|
|
@@ -264,18 +265,18 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
264
265
|
}
|
|
265
266
|
continue;
|
|
266
267
|
}
|
|
267
|
-
if ((activeBlock === 'logo' || activeBlock === 'photo' || activeBlock === 'video') && currentAssetId) {
|
|
268
|
+
if ((activeBlock === 'logo' || activeBlock === 'photo' || activeBlock === 'video' || activeBlock === 'illustration') && currentAssetId) {
|
|
268
269
|
const colonIdx = raw.indexOf(':');
|
|
269
270
|
if (colonIdx !== -1) {
|
|
270
271
|
const key = raw.slice(0, colonIdx).trim();
|
|
271
272
|
const value = raw.slice(colonIdx + 1).trim();
|
|
272
|
-
const arr = activeBlock === 'logo' ? logosRaw : activeBlock === 'photo' ? photosRaw : videosRaw;
|
|
273
|
+
const arr = activeBlock === 'logo' ? logosRaw : activeBlock === 'photo' ? photosRaw : activeBlock === 'video' ? videosRaw : illustrationsRaw;
|
|
273
274
|
const last = arr[arr.length - 1];
|
|
274
275
|
if (last && last.id === currentAssetId) last.props[key] = value;
|
|
275
276
|
} else {
|
|
276
277
|
const trimmed = raw.trim();
|
|
277
278
|
if (trimmed === 'animated' || trimmed === 'loop') {
|
|
278
|
-
const arr = activeBlock === 'logo' ? logosRaw : activeBlock === 'photo' ? photosRaw : videosRaw;
|
|
279
|
+
const arr = activeBlock === 'logo' ? logosRaw : activeBlock === 'photo' ? photosRaw : activeBlock === 'video' ? videosRaw : illustrationsRaw;
|
|
279
280
|
const last = arr[arr.length - 1];
|
|
280
281
|
if (last && last.id === currentAssetId) last.props[trimmed] = 'true';
|
|
281
282
|
}
|
|
@@ -341,13 +342,13 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
341
342
|
continue;
|
|
342
343
|
}
|
|
343
344
|
|
|
344
|
-
// logo/photo/video <id> — starts an asset block
|
|
345
|
-
const assetMatch = raw.match(/^(logo|photo|video)\s+(\S+)/);
|
|
345
|
+
// logo/photo/video/illustration <id> — starts an asset block
|
|
346
|
+
const assetMatch = raw.match(/^(logo|photo|video|illustration)\s+(\S+)/);
|
|
346
347
|
if (assetMatch) {
|
|
347
348
|
const [, type, id] = assetMatch;
|
|
348
349
|
currentAssetId = id;
|
|
349
|
-
activeBlock = type as 'logo' | 'photo' | 'video';
|
|
350
|
-
const arr = type === 'logo' ? logosRaw : type === 'photo' ? photosRaw : videosRaw;
|
|
350
|
+
activeBlock = type as 'logo' | 'photo' | 'video' | 'illustration';
|
|
351
|
+
const arr = type === 'logo' ? logosRaw : type === 'photo' ? photosRaw : type === 'video' ? videosRaw : illustrationsRaw;
|
|
351
352
|
arr.push({ id, props: {} });
|
|
352
353
|
continue;
|
|
353
354
|
}
|
|
@@ -510,11 +511,12 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
510
511
|
if (raw.props['loop'] === 'true') def.loop = true;
|
|
511
512
|
return def;
|
|
512
513
|
};
|
|
513
|
-
if (logosRaw.length > 0 || photosRaw.length > 0 || videosRaw.length > 0) {
|
|
514
|
+
if (logosRaw.length > 0 || photosRaw.length > 0 || videosRaw.length > 0 || illustrationsRaw.length > 0) {
|
|
514
515
|
result.assets = {};
|
|
515
516
|
if (logosRaw.length > 0) result.assets.logos = logosRaw.map(toAssetDef);
|
|
516
517
|
if (photosRaw.length > 0) result.assets.photos = photosRaw.map(toAssetDef);
|
|
517
518
|
if (videosRaw.length > 0) result.assets.videos = videosRaw.map(toAssetDef);
|
|
519
|
+
if (illustrationsRaw.length > 0) result.assets.illustrations = illustrationsRaw.map(toAssetDef);
|
|
518
520
|
}
|
|
519
521
|
|
|
520
522
|
// Finalise label / cta badge styles
|
|
@@ -22,6 +22,26 @@ export interface ParsedImageRegion {
|
|
|
22
22
|
colEnd?: number; // 0-based column (undefined = full width)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Anchor keywords for illustration placement. */
|
|
26
|
+
export type IllustrationAnchor =
|
|
27
|
+
| 'top-left' | 'top-center' | 'top-right'
|
|
28
|
+
| 'center-left' | 'center' | 'center-right'
|
|
29
|
+
| 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
30
|
+
|
|
31
|
+
/** Parsed illustration placement from `illustration:` line. */
|
|
32
|
+
export interface ParsedIllustration {
|
|
33
|
+
anchor: IllustrationAnchor;
|
|
34
|
+
fit: 'width' | 'height';
|
|
35
|
+
/** Scale as fraction 0–1 (e.g. 0.9 = 90% of canvas dimension). */
|
|
36
|
+
scale: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ILLUSTRATION_ANCHORS = new Set<string>([
|
|
40
|
+
'top-left', 'top-center', 'top-right',
|
|
41
|
+
'center-left', 'center', 'center-right',
|
|
42
|
+
'bottom-left', 'bottom-center', 'bottom-right',
|
|
43
|
+
]);
|
|
44
|
+
|
|
25
45
|
export interface ParsedComposition {
|
|
26
46
|
/** Programmatic ID (from `id:` line, or fallback from first `#` line). */
|
|
27
47
|
id: string;
|
|
@@ -44,6 +64,8 @@ export interface ParsedComposition {
|
|
|
44
64
|
sticky: ParsedStickyRegion[];
|
|
45
65
|
/** Flow regions — content-driven flow areas. */
|
|
46
66
|
flow: ParsedFlowRegion | null;
|
|
67
|
+
/** Illustration placement (grid mode only for now). */
|
|
68
|
+
illustration?: ParsedIllustration;
|
|
47
69
|
}
|
|
48
70
|
|
|
49
71
|
/** A sticky (repetition) region — composed elements that don't participate in flow. */
|
|
@@ -179,6 +201,7 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
|
|
|
179
201
|
const slots: ParsedSlot[] = [];
|
|
180
202
|
const sticky: ParsedStickyRegion[] = [];
|
|
181
203
|
let flow: ParsedFlowRegion | null = null;
|
|
204
|
+
let illustration: ParsedIllustration | undefined = undefined;
|
|
182
205
|
|
|
183
206
|
/** Parse a fraction string like "2/3" → 0.667. */
|
|
184
207
|
function parseFraction(s: string, ctx: string): number {
|
|
@@ -260,6 +283,23 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
|
|
|
260
283
|
continue;
|
|
261
284
|
}
|
|
262
285
|
|
|
286
|
+
// Illustration placement: "illustration: bottom-center fit-width 90%"
|
|
287
|
+
if (line.startsWith('illustration:')) {
|
|
288
|
+
const parts = line.slice(13).trim().split(/\s+/);
|
|
289
|
+
if (parts.length < 3) throw new Error(`${fileName}:${lineNum}: invalid illustration line — expected "illustration: <anchor> <fit-width|fit-height> <N%>"`);
|
|
290
|
+
const anchor = parts[0];
|
|
291
|
+
if (!ILLUSTRATION_ANCHORS.has(anchor)) throw new Error(`${fileName}:${lineNum}: invalid illustration anchor "${anchor}" (expected ${[...ILLUSTRATION_ANCHORS].join(', ')})`);
|
|
292
|
+
const fitStr = parts[1];
|
|
293
|
+
if (fitStr !== 'fit-width' && fitStr !== 'fit-height') throw new Error(`${fileName}:${lineNum}: invalid illustration fit "${fitStr}" (expected fit-width or fit-height)`);
|
|
294
|
+
const scaleStr = parts[2];
|
|
295
|
+
const scaleMatch = scaleStr.match(/^(\d+(?:\.\d+)?)%$/);
|
|
296
|
+
if (!scaleMatch) throw new Error(`${fileName}:${lineNum}: invalid illustration scale "${scaleStr}" (expected e.g. "90%")`);
|
|
297
|
+
const scale = parseFloat(scaleMatch[1]) / 100;
|
|
298
|
+
if (scale <= 0 || scale > 2) throw new Error(`${fileName}:${lineNum}: illustration scale out of range (expected 1%–200%)`);
|
|
299
|
+
illustration = { anchor: anchor as IllustrationAnchor, fit: fitStr === 'fit-width' ? 'width' : 'height', scale };
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
263
303
|
// grid: is no longer used in layout files (format owns the grid). Silently skip.
|
|
264
304
|
if (line.startsWith('grid:')) {
|
|
265
305
|
console.warn(`${fileName}:${lineNum}: "grid:" in layout files is no longer needed — format defines the grid`);
|
|
@@ -512,5 +552,5 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
|
|
|
512
552
|
// Validate required fields
|
|
513
553
|
if (!image) image = 'full'; // default to full-bleed image
|
|
514
554
|
if (!logo && !logoNone && !sticky.length) throw new Error(`${fileName}: missing "logo:" line (or use sticky: block)`);
|
|
515
|
-
return { id, name, description, mode, image, imageExplicit, logo, logoNone: logoNone || undefined, icon, slots, sticky, flow };
|
|
555
|
+
return { id, name, description, mode, image, imageExplicit, logo, logoNone: logoNone || undefined, icon, slots, sticky, flow, illustration };
|
|
516
556
|
}
|