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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brand-intent",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "An open file format specification for making brand intent machine-readable.",
5
5
  "author": "Christophe Stoll <christophe@stoll.studio>",
6
6
  "license": "MIT",
@@ -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
  }
package/parser/index.ts CHANGED
@@ -21,6 +21,8 @@ export type {
21
21
  ParsedStickyRegion,
22
22
  ParsedFlowItem,
23
23
  ParsedImageRegion,
24
+ ParsedIllustration,
25
+ IllustrationAnchor,
24
26
  GridYKeyword,
25
27
  } from './compositionParser';
26
28