brand-intent 0.16.0 → 0.18.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
package/parser/brandParser.ts
CHANGED
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
reachCoversAll,
|
|
38
38
|
} from './colorSystem';
|
|
39
39
|
import { parseIconSource, isIconId } from './icons';
|
|
40
|
+
import { parseDateWindowRanges } from './dateWindows';
|
|
41
|
+
import type { DateWindowRange } from './dateWindows';
|
|
40
42
|
import type { ParsedIconDef } from './icons';
|
|
41
43
|
|
|
42
44
|
// ── Parsed type ────────────────────────────────────────────────────────────────
|
|
@@ -116,6 +118,29 @@ export interface ParsedBrand {
|
|
|
116
118
|
framings?: ParsedFramingDef[];
|
|
117
119
|
/** Camera defaults (`camera` block). Absent → rear camera, no grid. */
|
|
118
120
|
camera?: ParsedCameraDef;
|
|
121
|
+
/** Photo treatment (`photo-look` block). Absent → photos stay as taken. */
|
|
122
|
+
photoLook?: ParsedPhotoLookDef;
|
|
123
|
+
/** Named yearly date windows (`windows` block): name → ranges. Referenced by sample `@name` markers and photo `when:`. */
|
|
124
|
+
windows?: Record<string, DateWindowRange[]>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type PhotoLookKey = 'saturation' | 'contrast' | 'warmth' | 'fade' | 'mono' | 'duotone';
|
|
128
|
+
|
|
129
|
+
export interface ParsedPhotoLookDef {
|
|
130
|
+
/** -100..100, 0 = unchanged. */
|
|
131
|
+
saturation: number;
|
|
132
|
+
/** -100..100, 0 = unchanged. */
|
|
133
|
+
contrast: number;
|
|
134
|
+
/** -100 (cool) .. 100 (warm), 0 = unchanged. */
|
|
135
|
+
warmth: number;
|
|
136
|
+
/** Lifted black point, 0..100. */
|
|
137
|
+
fade: number;
|
|
138
|
+
/** Grayscale (monochrome). */
|
|
139
|
+
mono: boolean;
|
|
140
|
+
/** Map luminance onto two colors (resolved hex), shadows → highlights. Excludes `mono`. */
|
|
141
|
+
duotone?: { shadow: string; highlight: string };
|
|
142
|
+
/** Keys the person cannot change. All others are starting values. */
|
|
143
|
+
fixed: PhotoLookKey[];
|
|
119
144
|
}
|
|
120
145
|
|
|
121
146
|
export interface ParsedCameraDef {
|
|
@@ -171,6 +196,8 @@ export interface ParsedAssetDef {
|
|
|
171
196
|
alignVariants?: Partial<Record<LogoAlign, { src: string; aspectRatio?: number }>>;
|
|
172
197
|
/** Library group (e.g. "experimental"). Consumers list each group as its own section after the ungrouped assets. */
|
|
173
198
|
group?: string;
|
|
199
|
+
/** Photos only: name of the date window the photo fits (`when:`). Absent → all year. */
|
|
200
|
+
when?: string;
|
|
174
201
|
}
|
|
175
202
|
|
|
176
203
|
export type LogoAlign = 'left' | 'center' | 'right';
|
|
@@ -380,7 +407,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
380
407
|
tokensSource: 'figma',
|
|
381
408
|
};
|
|
382
409
|
|
|
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';
|
|
410
|
+
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' | 'windows';
|
|
384
411
|
let activeBlock: ActiveBlock = 'none';
|
|
385
412
|
let currentThemeName = '';
|
|
386
413
|
let currentDividerName = '';
|
|
@@ -429,6 +456,10 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
429
456
|
let counterDeclared = false;
|
|
430
457
|
const cameraRaw: Record<string, { value: string; line: number }> = {};
|
|
431
458
|
let cameraDeclared = false;
|
|
459
|
+
const photoLookRaw: Record<string, { value: string; line: number }> = {};
|
|
460
|
+
let photoLookDeclared = false;
|
|
461
|
+
const windowsRaw: Array<{ name: string; value: string; line: number }> = [];
|
|
462
|
+
let windowsDeclared = false;
|
|
432
463
|
const framingsRaw: Array<{ id: string; line: number; props: Record<string, { value: string; line: number }> }> = [];
|
|
433
464
|
|
|
434
465
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -817,6 +848,27 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
817
848
|
cameraRaw[entry.slice(0, colonIdx).trim()] = { value: entry.slice(colonIdx + 1).trim(), line: lineNum };
|
|
818
849
|
continue;
|
|
819
850
|
}
|
|
851
|
+
if (activeBlock === 'photo-look') {
|
|
852
|
+
const trimmed = raw.trim();
|
|
853
|
+
if (trimmed.startsWith('#')) continue;
|
|
854
|
+
// `# …` is a comment, but `#RGB` / `#RRGGBB` is a duotone color
|
|
855
|
+
const commentIdx = trimmed.search(/\s+#(?![0-9a-f]{3}(?:[0-9a-f]{3}(?:[0-9a-f]{2})?)?(?:\s|$))/i);
|
|
856
|
+
const entry = commentIdx !== -1 ? trimmed.slice(0, commentIdx) : trimmed;
|
|
857
|
+
const colonIdx = entry.indexOf(':');
|
|
858
|
+
// Bare flag (`mono`) or KEY: VALUE
|
|
859
|
+
if (colonIdx === -1) photoLookRaw[entry.trim()] = { value: '', line: lineNum };
|
|
860
|
+
else photoLookRaw[entry.slice(0, colonIdx).trim()] = { value: entry.slice(colonIdx + 1).trim(), line: lineNum };
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (activeBlock === 'windows') {
|
|
864
|
+
const trimmed = raw.trim();
|
|
865
|
+
if (trimmed.startsWith('#')) continue;
|
|
866
|
+
const entry = trimmed.replace(/\s+#.*$/, '');
|
|
867
|
+
const colonIdx = entry.indexOf(':');
|
|
868
|
+
if (colonIdx === -1) throw new Error(`${fileName}:${lineNum}: window must be "NAME: MM-DD..MM-DD", got "${trimmed}"`);
|
|
869
|
+
windowsRaw.push({ name: entry.slice(0, colonIdx).trim(), value: entry.slice(colonIdx + 1).trim(), line: lineNum });
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
820
872
|
if (activeBlock === 'framing') {
|
|
821
873
|
const trimmed = raw.trim();
|
|
822
874
|
if (trimmed.startsWith('#')) continue;
|
|
@@ -897,6 +949,22 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
897
949
|
continue;
|
|
898
950
|
}
|
|
899
951
|
|
|
952
|
+
// photo-look — photo treatment (saturation)
|
|
953
|
+
if (raw.trim() === 'photo-look') {
|
|
954
|
+
if (photoLookDeclared) throw new Error(`${fileName}:${lineNum}: duplicate photo-look block`);
|
|
955
|
+
photoLookDeclared = true;
|
|
956
|
+
activeBlock = 'photo-look';
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// windows — named yearly date windows
|
|
961
|
+
if (raw.trim() === 'windows') {
|
|
962
|
+
if (windowsDeclared) throw new Error(`${fileName}:${lineNum}: duplicate windows block`);
|
|
963
|
+
windowsDeclared = true;
|
|
964
|
+
activeBlock = 'windows';
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
|
|
900
968
|
// framing <id> — photo framing module (horizon position)
|
|
901
969
|
const framingMatch = raw.match(/^framing\s+(\S+)\s*$/);
|
|
902
970
|
if (framingMatch) {
|
|
@@ -1456,6 +1524,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1456
1524
|
if (raw.props['gltf']) def.gltfSrc = raw.props['gltf'];
|
|
1457
1525
|
if (raw.props['generator']) def.generator = raw.props['generator'];
|
|
1458
1526
|
if (raw.props['group']) def.group = raw.props['group'];
|
|
1527
|
+
if (raw.props['when']) def.when = raw.props['when'];
|
|
1459
1528
|
// generator-params are stored as "generator-params.KEY" by the sub-block parser.
|
|
1460
1529
|
const paramEntries: [string, number][] = [];
|
|
1461
1530
|
for (const [k, v] of Object.entries(raw.props)) {
|
|
@@ -1475,6 +1544,26 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1475
1544
|
}
|
|
1476
1545
|
return def;
|
|
1477
1546
|
};
|
|
1547
|
+
// Finalise date windows (before assets: photo `when:` references them)
|
|
1548
|
+
if (windowsDeclared) {
|
|
1549
|
+
const windows: Record<string, DateWindowRange[]> = {};
|
|
1550
|
+
for (const w of windowsRaw) {
|
|
1551
|
+
if (!/^[a-z][a-z0-9-]*$/.test(w.name)) throw new Error(`${fileName}:${w.line}: window name must be lowercase kebab-case, got "${w.name}"`);
|
|
1552
|
+
if (windows[w.name]) throw new Error(`${fileName}:${w.line}: duplicate window "${w.name}"`);
|
|
1553
|
+
try { windows[w.name] = parseDateWindowRanges(w.value); }
|
|
1554
|
+
catch (e) { throw new Error(`${fileName}:${w.line}: window "${w.name}": ${(e as Error).message}`); }
|
|
1555
|
+
}
|
|
1556
|
+
result.windows = windows;
|
|
1557
|
+
}
|
|
1558
|
+
for (const [kind, arr] of [['logo', logosRaw], ['video', videosRaw], ['illustration', illustrationsRaw]] as const) {
|
|
1559
|
+
const a = arr.find(r => r.props['when']);
|
|
1560
|
+
if (a) throw new Error(`${fileName}: ${kind} "${a.id}": when: is for photos only`);
|
|
1561
|
+
}
|
|
1562
|
+
for (const p of photosRaw) {
|
|
1563
|
+
const when = p.props['when'];
|
|
1564
|
+
if (when && !result.windows?.[when]) throw new Error(`${fileName}: photo "${p.id}" references unknown window "${when}"`);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1478
1567
|
if (logosRaw.length > 0 || photosRaw.length > 0 || videosRaw.length > 0 || illustrationsRaw.length > 0) {
|
|
1479
1568
|
result.assets = {};
|
|
1480
1569
|
if (logosRaw.length > 0) result.assets.logos = logosRaw.map(toAssetDef);
|
|
@@ -1582,6 +1671,60 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1582
1671
|
result.camera = camera;
|
|
1583
1672
|
}
|
|
1584
1673
|
|
|
1674
|
+
// Finalise photo treatment
|
|
1675
|
+
if (photoLookDeclared) {
|
|
1676
|
+
const keys: PhotoLookKey[] = ['saturation', 'contrast', 'warmth', 'fade', 'mono', 'duotone'];
|
|
1677
|
+
for (const [key, { line }] of Object.entries(photoLookRaw)) {
|
|
1678
|
+
if (key !== 'fixed' && !keys.includes(key as PhotoLookKey)) throw new Error(`${fileName}:${line}: unknown photo-look property "${key}"`);
|
|
1679
|
+
}
|
|
1680
|
+
const int = (key: string, min: number, max: number): number => {
|
|
1681
|
+
const p = photoLookRaw[key];
|
|
1682
|
+
if (!p) return 0;
|
|
1683
|
+
const n = Number(p.value);
|
|
1684
|
+
if (!/^[+-]?\d+$/.test(p.value) || n < min || n > max) {
|
|
1685
|
+
throw new Error(`${fileName}:${p.line}: photo-look ${key} must be an integer from ${min} to ${max}, got "${p.value}"`);
|
|
1686
|
+
}
|
|
1687
|
+
return n;
|
|
1688
|
+
};
|
|
1689
|
+
const look: ParsedPhotoLookDef = {
|
|
1690
|
+
saturation: int('saturation', -100, 100),
|
|
1691
|
+
contrast: int('contrast', -100, 100),
|
|
1692
|
+
warmth: int('warmth', -100, 100),
|
|
1693
|
+
fade: int('fade', 0, 100),
|
|
1694
|
+
mono: false,
|
|
1695
|
+
fixed: [],
|
|
1696
|
+
};
|
|
1697
|
+
const mono = photoLookRaw['mono'];
|
|
1698
|
+
if (mono) {
|
|
1699
|
+
if (mono.value !== '') throw new Error(`${fileName}:${mono.line}: photo-look mono is a flag and takes no value`);
|
|
1700
|
+
look.mono = true;
|
|
1701
|
+
}
|
|
1702
|
+
const duo = photoLookRaw['duotone'];
|
|
1703
|
+
if (duo) {
|
|
1704
|
+
if (mono) throw new Error(`${fileName}:${duo.line}: photo-look cannot be both mono and duotone`);
|
|
1705
|
+
const refs = duo.value.split(/\s+/);
|
|
1706
|
+
if (refs.length !== 2) throw new Error(`${fileName}:${duo.line}: photo-look duotone must be "SHADOW HIGHLIGHT", got "${duo.value}"`);
|
|
1707
|
+
const [shadow, highlight] = refs.map(ref => {
|
|
1708
|
+
if (ref.startsWith('$') && !brandColors[ref.slice(1)]) {
|
|
1709
|
+
throw new Error(`${fileName}:${duo.line}: photo-look duotone references unknown brand color "${ref}"`);
|
|
1710
|
+
}
|
|
1711
|
+
const hex = normalizeHex(ref.startsWith('$') ? brandColors[ref.slice(1)] : ref);
|
|
1712
|
+
if (!hex) throw new Error(`${fileName}:${duo.line}: photo-look duotone colors must be $ref or #HEX, got "${ref}"`);
|
|
1713
|
+
return hex;
|
|
1714
|
+
});
|
|
1715
|
+
look.duotone = { shadow, highlight };
|
|
1716
|
+
}
|
|
1717
|
+
const fixed = photoLookRaw['fixed'];
|
|
1718
|
+
if (fixed) {
|
|
1719
|
+
for (const key of fixed.value.split(/\s+/)) {
|
|
1720
|
+
if (!keys.includes(key as PhotoLookKey)) throw new Error(`${fileName}:${fixed.line}: photo-look fixed lists unknown key "${key}"`);
|
|
1721
|
+
if (!photoLookRaw[key]) throw new Error(`${fileName}:${fixed.line}: photo-look fixed lists "${key}", which is not set`);
|
|
1722
|
+
look.fixed.push(key as PhotoLookKey);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
result.photoLook = look;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1585
1728
|
// Finalise framing modules
|
|
1586
1729
|
if (framingsRaw.length > 0) {
|
|
1587
1730
|
const known = new Set(['label', 'horizon', 'description']);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Date windows — named yearly date ranges (`windows` block in .brand).
|
|
2
|
+
// Samples (`@name`) and brand photos (`when:`) that only fit part of the year reference them.
|
|
3
|
+
|
|
4
|
+
/** One yearly range, inclusive, as `MM-DD`. `from` after `to` wraps over the year end. */
|
|
5
|
+
export interface DateWindowRange {
|
|
6
|
+
from: string;
|
|
7
|
+
to: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
11
|
+
|
|
12
|
+
/** Parse `MM-DD..MM-DD[, MM-DD..MM-DD …]`. Throws a message without file/line prefix. */
|
|
13
|
+
export function parseDateWindowRanges(value: string): DateWindowRange[] {
|
|
14
|
+
const parts = value.split(',').map(p => p.trim());
|
|
15
|
+
if (parts.length === 0 || parts.some(p => !p)) throw new Error(`expected "MM-DD..MM-DD", got "${value}"`);
|
|
16
|
+
return parts.map(part => {
|
|
17
|
+
const m = part.match(/^(\d{2}-\d{2})\.\.(\d{2}-\d{2})$/);
|
|
18
|
+
if (!m) throw new Error(`expected "MM-DD..MM-DD", got "${part}"`);
|
|
19
|
+
for (const md of [m[1], m[2]]) {
|
|
20
|
+
const [month, day] = md.split('-').map(Number);
|
|
21
|
+
if (month < 1 || month > 12 || day < 1 || day > DAYS_IN_MONTH[month - 1]) throw new Error(`invalid date "${md}"`);
|
|
22
|
+
}
|
|
23
|
+
return { from: m[1], to: m[2] };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** True when `date` (local calendar day) falls in any of the ranges. */
|
|
28
|
+
export function isInDateWindow(ranges: DateWindowRange[], date: Date): boolean {
|
|
29
|
+
const key = `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
30
|
+
return ranges.some(({ from, to }) => (from <= to ? key >= from && key <= to : key >= from || key <= to));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Trailing window marker on a sample line: `Raps blüht im Mai @rapsbluete`. */
|
|
34
|
+
export const SAMPLE_WINDOW_MARKER = /\s+@([a-z][a-z0-9-]*)\s*$/;
|
package/parser/index.ts
CHANGED
|
@@ -24,7 +24,10 @@ 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
|
+
|
|
29
|
+
export { parseDateWindowRanges, isInDateWindow, SAMPLE_WINDOW_MARKER } from './dateWindows';
|
|
30
|
+
export type { DateWindowRange } from './dateWindows';
|
|
28
31
|
|
|
29
32
|
export { parseIconRef, parseIconSource, resolveIconRef, parseListItem, phosphorName, PHOSPHOR_WEIGHTS } from './icons';
|
|
30
33
|
export type { IconRef, IconSource, ParsedIconDef, ResolvedIcon, PhosphorWeight } from './icons';
|
package/parser/purposeParser.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { SlotId } from './types';
|
|
9
9
|
import { parseIconRef, parseListItem } from './icons';
|
|
10
10
|
import type { IconRef } from './icons';
|
|
11
|
+
import { SAMPLE_WINDOW_MARKER } from './dateWindows';
|
|
11
12
|
|
|
12
13
|
// ── Parsed types ──────────────────────────────────────────────────────────────
|
|
13
14
|
|
|
@@ -55,6 +56,9 @@ export interface ParsedPurposeSlot {
|
|
|
55
56
|
label: string;
|
|
56
57
|
/** Sample text variants for this slot. Picked randomly on page creation. */
|
|
57
58
|
samples: string[];
|
|
59
|
+
/** Date window per sample (parallel to `samples`), from a trailing `@name` marker; undefined = all year.
|
|
60
|
+
* Absent when no sample in the slot has a marker. Names resolve against the `.brand` `windows` block. */
|
|
61
|
+
sampleWindows?: (string | undefined)[];
|
|
58
62
|
/** Fixed value — when set, this slot is never AI-generated or shuffled. Mutually exclusive with samples. */
|
|
59
63
|
value?: string;
|
|
60
64
|
maxLength: number;
|
|
@@ -166,6 +170,18 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
166
170
|
s.marker = 'icon';
|
|
167
171
|
}
|
|
168
172
|
if (s.defaultIcon && s.marker !== 'icon') throw new Error(`${fileName}:${lineNum}: slot "${s.id}": defaultIcon needs "marker: icon"`);
|
|
173
|
+
if (!s.value && s.samples) {
|
|
174
|
+
// Trailing `@name` window marker, on the sample's first line only
|
|
175
|
+
const windows = s.samples.map((sample, i) => {
|
|
176
|
+
const [first, ...rest] = sample.split('\n');
|
|
177
|
+
if (rest.some(l => SAMPLE_WINDOW_MARKER.test(l))) throw new Error(`${fileName}:${lineNum}: slot "${s.id}": window marker belongs on the sample's first line ("${rest.find(l => SAMPLE_WINDOW_MARKER.test(l))}")`);
|
|
178
|
+
const m = first.match(SAMPLE_WINDOW_MARKER);
|
|
179
|
+
if (!m) return undefined;
|
|
180
|
+
s.samples![i] = [first.slice(0, m.index).trimEnd(), ...rest].join('\n');
|
|
181
|
+
return m[1];
|
|
182
|
+
});
|
|
183
|
+
if (windows.some(Boolean)) s.sampleWindows = windows;
|
|
184
|
+
}
|
|
169
185
|
if (s.slotType === 'list') {
|
|
170
186
|
for (const sample of s.samples ?? []) {
|
|
171
187
|
for (const item of sample.split('\n')) {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
{ "include": "#color-group-reference" },
|
|
31
31
|
{ "include": "#color-group-entry-property" },
|
|
32
32
|
{ "include": "#icons-counter-header" },
|
|
33
|
+
{ "include": "#window-entry" },
|
|
33
34
|
{ "include": "#icon-entry" },
|
|
34
35
|
{ "include": "#counter-value" },
|
|
35
36
|
{ "include": "#framing-horizon" },
|
|
@@ -399,8 +400,8 @@
|
|
|
399
400
|
}
|
|
400
401
|
},
|
|
401
402
|
"icons-counter-header": {
|
|
402
|
-
"comment": "Bare `icons` / `counter` block headers.",
|
|
403
|
-
"match": "^(icons|counter)\\s*$",
|
|
403
|
+
"comment": "Bare `icons` / `counter` / `camera` / `photo-look` / `windows` block headers.",
|
|
404
|
+
"match": "^(icons|counter|camera|photo-look|windows)\\s*$",
|
|
404
405
|
"captures": {
|
|
405
406
|
"1": { "name": "keyword.control.brand-intent-brand" }
|
|
406
407
|
}
|
|
@@ -421,6 +422,16 @@
|
|
|
421
422
|
"11": { "name": "comment.line.number-sign.brand-intent-brand" }
|
|
422
423
|
}
|
|
423
424
|
},
|
|
425
|
+
"window-entry": {
|
|
426
|
+
"comment": "windows entry: NAME: MM-DD..MM-DD[, MM-DD..MM-DD]",
|
|
427
|
+
"match": "^(\\s{2,})([a-z][a-z0-9-]*)(:\\s*)(\\d{2}-\\d{2}\\.\\.\\d{2}-\\d{2}(?:\\s*,\\s*\\d{2}-\\d{2}\\.\\.\\d{2}-\\d{2})*)\\s*(#.*)?$",
|
|
428
|
+
"captures": {
|
|
429
|
+
"2": { "name": "variable.other.brand-intent-brand" },
|
|
430
|
+
"3": { "name": "punctuation.separator.key-value.brand-intent-brand" },
|
|
431
|
+
"4": { "name": "constant.numeric.brand-intent-brand" },
|
|
432
|
+
"5": { "name": "comment.line.number-sign.brand-intent-brand" }
|
|
433
|
+
}
|
|
434
|
+
},
|
|
424
435
|
"framing-horizon": {
|
|
425
436
|
"comment": "framing block horizon: N% | none",
|
|
426
437
|
"match": "^(\\s{2,})(horizon)(:\\s*)(\\d+(?:\\.\\d+)?%|none)\\s*(#.*)?$",
|
|
@@ -466,7 +477,7 @@
|
|
|
466
477
|
},
|
|
467
478
|
"keyword-partial": {
|
|
468
479
|
"comment": "Fallback: color the keyword token on incomplete lines (while typing)",
|
|
469
|
-
"match": "^(id:|name:|language:|locale:|domain:|app-icon:|favicon:|og-image:|tokens:|figma-file-key:|figma-collection:|tokens-file:|spacing:|label:|cta:|color-mode:|color-strategy:|color-adherence:|brand-colors|state-colors|color-bounds|color-rules|color-families|theme|divider|font|typography|logo|photo|video|illustration|icons|counter|framing)(?=\\s|$)",
|
|
480
|
+
"match": "^(id:|name:|language:|locale:|domain:|app-icon:|favicon:|og-image:|tokens:|figma-file-key:|figma-collection:|tokens-file:|spacing:|label:|cta:|color-mode:|color-strategy:|color-adherence:|brand-colors|state-colors|color-bounds|color-rules|color-families|theme|divider|font|typography|logo|photo|video|illustration|icons|counter|framing|windows)(?=\\s|$)",
|
|
470
481
|
"captures": {
|
|
471
482
|
"1": { "name": "keyword.control.brand-intent-brand" }
|
|
472
483
|
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
{ "include": "#list-property" },
|
|
25
25
|
{ "include": "#sample-item" },
|
|
26
26
|
{ "include": "#sample-icon-continuation" },
|
|
27
|
+
{ "include": "#sample-window" },
|
|
27
28
|
{ "include": "#property" },
|
|
28
29
|
{ "include": "#keyword-partial" }
|
|
29
30
|
],
|
|
@@ -246,6 +247,13 @@
|
|
|
246
247
|
"2": { "name": "entity.name.function.brand-intent-purpose" }
|
|
247
248
|
}
|
|
248
249
|
},
|
|
250
|
+
"sample-window": {
|
|
251
|
+
"comment": "Trailing @name date-window marker on a sample line",
|
|
252
|
+
"match": "(?<=\\s)(@[a-z][a-z0-9-]*)\\s*$",
|
|
253
|
+
"captures": {
|
|
254
|
+
"1": { "name": "variable.other.brand-intent-purpose" }
|
|
255
|
+
}
|
|
256
|
+
},
|
|
249
257
|
"keyword-partial": {
|
|
250
258
|
"comment": "Fallback: color the keyword token on incomplete lines (while typing)",
|
|
251
259
|
"patterns": [
|