okchroma 0.9.1 → 0.9.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.
@@ -597,14 +597,11 @@ var RED_SOLVE = {
597
597
  brickCoolGold: -3,
598
598
  brickDesat: 0.85,
599
599
  brickExtraDeep: 0.02,
600
- // red's usable zones (her density map, error-range-checks.json): deep core or the light
601
- // edge tier — the .50-.58 middle is ring territory, canonical itself lives there, so a
602
- // lightened brand ALWAYS gets a deep-core red. Cool-first beside warm brands.
603
- coreL: [0.49, 0.47, 0.45],
604
- edgeL: [0.65, 0.68, 0.71, 0.75],
605
- redHuesWarmBrand: [28, 24, 33.3, 38],
606
- redHuesMagentaBrand: [33.3, 38, 28],
607
- redHueMagentaDh: -15
600
+ // the red complement's hue sets (resolve.ts redComplementVariant): the canonical hue and
601
+ // the two ends of the error band the field ships, read through this engine (the cool end
602
+ // near Primer's danger, the gold side near USWDS's error). The set furthest from the brand
603
+ // stamp by the side-by-side measure ships.
604
+ setHues: { true: 33.3, cool: 24, "gold-side": 38 }
608
605
  };
609
606
  function redSolveDist(c, red) {
610
607
  const dh = (c.H - red.H + 540) % 360 - 180;
@@ -3112,63 +3109,61 @@ function hueCollisionPending(scale, sigScales) {
3112
3109
  }
3113
3110
  var blackLcAt = (L, C2, H2) => Math.abs(apcaLc(apcaYAt(0, 0, 0), apcaYAt(L, clampChromaToGamut(L, C2, H2), H2)));
3114
3111
  var redCleanBeside = (brandCta, c) => p2Diff(brandCta, c) >= (c.L < brandCta.L ? P2_D : P2_D_UP) && redSolveDist(brandCta, c) >= RED_GATE.G + RED_SOLVE.ring;
3115
- function redComplementVariant(brandCta, seed, brandWentUp, red, contrastProfile) {
3116
- const rctx = buildContext(red.def.hex, {
3117
- darkChromaCurve,
3118
- darkCtaC: "signal",
3119
- darkFillMinL: red.def.darkFillMinL,
3120
- enforceOnFillContrast: true,
3121
- suppressRedCool: true,
3122
- goldBoost: true,
3123
- signalWarmDrift: true,
3124
- contrastProfile
3125
- });
3112
+ var redSetsCache = /* @__PURE__ */ new Map();
3113
+ var redSetKey = (set, band) => `${set}:${band}`;
3114
+ var bandBelow = (band) => {
3115
+ const i = ARCHETYPES.findIndex((a) => a.name === band);
3116
+ return ARCHETYPES[Math.max(0, i - 1)].name;
3117
+ };
3118
+ function redSetsFor(red, contrastProfile) {
3119
+ const cacheKey = contrastProfile ?? "wcag";
3120
+ const hit = redSetsCache.get(cacheKey);
3121
+ if (hit) return hit;
3122
+ const own = classifyArchetype(red.scale.cta.L);
3123
+ const sets = /* @__PURE__ */ new Map();
3124
+ for (const [set, hue] of Object.entries(RED_SOLVE.setHues)) for (const band of [own, bandBelow(own)]) {
3125
+ sets.set(redSetKey(set, band), generateScale(red.def.hex, red.def.name, band === own ? void 0 : band, {
3126
+ hueShiftDeg: hue - red.def.H,
3127
+ darkChromaCurve,
3128
+ darkCtaC: "signal",
3129
+ darkFillMinL: red.def.darkFillMinL,
3130
+ enforceOnFillContrast: true,
3131
+ suppressRedCool: true,
3132
+ goldBoost: true,
3133
+ signalWarmDrift: true,
3134
+ contrastProfile,
3135
+ apcaClearance: true,
3136
+ apcaClearanceLc: CRITICAL_CLEARANCE_LC
3137
+ }));
3138
+ }
3139
+ redSetsCache.set(cacheKey, sets);
3140
+ return sets;
3141
+ }
3142
+ function redComplementVariant(brandCta, brandWentUp, red, contrastProfile) {
3126
3143
  const redCta = red.scale.cta;
3127
- const at = (L, H2) => ({ L, C: clampChromaToGamut(L, rctx.cAt("light", L, rctx.brandC), H2), H: H2 });
3128
3144
  const fireLc = CRITICAL_CLEARANCE_LC + APCA_ENFORCE_MARGIN_LC;
3129
- const shipsWhite = (c) => onFillIsWhiteDarkAt(c.L, c.C, c.H, true, contrastProfile === "apca" ? void 0 : 4.5);
3130
3145
  const poleOk = (c) => {
3131
- const white = shipsWhite(c);
3146
+ const white = onFillIsWhiteDarkAt(c.L, c.C, c.H, true, contrastProfile === "apca" ? void 0 : 4.5);
3132
3147
  const lc = white ? whiteTextLcAt(c.L, c.C, c.H) : blackLcAt(c.L, c.C, c.H);
3133
3148
  const legal = contrastProfile === "apca" || legalRatio(c.L, c.C, c.H, white ? 1 : 0) >= 4.5;
3134
3149
  return legal && lc >= fireLc;
3135
3150
  };
3136
- const slideToBar = (c) => {
3137
- if (poleOk(c)) return c;
3138
- const L2 = shipsWhite(c) ? findLForWhiteTextLc(c.L, c.C, c.H, fireLc + APCA_SOLVE_MARGIN_LC) : findLForBlackTextLc(c.L, c.C, c.H, fireLc + APCA_SOLVE_MARGIN_LC, 0.92);
3139
- const c2 = at(L2, c.H);
3140
- return poleOk(c2) ? c2 : null;
3141
- };
3142
- const clean = (c) => redCleanBeside(brandCta, c) && poleOk(c);
3143
- if (!brandWentUp && clean(redCta)) return null;
3144
- const hues = hueDelta(seed.H, redCta.H) <= RED_SOLVE.redHueMagentaDh ? RED_SOLVE.redHuesMagentaBrand : RED_SOLVE.redHuesWarmBrand;
3145
- const wantLighter = brandCta.L <= redCta.L;
3146
- const tiers = wantLighter ? [[...RED_SOLVE.edgeL], RED_SOLVE.coreL.filter((l) => l > brandCta.L)] : [[...RED_SOLVE.coreL], [...RED_SOLVE.edgeL]];
3151
+ if (!brandWentUp && redCleanBeside(brandCta, redCta) && poleOk(redCta)) return null;
3152
+ const own = classifyArchetype(redCta.L);
3153
+ const band = classifyArchetype(brandCta.L) === own ? bandBelow(own) : own;
3154
+ const sets = redSetsFor(red, contrastProfile);
3147
3155
  let pick = null;
3148
- outer: for (const tier of tiers) {
3149
- const ls = [...tier].sort((a, b) => wantLighter ? a - b : b - a);
3150
- for (const L of ls) for (const H2 of hues) {
3151
- const c = slideToBar(at(L, H2));
3152
- if (!c) continue;
3153
- const onSide = wantLighter ? c.L > brandCta.L : c.L < brandCta.L;
3154
- if (!onSide || !clean(c)) continue;
3155
- pick = c;
3156
- break outer;
3157
- }
3158
- }
3159
- if (!pick) return null;
3160
- const cta = makeStop(redCta.stop, pick.L, rctx.cAt("light", pick.L, rctx.brandC), pick.H);
3161
- const vCFor = (L) => rctx.cAt("light", L, rctx.brandC);
3162
- const hL = stateFillL(pick.L, "light", 1);
3163
- const ctaHover = makeStop(red.scale.ctaHover.stop, hL, vCFor(hL), pick.H);
3164
- const pL = stateFillL(pick.L, "light", 2);
3165
- const ctaPressed = makeStop(red.scale.ctaPressed.stop, pL, vCFor(pL), pick.H);
3166
- const onFillTextIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, true, contrastProfile === "apca" ? void 0 : 4.5);
3156
+ for (const set of Object.keys(RED_SOLVE.setHues)) {
3157
+ const scale = sets.get(redSetKey(set, band));
3158
+ const d = p2Diff(brandCta, scale.cta);
3159
+ if (!pick || d > pick.d) pick = { set, scale, d };
3160
+ }
3161
+ const { cta, ctaHover, ctaPressed, onFillTextIsWhite } = pick.scale;
3167
3162
  return {
3168
3163
  scale: { ...red.scale, cta, ctaHover, ctaPressed, onFillTextIsWhite },
3169
- // naming candidates only — the identity name is the owner's call at bless. No hue suffix:
3170
- // the plugin's note parser mints Figma variable paths from this string.
3171
- note: `red \u2192 ${pick.L < redCta.L ? "rich" : "coral"} L${pick.L.toFixed(2)}`
3164
+ // the community plugin shows the part after the arrow as a display label; the hex is
3165
+ // the variant's identity there, so the note carries no value
3166
+ note: `red \u2192 ${pick.set} at ${band}`
3172
3167
  };
3173
3168
  }
3174
3169
  function resolveBrand(hex, name, opts) {
@@ -3214,7 +3209,7 @@ function resolveBrand(hex, name, opts) {
3214
3209
  const signalOverrides = [];
3215
3210
  if (solving || opts?.exact && signalMoves && !opts?.archetypeOverride) {
3216
3211
  const brandWentUp = !!scale.ctaRepelled?.light && scale.cta.L > seedO.L + 1e-6;
3217
- const v2 = redComplementVariant(scale.cta, seedO, brandWentUp, sigScales.get("red"), opts?.contrastProfile);
3212
+ const v2 = redComplementVariant(scale.cta, brandWentUp, sigScales.get("red"), opts?.contrastProfile);
3218
3213
  if (v2) signalOverrides.push({ name: "red", scale: v2.scale, note: v2.note });
3219
3214
  }
3220
3215
  let warnVariant = null;
@@ -3376,7 +3371,7 @@ function resolveTheme(input) {
3376
3371
  const red = sigScales.get("red");
3377
3372
  const primaryRed = mergedOverrides.find((o) => o.name === "red");
3378
3373
  if (!primaryRed) {
3379
- const v2 = redComplementVariant(secScale.cta, hexToOklch(secSeedHex), false, red, cp2);
3374
+ const v2 = redComplementVariant(secScale.cta, false, red, cp2);
3380
3375
  if (v2 && redCleanBeside(primary.scale.cta, v2.scale.cta)) adopt("red", v2.scale, `${v2.note} (for the secondary)`);
3381
3376
  }
3382
3377
  for (const sigName of ["yellow", "green", "blue"]) {
@@ -504,14 +504,11 @@ var RED_SOLVE = {
504
504
  brickCoolGold: -3,
505
505
  brickDesat: 0.85,
506
506
  brickExtraDeep: 0.02,
507
- // red's usable zones (her density map, error-range-checks.json): deep core or the light
508
- // edge tier — the .50-.58 middle is ring territory, canonical itself lives there, so a
509
- // lightened brand ALWAYS gets a deep-core red. Cool-first beside warm brands.
510
- coreL: [0.49, 0.47, 0.45],
511
- edgeL: [0.65, 0.68, 0.71, 0.75],
512
- redHuesWarmBrand: [28, 24, 33.3, 38],
513
- redHuesMagentaBrand: [33.3, 38, 28],
514
- redHueMagentaDh: -15
507
+ // the red complement's hue sets (resolve.ts redComplementVariant): the canonical hue and
508
+ // the two ends of the error band the field ships, read through this engine (the cool end
509
+ // near Primer's danger, the gold side near USWDS's error). The set furthest from the brand
510
+ // stamp by the side-by-side measure ships.
511
+ setHues: { true: 33.3, cool: 24, "gold-side": 38 }
515
512
  };
516
513
  function redSolveDist(c, red) {
517
514
  const dh = (c.H - red.H + 540) % 360 - 180;
@@ -3019,63 +3016,61 @@ function hueCollisionPending(scale, sigScales) {
3019
3016
  }
3020
3017
  var blackLcAt = (L, C2, H2) => Math.abs(apcaLc(apcaYAt(0, 0, 0), apcaYAt(L, clampChromaToGamut(L, C2, H2), H2)));
3021
3018
  var redCleanBeside = (brandCta, c) => p2Diff(brandCta, c) >= (c.L < brandCta.L ? P2_D : P2_D_UP) && redSolveDist(brandCta, c) >= RED_GATE.G + RED_SOLVE.ring;
3022
- function redComplementVariant(brandCta, seed, brandWentUp, red, contrastProfile) {
3023
- const rctx = buildContext(red.def.hex, {
3024
- darkChromaCurve,
3025
- darkCtaC: "signal",
3026
- darkFillMinL: red.def.darkFillMinL,
3027
- enforceOnFillContrast: true,
3028
- suppressRedCool: true,
3029
- goldBoost: true,
3030
- signalWarmDrift: true,
3031
- contrastProfile
3032
- });
3019
+ var redSetsCache = /* @__PURE__ */ new Map();
3020
+ var redSetKey = (set, band) => `${set}:${band}`;
3021
+ var bandBelow = (band) => {
3022
+ const i = ARCHETYPES.findIndex((a) => a.name === band);
3023
+ return ARCHETYPES[Math.max(0, i - 1)].name;
3024
+ };
3025
+ function redSetsFor(red, contrastProfile) {
3026
+ const cacheKey = contrastProfile ?? "wcag";
3027
+ const hit = redSetsCache.get(cacheKey);
3028
+ if (hit) return hit;
3029
+ const own = classifyArchetype(red.scale.cta.L);
3030
+ const sets = /* @__PURE__ */ new Map();
3031
+ for (const [set, hue] of Object.entries(RED_SOLVE.setHues)) for (const band of [own, bandBelow(own)]) {
3032
+ sets.set(redSetKey(set, band), generateScale(red.def.hex, red.def.name, band === own ? void 0 : band, {
3033
+ hueShiftDeg: hue - red.def.H,
3034
+ darkChromaCurve,
3035
+ darkCtaC: "signal",
3036
+ darkFillMinL: red.def.darkFillMinL,
3037
+ enforceOnFillContrast: true,
3038
+ suppressRedCool: true,
3039
+ goldBoost: true,
3040
+ signalWarmDrift: true,
3041
+ contrastProfile,
3042
+ apcaClearance: true,
3043
+ apcaClearanceLc: CRITICAL_CLEARANCE_LC
3044
+ }));
3045
+ }
3046
+ redSetsCache.set(cacheKey, sets);
3047
+ return sets;
3048
+ }
3049
+ function redComplementVariant(brandCta, brandWentUp, red, contrastProfile) {
3033
3050
  const redCta = red.scale.cta;
3034
- const at = (L, H2) => ({ L, C: clampChromaToGamut(L, rctx.cAt("light", L, rctx.brandC), H2), H: H2 });
3035
3051
  const fireLc = CRITICAL_CLEARANCE_LC + APCA_ENFORCE_MARGIN_LC;
3036
- const shipsWhite = (c) => onFillIsWhiteDarkAt(c.L, c.C, c.H, true, contrastProfile === "apca" ? void 0 : 4.5);
3037
3052
  const poleOk = (c) => {
3038
- const white = shipsWhite(c);
3053
+ const white = onFillIsWhiteDarkAt(c.L, c.C, c.H, true, contrastProfile === "apca" ? void 0 : 4.5);
3039
3054
  const lc = white ? whiteTextLcAt(c.L, c.C, c.H) : blackLcAt(c.L, c.C, c.H);
3040
3055
  const legal = contrastProfile === "apca" || legalRatio(c.L, c.C, c.H, white ? 1 : 0) >= 4.5;
3041
3056
  return legal && lc >= fireLc;
3042
3057
  };
3043
- const slideToBar = (c) => {
3044
- if (poleOk(c)) return c;
3045
- const L2 = shipsWhite(c) ? findLForWhiteTextLc(c.L, c.C, c.H, fireLc + APCA_SOLVE_MARGIN_LC) : findLForBlackTextLc(c.L, c.C, c.H, fireLc + APCA_SOLVE_MARGIN_LC, 0.92);
3046
- const c2 = at(L2, c.H);
3047
- return poleOk(c2) ? c2 : null;
3048
- };
3049
- const clean = (c) => redCleanBeside(brandCta, c) && poleOk(c);
3050
- if (!brandWentUp && clean(redCta)) return null;
3051
- const hues = hueDelta(seed.H, redCta.H) <= RED_SOLVE.redHueMagentaDh ? RED_SOLVE.redHuesMagentaBrand : RED_SOLVE.redHuesWarmBrand;
3052
- const wantLighter = brandCta.L <= redCta.L;
3053
- const tiers = wantLighter ? [[...RED_SOLVE.edgeL], RED_SOLVE.coreL.filter((l) => l > brandCta.L)] : [[...RED_SOLVE.coreL], [...RED_SOLVE.edgeL]];
3058
+ if (!brandWentUp && redCleanBeside(brandCta, redCta) && poleOk(redCta)) return null;
3059
+ const own = classifyArchetype(redCta.L);
3060
+ const band = classifyArchetype(brandCta.L) === own ? bandBelow(own) : own;
3061
+ const sets = redSetsFor(red, contrastProfile);
3054
3062
  let pick = null;
3055
- outer: for (const tier of tiers) {
3056
- const ls = [...tier].sort((a, b) => wantLighter ? a - b : b - a);
3057
- for (const L of ls) for (const H2 of hues) {
3058
- const c = slideToBar(at(L, H2));
3059
- if (!c) continue;
3060
- const onSide = wantLighter ? c.L > brandCta.L : c.L < brandCta.L;
3061
- if (!onSide || !clean(c)) continue;
3062
- pick = c;
3063
- break outer;
3064
- }
3065
- }
3066
- if (!pick) return null;
3067
- const cta = makeStop(redCta.stop, pick.L, rctx.cAt("light", pick.L, rctx.brandC), pick.H);
3068
- const vCFor = (L) => rctx.cAt("light", L, rctx.brandC);
3069
- const hL = stateFillL(pick.L, "light", 1);
3070
- const ctaHover = makeStop(red.scale.ctaHover.stop, hL, vCFor(hL), pick.H);
3071
- const pL = stateFillL(pick.L, "light", 2);
3072
- const ctaPressed = makeStop(red.scale.ctaPressed.stop, pL, vCFor(pL), pick.H);
3073
- const onFillTextIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, true, contrastProfile === "apca" ? void 0 : 4.5);
3063
+ for (const set of Object.keys(RED_SOLVE.setHues)) {
3064
+ const scale = sets.get(redSetKey(set, band));
3065
+ const d = p2Diff(brandCta, scale.cta);
3066
+ if (!pick || d > pick.d) pick = { set, scale, d };
3067
+ }
3068
+ const { cta, ctaHover, ctaPressed, onFillTextIsWhite } = pick.scale;
3074
3069
  return {
3075
3070
  scale: { ...red.scale, cta, ctaHover, ctaPressed, onFillTextIsWhite },
3076
- // naming candidates only — the identity name is the owner's call at bless. No hue suffix:
3077
- // the plugin's note parser mints Figma variable paths from this string.
3078
- note: `red \u2192 ${pick.L < redCta.L ? "rich" : "coral"} L${pick.L.toFixed(2)}`
3071
+ // the community plugin shows the part after the arrow as a display label; the hex is
3072
+ // the variant's identity there, so the note carries no value
3073
+ note: `red \u2192 ${pick.set} at ${band}`
3079
3074
  };
3080
3075
  }
3081
3076
  function resolveBrand(hex, name, opts) {
@@ -3121,7 +3116,7 @@ function resolveBrand(hex, name, opts) {
3121
3116
  const signalOverrides = [];
3122
3117
  if (solving || opts?.exact && signalMoves && !opts?.archetypeOverride) {
3123
3118
  const brandWentUp = !!scale.ctaRepelled?.light && scale.cta.L > seedO.L + 1e-6;
3124
- const v2 = redComplementVariant(scale.cta, seedO, brandWentUp, sigScales.get("red"), opts?.contrastProfile);
3119
+ const v2 = redComplementVariant(scale.cta, brandWentUp, sigScales.get("red"), opts?.contrastProfile);
3125
3120
  if (v2) signalOverrides.push({ name: "red", scale: v2.scale, note: v2.note });
3126
3121
  }
3127
3122
  let warnVariant = null;
@@ -3283,7 +3278,7 @@ function resolveTheme(input) {
3283
3278
  const red = sigScales.get("red");
3284
3279
  const primaryRed = mergedOverrides.find((o) => o.name === "red");
3285
3280
  if (!primaryRed) {
3286
- const v2 = redComplementVariant(secScale.cta, hexToOklch(secSeedHex), false, red, cp2);
3281
+ const v2 = redComplementVariant(secScale.cta, false, red, cp2);
3287
3282
  if (v2 && redCleanBeside(primary.scale.cta, v2.scale.cta)) adopt("red", v2.scale, `${v2.note} (for the secondary)`);
3288
3283
  }
3289
3284
  for (const sigName of ["yellow", "green", "blue"]) {
@@ -66,11 +66,11 @@ export declare const RED_SOLVE: {
66
66
  readonly brickCoolGold: -3;
67
67
  readonly brickDesat: 0.85;
68
68
  readonly brickExtraDeep: 0.02;
69
- readonly coreL: readonly [0.49, 0.47, 0.45];
70
- readonly edgeL: readonly [0.65, 0.68, 0.71, 0.75];
71
- readonly redHuesWarmBrand: readonly [28, 24, 33.3, 38];
72
- readonly redHuesMagentaBrand: readonly [33.3, 38, 28];
73
- readonly redHueMagentaDh: -15;
69
+ readonly setHues: {
70
+ readonly true: 33.3;
71
+ readonly cool: 24;
72
+ readonly 'gold-side': 38;
73
+ };
74
74
  };
75
75
  export declare function redSolveDist(c: {
76
76
  L: number;
@@ -1,6 +1,5 @@
1
1
  export declare const P2_D = 0.12;
2
2
  export declare const P2_D_UP = 0.11;
3
- export declare const P2_D_FALLBACK = 0.115;
4
3
  export declare function p2Diff(a: {
5
4
  L: number;
6
5
  C: number;
@@ -1,5 +1,5 @@
1
1
  import { type GeneratedScale, type ContrastProfile, type ColorStop } from './colorEngine';
2
- import type { Archetype } from './archetypes';
2
+ import { type Archetype } from './archetypes';
3
3
  import { type SignalDef } from './signals';
4
4
  type SignalScales = Map<SignalDef['name'], {
5
5
  def: SignalDef;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okchroma",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "license": "MIT",
5
5
  "description": "Color-system engine: one brand hex in, a light and dark set of color primitives out, with the contrast requirements solved during generation; emitted as CSS custom properties, DTCG documents, and Figma variables",
6
6
  "repository": {