brand-intent 0.15.0 → 0.16.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.15.0",
3
+ "version": "0.16.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",
@@ -250,6 +250,14 @@ const VALID_STRATEGIES = new Set<PaletteStrategy>(['monochrome', 'duotone', 'tri
250
250
  const VALID_ADHERENCES = new Set<Adherence>(['strict', 'on-brand', 'flexible']);
251
251
  const TONE_CLASSES = new Set<ToneClass>(['light', 'mid', 'dark']);
252
252
  const FAMILY_ROLES = new Set<FamilyRole>(['core', 'accent', 'contrast']);
253
+
254
+ const DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
255
+ function isMonthDay(value: string): boolean {
256
+ const m = value.match(/^(\d{2})-(\d{2})$/);
257
+ if (!m) return false;
258
+ const month = Number(m[1]), day = Number(m[2]);
259
+ return month >= 1 && month <= 12 && day >= 1 && day <= DAYS_IN_MONTH[month - 1];
260
+ }
253
261
  const FAMILY_ORDERS = new Set<FamilyOrder>(['cycle', 'sequence']);
254
262
  const FAMILY_BORROWS = new Set<FamilyBorrow>(['previous', 'next', 'both', 'none']);
255
263
  const PAIR_KEYS = ['ground', 'figure', 'text'] as const;
@@ -395,7 +403,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
395
403
  const adherenceOverridesRaw: Record<string, string> = {};
396
404
  const colorRulesRaw: Record<string, string> = {};
397
405
  let colorRulesDeclared = false;
398
- type RawFamily = { name: string; line: number; colors: Array<{ role: string; value: string; line: number }>; pairs: Array<{ ground: string; figure: string; line: number }> };
406
+ type RawFamily = { name: string; line: number; from?: string; colors: Array<{ role: string; value: string; line: number }>; pairs: Array<{ ground: string; figure: string; line: number }> };
399
407
  const familiesProps: Record<string, string> = {};
400
408
  const familiesRaw: RawFamily[] = [];
401
409
  let familiesDeclared = false;
@@ -653,8 +661,16 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
653
661
  }
654
662
  const colonIdx = trimmed.indexOf(':');
655
663
  const key = colonIdx !== -1 ? trimmed.slice(0, colonIdx).trim() : trimmed;
664
+ if (key === 'from' && colonIdx !== -1) {
665
+ const value = trimmed.slice(colonIdx + 1).trim();
666
+ if (currentFamily.from !== undefined) throw new Error(`${fileName}:${lineNum}: family "${currentFamily.name}" declares from: twice`);
667
+ if (!isMonthDay(value)) throw new Error(`${fileName}:${lineNum}: family "${currentFamily.name}" from: must be "MM-DD" (e.g. 12-01), got "${value}"`);
668
+ inPairs = false;
669
+ currentFamily.from = value;
670
+ continue;
671
+ }
656
672
  if (colonIdx === -1 || !FAMILY_ROLES.has(key as FamilyRole)) {
657
- throw new Error(`${fileName}:${lineNum}: family "${currentFamily.name}": expected core / accent / contrast / pairs, got "${trimmed}"`);
673
+ throw new Error(`${fileName}:${lineNum}: family "${currentFamily.name}": expected from / core / accent / contrast / pairs, got "${trimmed}"`);
658
674
  }
659
675
  inPairs = false;
660
676
  currentFamily.colors.push({ role: key, value: trimmed.slice(colonIdx + 1).trim(), line: lineNum });
@@ -1150,7 +1166,16 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
1150
1166
  }
1151
1167
  brandColors[c.key] = c.hex;
1152
1168
  }
1153
- families.push({ name: raw.name, colors, pairs: [] });
1169
+ families.push({ name: raw.name, ...(raw.from ? { from: raw.from } : {}), colors, pairs: [] });
1170
+ }
1171
+ const dated = familiesRaw.filter(f => f.from !== undefined);
1172
+ if (dated.length > 0 && dated.length < familiesRaw.length) {
1173
+ const missing = familiesRaw.filter(f => f.from === undefined).map(f => f.name).join(', ');
1174
+ throw new Error(`${fileName}: color-families: from: must be declared on every family or on none (missing: ${missing})`);
1175
+ }
1176
+ for (const f of dated) {
1177
+ const twin = dated.find(o => o !== f && o.from === f.from);
1178
+ if (twin) throw new Error(`${fileName}:${f.line}: families "${twin.name}" and "${f.name}" share from: ${f.from}`);
1154
1179
  }
1155
1180
  colorFamilies = { order, borrow, distance, shared, families };
1156
1181
  if (borrow !== 'none' && reachCoversAll(families.length, { order, borrow, distance })) {
@@ -193,6 +193,23 @@ export function colorOrigin(
193
193
  return null;
194
194
  }
195
195
 
196
+ /**
197
+ * Family in season on `date`: the one with the latest `from:` on or before that
198
+ * month/day, wrapping to the year's last start before the first one. Uses the
199
+ * date's local calendar day. -1 when the families declare no dates.
200
+ */
201
+ export function familyIndexForDate(families: Pick<ColorFamily, 'from'>[], date: Date): number {
202
+ if (families.length === 0 || families.some(f => !f.from)) return -1;
203
+ const today = `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
204
+ let best = -1;
205
+ let latest = -1;
206
+ families.forEach((f, i) => {
207
+ if (f.from! <= today && (best === -1 || f.from! > families[best].from!)) best = i;
208
+ if (latest === -1 || f.from! > families[latest].from!) latest = i;
209
+ });
210
+ return best !== -1 ? best : latest;
211
+ }
212
+
196
213
  /** Name of the family that declares `hex` (first match), for error messages. */
197
214
  export function owningFamily(hex: string, families: ColorFamily[]): string | undefined {
198
215
  return families.find(f => f.colors.some(c => c.hex === hex))?.name;
package/parser/index.ts CHANGED
@@ -20,6 +20,7 @@ export {
20
20
  familyTextCandidates,
21
21
  checkFamilyPair,
22
22
  enumerateFamilyPairs,
23
+ familyIndexForDate,
23
24
  DEFAULT_COLOR_RULES,
24
25
  } from './colorSystem';
25
26
  export type { ReachConfig } from './colorSystem';
package/parser/types.ts CHANGED
@@ -156,6 +156,8 @@ export interface FamilyPair extends PairCheck {
156
156
 
157
157
  export interface ColorFamily {
158
158
  name: string;
159
+ /** Start date `MM-DD` (`from:`). Declared on every family or on none. */
160
+ from?: string;
159
161
  colors: FamilyColor[];
160
162
  pairs: FamilyPair[];
161
163
  }
@@ -15,6 +15,7 @@
15
15
  { "include": "#color-rules-entry" },
16
16
  { "include": "#color-families-property" },
17
17
  { "include": "#family-header" },
18
+ { "include": "#family-from" },
18
19
  { "include": "#family-role" },
19
20
  { "include": "#family-pairs-keyword" },
20
21
  { "include": "#color-mode-enum" },
@@ -143,6 +144,15 @@
143
144
  "5": { "name": "string.unquoted.brand-intent-brand" }
144
145
  }
145
146
  },
147
+ "family-from": {
148
+ "comment": "Family start date: from: MM-DD",
149
+ "match": "^(\\s{4,})(from)(:\\s*)(\\d{2}-\\d{2})\\s*$",
150
+ "captures": {
151
+ "2": { "name": "support.type.property-name.brand-intent-brand" },
152
+ "3": { "name": "punctuation.separator.key-value.brand-intent-brand" },
153
+ "4": { "name": "constant.numeric.brand-intent-brand" }
154
+ }
155
+ },
146
156
  "family-pairs-keyword": {
147
157
  "match": "^(\\s{4,})(pairs)\\s*$",
148
158
  "captures": {