brand-intent 0.32.0 → 0.33.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.33.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",
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" },