brand-intent 0.20.0 → 0.20.2
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 +6 -1
- package/parser/colorSystem.ts +39 -10
- package/parser/index.ts +2 -1
package/package.json
CHANGED
package/parser/brandParser.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
derivePairSlots,
|
|
38
38
|
resolveTextColor,
|
|
39
39
|
checkFamilyPair,
|
|
40
|
+
toneAnchorsFrom,
|
|
40
41
|
reachCoversAll,
|
|
41
42
|
cycleMembers,
|
|
42
43
|
LEVEL_SLOTS,
|
|
@@ -1626,6 +1627,10 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1626
1627
|
// Family pairs → checked; valid ones become themes named `{Family}-{n}` (n = declared position).
|
|
1627
1628
|
if (colorFamilies) {
|
|
1628
1629
|
const config = { order: colorFamilies.order, borrow: colorFamilies.borrow, distance: colorFamilies.distance };
|
|
1630
|
+
const toneAnchors = toneAnchorsFrom(tones?.colors, name => {
|
|
1631
|
+
const raw = brandColors[name];
|
|
1632
|
+
return raw ? normalizeHex(raw) ?? undefined : undefined;
|
|
1633
|
+
});
|
|
1629
1634
|
colorFamilies.families.forEach((family, index) => {
|
|
1630
1635
|
const raw = familiesRaw[index];
|
|
1631
1636
|
raw.pairs.forEach((p, n) => {
|
|
@@ -1635,7 +1640,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1635
1640
|
if (!hex || !isHex6(hex)) throw new Error(`${fileName}:${p.line}: pair color "${value}" is not a known 6-digit color`);
|
|
1636
1641
|
return hex;
|
|
1637
1642
|
};
|
|
1638
|
-
const check = checkFamilyPair(resolve(p.ground), resolve(p.figure), index, colorFamilies!, config, rules, neutralText);
|
|
1643
|
+
const check = checkFamilyPair(resolve(p.ground), resolve(p.figure), index, colorFamilies!, config, rules, neutralText, toneAnchors);
|
|
1639
1644
|
const themeName = `${family.name}-${n + 1}`;
|
|
1640
1645
|
if (check.valid) {
|
|
1641
1646
|
if (themes[themeName]) throw new Error(`${fileName}: family pair theme "${themeName}" collides with an authored theme`);
|
package/parser/colorSystem.ts
CHANGED
|
@@ -195,23 +195,50 @@ export function reachCoversAll(count: number, config: ReachConfig): boolean {
|
|
|
195
195
|
return true;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
/** Where a color comes from, seen from family `index`. Null = not usable there.
|
|
198
|
+
/** Where a color comes from, seen from family `index`. Null = not usable there.
|
|
199
|
+
* `toneAnchors` maps a tone's hex to the hex of the color(s) whose ladder holds it, so a tone
|
|
200
|
+
* counts as its color: a family color's tone is own, a shared color's tone shared, a neighbour's by reach. */
|
|
199
201
|
export function colorOrigin(
|
|
200
202
|
hex: string,
|
|
201
203
|
index: number,
|
|
202
204
|
system: Pick<ColorFamilies, 'families' | 'shared'>,
|
|
203
205
|
config: ReachConfig,
|
|
206
|
+
toneAnchors: ToneAnchors = {},
|
|
204
207
|
): PairOrigin | null {
|
|
205
208
|
const families = system.families;
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
const candidates = [hex, ...(toneAnchors[hex] ?? [])];
|
|
210
|
+
const holds = (colors: Array<{ hex: string }>) => colors.some(c => candidates.includes(c.hex));
|
|
211
|
+
if (families[index] && holds(families[index].colors)) return 'own';
|
|
212
|
+
if (holds(system.shared)) return 'shared';
|
|
208
213
|
const reach = reachableFamilies(families.length, index, config, cycleMembers(families));
|
|
209
214
|
for (const [j, dir] of reach) {
|
|
210
|
-
if (families[j].colors
|
|
215
|
+
if (holds(families[j].colors)) return dir;
|
|
211
216
|
}
|
|
212
217
|
return null;
|
|
213
218
|
}
|
|
214
219
|
|
|
220
|
+
/** Tone hex → hex of every color whose declared ladder holds it (`tones` block). Empty without tones. */
|
|
221
|
+
export type ToneAnchors = Record<string, string[]>;
|
|
222
|
+
|
|
223
|
+
/** Build `ToneAnchors` from ladders (color name → step → hex) and the colors' own hexes. */
|
|
224
|
+
export function toneAnchorsFrom(
|
|
225
|
+
ladders: Record<string, Record<number, string>> | undefined,
|
|
226
|
+
colorHex: (name: string) => string | undefined,
|
|
227
|
+
): ToneAnchors {
|
|
228
|
+
const out: ToneAnchors = {};
|
|
229
|
+
for (const [name, ladder] of Object.entries(ladders ?? {})) {
|
|
230
|
+
const anchor = colorHex(name);
|
|
231
|
+
if (!anchor) continue;
|
|
232
|
+
for (const hex of Object.values(ladder)) {
|
|
233
|
+
if (hex === anchor) continue;
|
|
234
|
+
const list: string[] = out[hex] ?? [];
|
|
235
|
+
if (!list.includes(anchor)) list.push(anchor);
|
|
236
|
+
out[hex] = list;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
215
242
|
/**
|
|
216
243
|
* Family in season on `date`: the one with the latest `from:` on or before that
|
|
217
244
|
* month/day, wrapping to the year's last start before the first one. Uses the
|
|
@@ -229,9 +256,10 @@ export function familyIndexForDate(families: Pick<ColorFamily, 'from'>[], date:
|
|
|
229
256
|
return best !== -1 ? best : latest;
|
|
230
257
|
}
|
|
231
258
|
|
|
232
|
-
/** Name of the family that declares `hex` (first match), for error messages. */
|
|
233
|
-
export function owningFamily(hex: string, families: ColorFamily[]): string | undefined {
|
|
234
|
-
|
|
259
|
+
/** Name of the family that declares `hex` or the color it is a tone of (first match), for error messages. */
|
|
260
|
+
export function owningFamily(hex: string, families: ColorFamily[], toneAnchors: ToneAnchors = {}): string | undefined {
|
|
261
|
+
const candidates = [hex, ...(toneAnchors[hex] ?? [])];
|
|
262
|
+
return families.find(f => f.colors.some(c => candidates.includes(c.hex)))?.name;
|
|
235
263
|
}
|
|
236
264
|
|
|
237
265
|
/** Text candidates for a family ground: its contrast color(s), then shared colors, then extras. */
|
|
@@ -252,15 +280,16 @@ export function checkFamilyPair(
|
|
|
252
280
|
config: ReachConfig,
|
|
253
281
|
rules: ColorRules,
|
|
254
282
|
extraTextCandidates: string[] = [],
|
|
283
|
+
toneAnchors: ToneAnchors = {},
|
|
255
284
|
): PairCheck {
|
|
256
285
|
const reasons: string[] = [];
|
|
257
286
|
const origins: [PairOrigin | null, PairOrigin | null] = [
|
|
258
|
-
colorOrigin(ground, index, system, config),
|
|
259
|
-
colorOrigin(figure, index, system, config),
|
|
287
|
+
colorOrigin(ground, index, system, config, toneAnchors),
|
|
288
|
+
colorOrigin(figure, index, system, config, toneAnchors),
|
|
260
289
|
];
|
|
261
290
|
for (const [hex, origin] of [[ground, origins[0]], [figure, origins[1]]] as const) {
|
|
262
291
|
if (origin === null) {
|
|
263
|
-
const owner = owningFamily(hex, system.families);
|
|
292
|
+
const owner = owningFamily(hex, system.families, toneAnchors);
|
|
264
293
|
reasons.push(owner ? `${hex} from ${owner} is out of reach` : `${hex} is not in any family or shared`);
|
|
265
294
|
}
|
|
266
295
|
}
|
package/parser/index.ts
CHANGED
|
@@ -21,13 +21,14 @@ export {
|
|
|
21
21
|
levelRuleViolations,
|
|
22
22
|
reachCoversAll,
|
|
23
23
|
colorOrigin,
|
|
24
|
+
toneAnchorsFrom,
|
|
24
25
|
familyTextCandidates,
|
|
25
26
|
checkFamilyPair,
|
|
26
27
|
enumerateFamilyPairs,
|
|
27
28
|
familyIndexForDate,
|
|
28
29
|
DEFAULT_COLOR_RULES,
|
|
29
30
|
} from './colorSystem';
|
|
30
|
-
export type { ReachConfig } from './colorSystem';
|
|
31
|
+
export type { ReachConfig, ToneAnchors } from './colorSystem';
|
|
31
32
|
export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef, BrandColorPrintSpec, ParsedAssetDef, ParsedBadgeStyle, LogoAlign, ParsedCounterDef, CounterFormat, CounterShape, ParsedFramingDef, ParsedCameraDef, ParsedPhotoLookDef, PhotoLookKey } from './brandParser';
|
|
32
33
|
|
|
33
34
|
export { parseDateWindowRanges, isInDateWindow, SAMPLE_WINDOW_MARKER } from './dateWindows';
|