brand-intent 0.25.0 → 0.26.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brand-intent",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
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",
@@ -37,6 +37,8 @@ export interface ParsedPurpose {
37
37
  defaultPhoto?: string;
38
38
  /** Default video asset ID — pre-selects this video on page creation. */
39
39
  defaultVideo?: string;
40
+ /** Illustration ids a new page picks from; a .story page's `illustration:` overrides them. */
41
+ illustrations?: string[];
40
42
  /** When false, hides Take Photo / Upload buttons for this purpose. Default: true. */
41
43
  camera?: boolean;
42
44
  /** `camera: front | rear` — overrides the brand's camera facing for this purpose. */
@@ -170,6 +172,7 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
170
172
  let defaultLogo: string | undefined;
171
173
  let defaultPhoto: string | undefined;
172
174
  let defaultVideo: string | undefined;
175
+ let illustrations: string[] | undefined;
173
176
  let camera: boolean | undefined;
174
177
  let cameraFacing: 'front' | 'rear' | undefined;
175
178
  let framing: string | undefined;
@@ -422,6 +425,12 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
422
425
  continue;
423
426
  }
424
427
 
428
+ // Illustrations a new page picks from
429
+ if (line.startsWith('illustration:')) {
430
+ illustrations = line.slice(13).split(',').map(id => id.trim()).filter(Boolean);
431
+ continue;
432
+ }
433
+
425
434
  // Camera control
426
435
  if (line.startsWith('camera:')) {
427
436
  const value = line.slice(7).trim();
@@ -788,7 +797,7 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
788
797
 
789
798
  const parsedExamples = resolveExamples(examples, slots, legacySamplesLine, fileName, warnings);
790
799
 
791
- return { id, name, description, preferredCompositions: compositions, slots, density, palette, scope, context, voice, defaultLogo, defaultPhoto, defaultVideo, camera, cameraFacing, framing, goal, analyze, defaultIcon, bgPrompt, examples: parsedExamples, warnings };
800
+ return { id, name, description, preferredCompositions: compositions, slots, density, palette, scope, context, voice, defaultLogo, defaultPhoto, defaultVideo, illustrations, camera, cameraFacing, framing, goal, analyze, defaultIcon, bgPrompt, examples: parsedExamples, warnings };
792
801
  }
793
802
 
794
803
  /** Validate `example` blocks against the declared slots, or build examples from deprecated slot
@@ -38,6 +38,8 @@ export interface ParsedStoryPage {
38
38
  purpose: string;
39
39
  /** Composition id; undefined = the implementation picks one of the purpose's compositions. */
40
40
  composition?: string;
41
+ /** Illustration ids; implementations pick one per story date, the same one every time for that date. */
42
+ illustrations?: string[];
41
43
  /** Page topic: what this page adds to the story. */
42
44
  topic?: string;
43
45
  /** Fixed slot texts, may contain `{variable}`s. A slot the page doesn't name is open. */
@@ -190,9 +192,10 @@ export function parseStoryFile(content: string, fileName: string): ParsedStory {
190
192
  // Page lines (2-space)
191
193
  if (currentPage) {
192
194
  if (indent !== 2) throw new Error(`${fileName}:${lineNum}: unexpected indent in page (page keys use 2 spaces)`);
193
- const read = readKey(line, lineNum, currentPage, ['purpose', 'composition'], true);
195
+ const read = readKey(line, lineNum, currentPage, ['purpose', 'composition', 'illustration'], true);
194
196
  if (read && 'multi' in read) { multiKey = read.multi; multiIndent = 4; }
195
197
  else if (read?.key === 'purpose') currentPage.purpose = read.value;
198
+ else if (read?.key === 'illustration') currentPage.illustrations = read.value.split(',').map(id => id.trim()).filter(Boolean);
196
199
  else if (read) currentPage.composition = read.value;
197
200
  continue;
198
201
  }
@@ -278,7 +281,7 @@ export function parseStoryFile(content: string, fileName: string): ParsedStory {
278
281
  * empty when the story fits. Variables count at their longest form for `maxLength`. */
279
282
  export function checkStory(
280
283
  story: ParsedStory,
281
- brand: { purposes: ParsedPurpose[]; compositions: string[]; formats: string[] },
284
+ brand: { purposes: ParsedPurpose[]; compositions: string[]; formats: string[]; illustrations?: string[] },
282
285
  fileName: string,
283
286
  ): string[] {
284
287
  const problems: string[] = [];
@@ -308,6 +311,9 @@ export function checkStory(
308
311
  if (!brand.compositions.includes(page.composition)) say(`${where}: composition "${page.composition}" does not exist`);
309
312
  else if (!purpose.preferredCompositions.includes(page.composition)) say(`${where}: composition "${page.composition}" is not one of purpose "${purpose.id}"'s compositions`);
310
313
  }
314
+ for (const id of page.illustrations ?? []) {
315
+ if (brand.illustrations && !brand.illustrations.includes(id)) say(`${where}: illustration "${id}" does not exist`);
316
+ }
311
317
  checkTexts(where, purpose, page.texts);
312
318
  });
313
319
  story.examples.forEach((ex, i) => {
@@ -110,7 +110,7 @@
110
110
  ]
111
111
  },
112
112
  "asset-field": {
113
- "match": "^(logo:|photo:|video:)\\s+([\\w-]+)",
113
+ "match": "^(logo:|photo:|video:|illustration:)\\s+([\\w, -]+)",
114
114
  "captures": {
115
115
  "1": { "name": "keyword.control.brand-intent-purpose" },
116
116
  "2": { "name": "entity.name.type.brand-intent-purpose" }
@@ -265,7 +265,7 @@
265
265
  "comment": "Fallback: color the keyword token on incomplete lines (while typing)",
266
266
  "patterns": [
267
267
  {
268
- "match": "^(id:|name:|compositions:|density:|defaultIcon:|scope|voice|palette:|logo:|photo:|video:|camera:|framing:|goal:|analyze:|bg-prompt:|context|slot|example)(?=\\s|$)",
268
+ "match": "^(id:|name:|compositions:|density:|defaultIcon:|scope|voice|palette:|logo:|photo:|video:|illustration:|camera:|framing:|goal:|analyze:|bg-prompt:|context|slot|example)(?=\\s|$)",
269
269
  "captures": {
270
270
  "1": { "name": "keyword.control.brand-intent-purpose" }
271
271
  }
@@ -37,7 +37,7 @@
37
37
  }
38
38
  },
39
39
  "reference": {
40
- "match": "^\\s+(purpose|composition)(:)\\s+([\\w:-]+)",
40
+ "match": "^\\s+(purpose|composition|illustration)(:)\\s+([\\w:, -]+)",
41
41
  "captures": {
42
42
  "1": { "name": "support.type.property-name.brand-intent-story" },
43
43
  "2": { "name": "punctuation.separator.brand-intent-story" },
@@ -66,7 +66,7 @@
66
66
  }
67
67
  },
68
68
  {
69
- "match": "^\\s+(purpose|composition|topic|primary|secondary|detail|meta|cta|label)(?=:|\\s|$)",
69
+ "match": "^\\s+(purpose|composition|illustration|topic|primary|secondary|detail|meta|cta|label)(?=:|\\s|$)",
70
70
  "captures": {
71
71
  "1": { "name": "support.type.property-name.brand-intent-story" }
72
72
  }