brand-intent 0.32.0 → 0.34.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.32.0",
3
+ "version": "0.34.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",
@@ -138,6 +138,8 @@ export interface ParsedPhotoLookDef {
138
138
  duotone?: { shadow: string; highlight: string };
139
139
  /** Keys the person cannot change. All others are starting values. */
140
140
  fixed: PhotoLookKey[];
141
+ /** Darkest a gradient under light text on a photo may get, 0..100 (%). Absent → no scrim. */
142
+ scrim?: number;
141
143
  }
142
144
 
143
145
  export interface ParsedCameraDef {
@@ -1973,7 +1975,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
1973
1975
  if (photoLookDeclared) {
1974
1976
  const keys: PhotoLookKey[] = ['saturation', 'contrast', 'warmth', 'fade', 'mono', 'duotone'];
1975
1977
  for (const [key, { line }] of Object.entries(photoLookRaw)) {
1976
- if (key !== 'fixed' && !keys.includes(key as PhotoLookKey)) throw new Error(`${fileName}:${line}: unknown photo-look property "${key}"`);
1978
+ if (key !== 'fixed' && key !== 'scrim' && !keys.includes(key as PhotoLookKey)) throw new Error(`${fileName}:${line}: unknown photo-look property "${key}"`);
1977
1979
  }
1978
1980
  const int = (key: string, min: number, max: number): number => {
1979
1981
  const p = photoLookRaw[key];
@@ -2021,6 +2023,14 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
2021
2023
  look.fixed.push(key as PhotoLookKey);
2022
2024
  }
2023
2025
  }
2026
+ const scrim = photoLookRaw['scrim'];
2027
+ if (scrim) {
2028
+ const m = /^(\d+)%$/.exec(scrim.value);
2029
+ if (!m || Number(m[1]) < 1 || Number(m[1]) > 100) {
2030
+ throw new Error(`${fileName}:${scrim.line}: photo-look scrim must be a percentage from 1% to 100%, got "${scrim.value}"`);
2031
+ }
2032
+ look.scrim = Number(m[1]);
2033
+ }
2024
2034
  result.photoLook = look;
2025
2035
  }
2026
2036
 
package/parser/icons.ts CHANGED
@@ -138,3 +138,10 @@ export function parseListItem(item: string): { icon?: IconRef; text: string } {
138
138
  if (!m) return { text: item };
139
139
  return { icon: parseIconRef(m[1], `list item "${item}"`), text: m[2] };
140
140
  }
141
+
142
+ /** A `labels: above` list item: `Label: value`, split at the first colon followed by a space. */
143
+ export function splitLabelledItem(text: string): { label?: string; value: string } {
144
+ const at = text.indexOf(': ');
145
+ if (at <= 0) return { value: text };
146
+ return { label: text.slice(0, at).trim(), value: text.slice(at + 2).trim() };
147
+ }
package/parser/index.ts CHANGED
@@ -34,7 +34,7 @@ export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef,
34
34
  export { parseDateWindowRanges, isInDateWindow, SAMPLE_WINDOW_MARKER } from './dateWindows';
35
35
  export type { DateWindowRange } from './dateWindows';
36
36
 
37
- export { parseIconRef, parseIconSource, resolveIconRef, parseListItem, phosphorName, PHOSPHOR_WEIGHTS } from './icons';
37
+ export { parseIconRef, parseIconSource, resolveIconRef, parseListItem, splitLabelledItem, phosphorName, PHOSPHOR_WEIGHTS } from './icons';
38
38
  export type { IconRef, IconSource, ParsedIconDef, ResolvedIcon, PhosphorWeight } from './icons';
39
39
  export type {
40
40
  ColorMode,
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { SlotId } from './types';
9
- import { parseIconRef, parseListItem } from './icons';
9
+ import { parseIconRef, parseListItem, splitLabelledItem } from './icons';
10
10
  import type { IconRef } from './icons';
11
11
  import { SAMPLE_WINDOW_MARKER } from './dateWindows';
12
12
 
@@ -105,6 +105,14 @@ export interface ParsedPurposeSlot {
105
105
  itemGap?: number;
106
106
  /** List layout direction: 'vertical' (default) or 'horizontal'. */
107
107
  listDirection?: 'vertical' | 'horizontal';
108
+ /** Grid columns for a vertical list, filled row by row (list slots only). Default: 1 */
109
+ listColumns?: number;
110
+ /** 'above': each item is `Label: value`, the label set small above the value (list slots only). */
111
+ listLabels?: 'none' | 'above';
112
+ /** Label size relative to the item text, with `labels: above`. Default: 0.7 */
113
+ labelSize?: number;
114
+ /** Line between grid columns (list slots with `columns:` 2 or more). */
115
+ listDivider?: 'none' | 'hairline';
108
116
  /** When false, this slot is locked — users cannot edit it in any mode. Default: true (editable). */
109
117
  editable?: boolean;
110
118
  typography: {
@@ -210,6 +218,9 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
210
218
  if (s.marker && s.marker !== 'icon') throw new Error(`${fileName}:${lineNum}: slot "${s.id}": iconOnly needs "marker: icon", got "${s.marker}"`);
211
219
  s.marker = 'icon';
212
220
  }
221
+ if (s.listColumns && s.listColumns > 1 && s.listDirection === 'horizontal') throw new Error(`${fileName}:${lineNum}: slot "${s.id}": columns needs a vertical list`);
222
+ if (s.labelSize !== undefined && s.listLabels !== 'above') throw new Error(`${fileName}:${lineNum}: slot "${s.id}": labelSize needs "labels: above"`);
223
+ if (s.listDivider === 'hairline' && !(s.listColumns && s.listColumns > 1)) throw new Error(`${fileName}:${lineNum}: slot "${s.id}": divider needs "columns:" 2 or more`);
213
224
  if (s.defaultIcon && s.marker !== 'icon') throw new Error(`${fileName}:${lineNum}: slot "${s.id}": defaultIcon needs "marker: icon"`);
214
225
  if (!s.value && s.samples) {
215
226
  // Trailing `@name` window marker, on the sample's first line only
@@ -644,6 +655,30 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
644
655
  currentSlot.listDirection = d;
645
656
  continue;
646
657
  }
658
+ if (prop.startsWith('columns:')) {
659
+ const n = Number(prop.slice(8).trim());
660
+ if (!Number.isInteger(n) || n < 1 || n > 6) throw new Error(`${fileName}:${lineNum}: invalid columns "${prop.slice(8).trim()}" — must be 1 to 6`);
661
+ currentSlot.listColumns = n;
662
+ continue;
663
+ }
664
+ if (prop.startsWith('labels:')) {
665
+ const l = prop.slice(7).trim();
666
+ if (l !== 'none' && l !== 'above') throw new Error(`${fileName}:${lineNum}: invalid labels "${l}" — must be "none" or "above"`);
667
+ currentSlot.listLabels = l;
668
+ continue;
669
+ }
670
+ if (prop.startsWith('labelSize:')) {
671
+ const n = parseFloat(prop.slice(10).trim());
672
+ if (isNaN(n) || n <= 0 || n > 1) throw new Error(`${fileName}:${lineNum}: invalid labelSize "${prop.slice(10).trim()}" — must be above 0 and at most 1`);
673
+ currentSlot.labelSize = n;
674
+ continue;
675
+ }
676
+ if (prop.startsWith('divider:')) {
677
+ const d = prop.slice(8).trim();
678
+ if (d !== 'none' && d !== 'hairline') throw new Error(`${fileName}:${lineNum}: invalid list divider "${d}" — must be "none" or "hairline"`);
679
+ currentSlot.listDivider = d;
680
+ continue;
681
+ }
647
682
  throw new Error(`${fileName}:${lineNum}: unknown list property "${prop}"`);
648
683
  }
649
684
  }
@@ -828,6 +863,7 @@ function resolveExamples(
828
863
  for (const item of text.split('\n')) {
829
864
  const { icon } = parseListItem(item);
830
865
  if (icon && slot.marker !== 'icon') throw new Error(`${at(slotId)}: slot "${slotId}": item "${item}" names an icon, but the list has no "marker: icon"`);
866
+ if (slot.listLabels === 'above' && !splitLabelledItem(item).label) throw new Error(`${at(slotId)}: slot "${slotId}": item "${item}" needs a label ("Label: value") for "labels: above"`);
831
867
  }
832
868
  }
833
869
  }
@@ -232,7 +232,7 @@
232
232
  },
233
233
  "list-property": {
234
234
  "comment": "4-space indented properties under type: list blocks",
235
- "match": "^\\s{4,}(marker|icons|iconOnly|iconGap|itemGap|maxItems|direction)(:)\\s+(.*)",
235
+ "match": "^\\s{4,}(marker|icons|iconOnly|iconGap|itemGap|maxItems|direction|columns|labels|labelSize|divider)(:)\\s+(.*)",
236
236
  "captures": {
237
237
  "1": { "name": "support.type.property-name.brand-intent-purpose" },
238
238
  "2": { "name": "punctuation.separator.brand-intent-purpose" },