brand-intent 0.16.0 → 0.17.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.16.0",
3
+ "version": "0.17.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",
@@ -116,6 +116,27 @@ export interface ParsedBrand {
116
116
  framings?: ParsedFramingDef[];
117
117
  /** Camera defaults (`camera` block). Absent → rear camera, no grid. */
118
118
  camera?: ParsedCameraDef;
119
+ /** Photo treatment (`photo-look` block). Absent → photos stay as taken. */
120
+ photoLook?: ParsedPhotoLookDef;
121
+ }
122
+
123
+ export type PhotoLookKey = 'saturation' | 'contrast' | 'warmth' | 'fade' | 'mono' | 'duotone';
124
+
125
+ export interface ParsedPhotoLookDef {
126
+ /** -100..100, 0 = unchanged. */
127
+ saturation: number;
128
+ /** -100..100, 0 = unchanged. */
129
+ contrast: number;
130
+ /** -100 (cool) .. 100 (warm), 0 = unchanged. */
131
+ warmth: number;
132
+ /** Lifted black point, 0..100. */
133
+ fade: number;
134
+ /** Black-and-white. */
135
+ mono: boolean;
136
+ /** Map luminance onto two colors (resolved hex), shadows → highlights. Excludes `mono`. */
137
+ duotone?: { shadow: string; highlight: string };
138
+ /** Keys the person cannot change. All others are starting values. */
139
+ fixed: PhotoLookKey[];
119
140
  }
120
141
 
121
142
  export interface ParsedCameraDef {
@@ -380,7 +401,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
380
401
  tokensSource: 'figma',
381
402
  };
382
403
 
383
- 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';
404
+ 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' | 'photo-look';
384
405
  let activeBlock: ActiveBlock = 'none';
385
406
  let currentThemeName = '';
386
407
  let currentDividerName = '';
@@ -429,6 +450,8 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
429
450
  let counterDeclared = false;
430
451
  const cameraRaw: Record<string, { value: string; line: number }> = {};
431
452
  let cameraDeclared = false;
453
+ const photoLookRaw: Record<string, { value: string; line: number }> = {};
454
+ let photoLookDeclared = false;
432
455
  const framingsRaw: Array<{ id: string; line: number; props: Record<string, { value: string; line: number }> }> = [];
433
456
 
434
457
  for (let i = 0; i < lines.length; i++) {
@@ -817,6 +840,18 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
817
840
  cameraRaw[entry.slice(0, colonIdx).trim()] = { value: entry.slice(colonIdx + 1).trim(), line: lineNum };
818
841
  continue;
819
842
  }
843
+ if (activeBlock === 'photo-look') {
844
+ const trimmed = raw.trim();
845
+ if (trimmed.startsWith('#')) continue;
846
+ // `# …` is a comment, but `#RGB` / `#RRGGBB` is a duotone color
847
+ const commentIdx = trimmed.search(/\s+#(?![0-9a-f]{3}(?:[0-9a-f]{3}(?:[0-9a-f]{2})?)?(?:\s|$))/i);
848
+ const entry = commentIdx !== -1 ? trimmed.slice(0, commentIdx) : trimmed;
849
+ const colonIdx = entry.indexOf(':');
850
+ // Bare flag (`mono`) or KEY: VALUE
851
+ if (colonIdx === -1) photoLookRaw[entry.trim()] = { value: '', line: lineNum };
852
+ else photoLookRaw[entry.slice(0, colonIdx).trim()] = { value: entry.slice(colonIdx + 1).trim(), line: lineNum };
853
+ continue;
854
+ }
820
855
  if (activeBlock === 'framing') {
821
856
  const trimmed = raw.trim();
822
857
  if (trimmed.startsWith('#')) continue;
@@ -897,6 +932,14 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
897
932
  continue;
898
933
  }
899
934
 
935
+ // photo-look — photo treatment (saturation)
936
+ if (raw.trim() === 'photo-look') {
937
+ if (photoLookDeclared) throw new Error(`${fileName}:${lineNum}: duplicate photo-look block`);
938
+ photoLookDeclared = true;
939
+ activeBlock = 'photo-look';
940
+ continue;
941
+ }
942
+
900
943
  // framing <id> — photo framing module (horizon position)
901
944
  const framingMatch = raw.match(/^framing\s+(\S+)\s*$/);
902
945
  if (framingMatch) {
@@ -1582,6 +1625,60 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
1582
1625
  result.camera = camera;
1583
1626
  }
1584
1627
 
1628
+ // Finalise photo treatment
1629
+ if (photoLookDeclared) {
1630
+ const keys: PhotoLookKey[] = ['saturation', 'contrast', 'warmth', 'fade', 'mono', 'duotone'];
1631
+ for (const [key, { line }] of Object.entries(photoLookRaw)) {
1632
+ if (key !== 'fixed' && !keys.includes(key as PhotoLookKey)) throw new Error(`${fileName}:${line}: unknown photo-look property "${key}"`);
1633
+ }
1634
+ const int = (key: string, min: number, max: number): number => {
1635
+ const p = photoLookRaw[key];
1636
+ if (!p) return 0;
1637
+ const n = Number(p.value);
1638
+ if (!/^[+-]?\d+$/.test(p.value) || n < min || n > max) {
1639
+ throw new Error(`${fileName}:${p.line}: photo-look ${key} must be an integer from ${min} to ${max}, got "${p.value}"`);
1640
+ }
1641
+ return n;
1642
+ };
1643
+ const look: ParsedPhotoLookDef = {
1644
+ saturation: int('saturation', -100, 100),
1645
+ contrast: int('contrast', -100, 100),
1646
+ warmth: int('warmth', -100, 100),
1647
+ fade: int('fade', 0, 100),
1648
+ mono: false,
1649
+ fixed: [],
1650
+ };
1651
+ const mono = photoLookRaw['mono'];
1652
+ if (mono) {
1653
+ if (mono.value !== '') throw new Error(`${fileName}:${mono.line}: photo-look mono is a flag and takes no value`);
1654
+ look.mono = true;
1655
+ }
1656
+ const duo = photoLookRaw['duotone'];
1657
+ if (duo) {
1658
+ if (mono) throw new Error(`${fileName}:${duo.line}: photo-look cannot be both mono and duotone`);
1659
+ const refs = duo.value.split(/\s+/);
1660
+ if (refs.length !== 2) throw new Error(`${fileName}:${duo.line}: photo-look duotone must be "SHADOW HIGHLIGHT", got "${duo.value}"`);
1661
+ const [shadow, highlight] = refs.map(ref => {
1662
+ if (ref.startsWith('$') && !brandColors[ref.slice(1)]) {
1663
+ throw new Error(`${fileName}:${duo.line}: photo-look duotone references unknown brand color "${ref}"`);
1664
+ }
1665
+ const hex = normalizeHex(ref.startsWith('$') ? brandColors[ref.slice(1)] : ref);
1666
+ if (!hex) throw new Error(`${fileName}:${duo.line}: photo-look duotone colors must be $ref or #HEX, got "${ref}"`);
1667
+ return hex;
1668
+ });
1669
+ look.duotone = { shadow, highlight };
1670
+ }
1671
+ const fixed = photoLookRaw['fixed'];
1672
+ if (fixed) {
1673
+ for (const key of fixed.value.split(/\s+/)) {
1674
+ if (!keys.includes(key as PhotoLookKey)) throw new Error(`${fileName}:${fixed.line}: photo-look fixed lists unknown key "${key}"`);
1675
+ if (!photoLookRaw[key]) throw new Error(`${fileName}:${fixed.line}: photo-look fixed lists "${key}", which is not set`);
1676
+ look.fixed.push(key as PhotoLookKey);
1677
+ }
1678
+ }
1679
+ result.photoLook = look;
1680
+ }
1681
+
1585
1682
  // Finalise framing modules
1586
1683
  if (framingsRaw.length > 0) {
1587
1684
  const known = new Set(['label', 'horizon', 'description']);
package/parser/index.ts CHANGED
@@ -24,7 +24,7 @@ export {
24
24
  DEFAULT_COLOR_RULES,
25
25
  } from './colorSystem';
26
26
  export type { ReachConfig } from './colorSystem';
27
- export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef, BrandColorPrintSpec, ParsedAssetDef, ParsedBadgeStyle, LogoAlign, ParsedCounterDef, CounterFormat, CounterShape, ParsedFramingDef, ParsedCameraDef } from './brandParser';
27
+ export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef, BrandColorPrintSpec, ParsedAssetDef, ParsedBadgeStyle, LogoAlign, ParsedCounterDef, CounterFormat, CounterShape, ParsedFramingDef, ParsedCameraDef, ParsedPhotoLookDef, PhotoLookKey } from './brandParser';
28
28
 
29
29
  export { parseIconRef, parseIconSource, resolveIconRef, parseListItem, phosphorName, PHOSPHOR_WEIGHTS } from './icons';
30
30
  export type { IconRef, IconSource, ParsedIconDef, ResolvedIcon, PhosphorWeight } from './icons';
@@ -399,8 +399,8 @@
399
399
  }
400
400
  },
401
401
  "icons-counter-header": {
402
- "comment": "Bare `icons` / `counter` block headers.",
403
- "match": "^(icons|counter)\\s*$",
402
+ "comment": "Bare `icons` / `counter` / `camera` / `photo-look` block headers.",
403
+ "match": "^(icons|counter|camera|photo-look)\\s*$",
404
404
  "captures": {
405
405
  "1": { "name": "keyword.control.brand-intent-brand" }
406
406
  }