brand-intent 0.17.0 → 0.19.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 +1 -1
- package/parser/brandParser.ts +59 -10
- package/parser/dateWindows.ts +34 -0
- package/parser/index.ts +3 -0
- package/parser/purposeParser.ts +16 -0
- package/parser/types.ts +3 -0
- package/vscode-extension/syntaxes/brand.tmLanguage.json +16 -5
- package/vscode-extension/syntaxes/purpose.tmLanguage.json +8 -0
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 ────────────────────────────────────────────────────────────────
|
|
@@ -118,6 +120,8 @@ export interface ParsedBrand {
|
|
|
118
120
|
camera?: ParsedCameraDef;
|
|
119
121
|
/** Photo treatment (`photo-look` block). Absent → photos stay as taken. */
|
|
120
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[]>;
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
export type PhotoLookKey = 'saturation' | 'contrast' | 'warmth' | 'fade' | 'mono' | 'duotone';
|
|
@@ -131,7 +135,7 @@ export interface ParsedPhotoLookDef {
|
|
|
131
135
|
warmth: number;
|
|
132
136
|
/** Lifted black point, 0..100. */
|
|
133
137
|
fade: number;
|
|
134
|
-
/**
|
|
138
|
+
/** Grayscale (monochrome). */
|
|
135
139
|
mono: boolean;
|
|
136
140
|
/** Map luminance onto two colors (resolved hex), shadows → highlights. Excludes `mono`. */
|
|
137
141
|
duotone?: { shadow: string; highlight: string };
|
|
@@ -192,6 +196,8 @@ export interface ParsedAssetDef {
|
|
|
192
196
|
alignVariants?: Partial<Record<LogoAlign, { src: string; aspectRatio?: number }>>;
|
|
193
197
|
/** Library group (e.g. "experimental"). Consumers list each group as its own section after the ungrouped assets. */
|
|
194
198
|
group?: string;
|
|
199
|
+
/** Photos only: name of the date window the photo fits (`when:`). Absent → all year. */
|
|
200
|
+
when?: string;
|
|
195
201
|
}
|
|
196
202
|
|
|
197
203
|
export type LogoAlign = 'left' | 'center' | 'right';
|
|
@@ -401,7 +407,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
401
407
|
tokensSource: 'figma',
|
|
402
408
|
};
|
|
403
409
|
|
|
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';
|
|
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';
|
|
405
411
|
let activeBlock: ActiveBlock = 'none';
|
|
406
412
|
let currentThemeName = '';
|
|
407
413
|
let currentDividerName = '';
|
|
@@ -452,6 +458,8 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
452
458
|
let cameraDeclared = false;
|
|
453
459
|
const photoLookRaw: Record<string, { value: string; line: number }> = {};
|
|
454
460
|
let photoLookDeclared = false;
|
|
461
|
+
const windowsRaw: Array<{ name: string; value: string; line: number }> = [];
|
|
462
|
+
let windowsDeclared = false;
|
|
455
463
|
const framingsRaw: Array<{ id: string; line: number; props: Record<string, { value: string; line: number }> }> = [];
|
|
456
464
|
|
|
457
465
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -852,6 +860,15 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
852
860
|
else photoLookRaw[entry.slice(0, colonIdx).trim()] = { value: entry.slice(colonIdx + 1).trim(), line: lineNum };
|
|
853
861
|
continue;
|
|
854
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
|
+
}
|
|
855
872
|
if (activeBlock === 'framing') {
|
|
856
873
|
const trimmed = raw.trim();
|
|
857
874
|
if (trimmed.startsWith('#')) continue;
|
|
@@ -940,6 +957,14 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
940
957
|
continue;
|
|
941
958
|
}
|
|
942
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
|
+
|
|
943
968
|
// framing <id> — photo framing module (horizon position)
|
|
944
969
|
const framingMatch = raw.match(/^framing\s+(\S+)\s*$/);
|
|
945
970
|
if (framingMatch) {
|
|
@@ -1177,13 +1202,16 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1177
1202
|
if (!Number.isInteger(distance) || distance < 1) throw new Error(`${fileName}: color-families distance must be a whole number ≥ 1, got "${familiesProps.distance}"`);
|
|
1178
1203
|
if (familiesRaw.length === 0) throw new Error(`${fileName}: color-families declares no family`);
|
|
1179
1204
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1205
|
+
/** Comma-separated `$refs` to brand-colors (`shared:`, `photo-fallback:`). */
|
|
1206
|
+
const colorRefs = (key: string): Array<{ name: string; hex: string }> =>
|
|
1207
|
+
(familiesProps[key] ?? '').split(',').map(r => r.trim()).filter(Boolean).map(ref => {
|
|
1208
|
+
if (!ref.startsWith('$')) throw new Error(`${fileName}: color-families ${key} entries must be $refs to brand-colors, got "${ref}"`);
|
|
1209
|
+
const hex = brandColors[ref.slice(1)] ? normalizeHex(brandColors[ref.slice(1)]) : null;
|
|
1210
|
+
if (!hex || !isHex6(hex)) throw new Error(`${fileName}: color-families ${key} "${ref}" is not a known 6-digit brand color`);
|
|
1211
|
+
return { name: ref.slice(1), hex };
|
|
1212
|
+
});
|
|
1213
|
+
const shared = colorRefs('shared');
|
|
1214
|
+
const photoFallback = colorRefs('photo-fallback');
|
|
1187
1215
|
|
|
1188
1216
|
const families: ColorFamily[] = [];
|
|
1189
1217
|
for (const raw of familiesRaw) {
|
|
@@ -1220,7 +1248,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1220
1248
|
const twin = dated.find(o => o !== f && o.from === f.from);
|
|
1221
1249
|
if (twin) throw new Error(`${fileName}:${f.line}: families "${twin.name}" and "${f.name}" share from: ${f.from}`);
|
|
1222
1250
|
}
|
|
1223
|
-
colorFamilies = { order, borrow, distance, shared, families };
|
|
1251
|
+
colorFamilies = { order, borrow, distance, shared, ...(photoFallback.length > 0 ? { photoFallback } : {}), families };
|
|
1224
1252
|
if (borrow !== 'none' && reachCoversAll(families.length, { order, borrow, distance })) {
|
|
1225
1253
|
colorWarnings.push(`color-families: borrow "${borrow}" with distance ${distance} reaches every family — the borrow rule has no effect`);
|
|
1226
1254
|
}
|
|
@@ -1499,6 +1527,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1499
1527
|
if (raw.props['gltf']) def.gltfSrc = raw.props['gltf'];
|
|
1500
1528
|
if (raw.props['generator']) def.generator = raw.props['generator'];
|
|
1501
1529
|
if (raw.props['group']) def.group = raw.props['group'];
|
|
1530
|
+
if (raw.props['when']) def.when = raw.props['when'];
|
|
1502
1531
|
// generator-params are stored as "generator-params.KEY" by the sub-block parser.
|
|
1503
1532
|
const paramEntries: [string, number][] = [];
|
|
1504
1533
|
for (const [k, v] of Object.entries(raw.props)) {
|
|
@@ -1518,6 +1547,26 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1518
1547
|
}
|
|
1519
1548
|
return def;
|
|
1520
1549
|
};
|
|
1550
|
+
// Finalise date windows (before assets: photo `when:` references them)
|
|
1551
|
+
if (windowsDeclared) {
|
|
1552
|
+
const windows: Record<string, DateWindowRange[]> = {};
|
|
1553
|
+
for (const w of windowsRaw) {
|
|
1554
|
+
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}"`);
|
|
1555
|
+
if (windows[w.name]) throw new Error(`${fileName}:${w.line}: duplicate window "${w.name}"`);
|
|
1556
|
+
try { windows[w.name] = parseDateWindowRanges(w.value); }
|
|
1557
|
+
catch (e) { throw new Error(`${fileName}:${w.line}: window "${w.name}": ${(e as Error).message}`); }
|
|
1558
|
+
}
|
|
1559
|
+
result.windows = windows;
|
|
1560
|
+
}
|
|
1561
|
+
for (const [kind, arr] of [['logo', logosRaw], ['video', videosRaw], ['illustration', illustrationsRaw]] as const) {
|
|
1562
|
+
const a = arr.find(r => r.props['when']);
|
|
1563
|
+
if (a) throw new Error(`${fileName}: ${kind} "${a.id}": when: is for photos only`);
|
|
1564
|
+
}
|
|
1565
|
+
for (const p of photosRaw) {
|
|
1566
|
+
const when = p.props['when'];
|
|
1567
|
+
if (when && !result.windows?.[when]) throw new Error(`${fileName}: photo "${p.id}" references unknown window "${when}"`);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1521
1570
|
if (logosRaw.length > 0 || photosRaw.length > 0 || videosRaw.length > 0 || illustrationsRaw.length > 0) {
|
|
1522
1571
|
result.assets = {};
|
|
1523
1572
|
if (logosRaw.length > 0) result.assets.logos = logosRaw.map(toAssetDef);
|
|
@@ -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
|
@@ -26,6 +26,9 @@ export {
|
|
|
26
26
|
export type { ReachConfig } from './colorSystem';
|
|
27
27
|
export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef, BrandColorPrintSpec, ParsedAssetDef, ParsedBadgeStyle, LogoAlign, ParsedCounterDef, CounterFormat, CounterShape, ParsedFramingDef, ParsedCameraDef, ParsedPhotoLookDef, PhotoLookKey } from './brandParser';
|
|
28
28
|
|
|
29
|
+
export { parseDateWindowRanges, isInDateWindow, SAMPLE_WINDOW_MARKER } from './dateWindows';
|
|
30
|
+
export type { DateWindowRange } from './dateWindows';
|
|
31
|
+
|
|
29
32
|
export { parseIconRef, parseIconSource, resolveIconRef, parseListItem, phosphorName, PHOSPHOR_WEIGHTS } from './icons';
|
|
30
33
|
export type { IconRef, IconSource, ParsedIconDef, ResolvedIcon, PhosphorWeight } from './icons';
|
|
31
34
|
export type {
|
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')) {
|
package/parser/types.ts
CHANGED
|
@@ -168,6 +168,9 @@ export interface ColorFamilies {
|
|
|
168
168
|
distance: number;
|
|
169
169
|
/** Colors allowed in every family (resolved from `shared:` refs). */
|
|
170
170
|
shared: Array<{ name: string; hex: string }>;
|
|
171
|
+
/** Text and figure colors for photos no pair reads on, in order of preference (`photo-fallback:` refs).
|
|
172
|
+
* Used only over photos, never in pairs or guided generation. Absent when not declared. */
|
|
173
|
+
photoFallback?: Array<{ name: string; hex: string }>;
|
|
171
174
|
families: ColorFamily[];
|
|
172
175
|
}
|
|
173
176
|
|
|
@@ -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" },
|
|
@@ -119,8 +120,8 @@
|
|
|
119
120
|
}
|
|
120
121
|
},
|
|
121
122
|
"color-families-property": {
|
|
122
|
-
"comment": "Block keys under color-families: order/borrow/distance/shared",
|
|
123
|
-
"match": "^( {2})(order|borrow|distance|shared)(:\\s*)(.+)$",
|
|
123
|
+
"comment": "Block keys under color-families: order/borrow/distance/shared/photo-fallback",
|
|
124
|
+
"match": "^( {2})(order|borrow|distance|shared|photo-fallback)(:\\s*)(.+)$",
|
|
124
125
|
"captures": {
|
|
125
126
|
"2": { "name": "support.type.property-name.brand-intent-brand" },
|
|
126
127
|
"3": { "name": "punctuation.separator.key-value.brand-intent-brand" },
|
|
@@ -399,8 +400,8 @@
|
|
|
399
400
|
}
|
|
400
401
|
},
|
|
401
402
|
"icons-counter-header": {
|
|
402
|
-
"comment": "Bare `icons` / `counter` / `camera` / `photo-look` block headers.",
|
|
403
|
-
"match": "^(icons|counter|camera|photo-look)\\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": [
|