brand-intent 0.4.0 → 0.4.2

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.4.0",
3
+ "version": "0.4.2",
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
@@ -34,6 +34,8 @@ export interface ParsedIllustration {
34
34
  fit: 'width' | 'height';
35
35
  /** Scale as fraction 0–1 (e.g. 0.9 = 90% of canvas dimension). */
36
36
  scale: number;
37
+ /** Offset from anchor position in % of canvas [x, y]. Positive = right/down. */
38
+ offset?: [number, number];
37
39
  }
38
40
 
39
41
  const ILLUSTRATION_ANCHORS = new Set<string>([
@@ -296,7 +298,14 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
296
298
  if (!scaleMatch) throw new Error(`${fileName}:${lineNum}: invalid illustration scale "${scaleStr}" (expected e.g. "90%")`);
297
299
  const scale = parseFloat(scaleMatch[1]) / 100;
298
300
  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 };
301
+ const illu: ParsedIllustration = { anchor: anchor as IllustrationAnchor, fit: fitStr === 'fit-width' ? 'width' : 'height', scale };
302
+ // Optional: "offset X Y" in % of canvas (positive = right/down)
303
+ if (parts.length >= 5 && parts[3] === 'offset') {
304
+ const ox = parseFloat(parts[4]);
305
+ const oy = parts.length >= 6 ? parseFloat(parts[5]) : 0;
306
+ if (!isNaN(ox) && !isNaN(oy)) illu.offset = [ox, oy];
307
+ }
308
+ illustration = illu;
300
309
  continue;
301
310
  }
302
311