okchroma 0.8.2 → 0.9.1

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.
@@ -21,13 +21,13 @@ function stateStepL(restL, _mode, k) {
21
21
  }
22
22
  var OVERRIDE_LIGHT_BELOW = ARCHETYPES.find((a) => a.name === "rich").min;
23
23
  var OVERRIDE_DARK_ABOVE = ARCHETYPES.find((a) => a.name === "light").min;
24
- function stateFillL(restL, mode, k) {
24
+ function stateFillL(restL, mode, k, keepsPole) {
25
25
  const natural = mode === "dark" ? 1 : -1;
26
26
  const overridden = mode === "light" ? restL < OVERRIDE_LIGHT_BELOW : restL > OVERRIDE_DARK_ABOVE;
27
27
  const preferred = overridden ? -natural : natural;
28
28
  const fits = (d) => {
29
29
  const p = restL + d * stateStepL(restL, mode, 2);
30
- return p >= STATE_L_MIN && p <= STATE_L_MAX;
30
+ return p >= STATE_L_MIN && p <= STATE_L_MAX && (keepsPole?.(p) ?? true);
31
31
  };
32
32
  const dir = fits(preferred) ? preferred : fits(-preferred) ? -preferred : preferred;
33
33
  const L = restL + dir * stateStepL(restL, mode, k);
@@ -745,32 +745,20 @@ function stopTokenName(stop) {
745
745
  }
746
746
  var PAPER_0 = "paper-0";
747
747
  var PEN_100 = "pen-100";
748
- var STAMP_FILL = "stamp-fill";
749
- var STAMP_FILL_HOVER = "stamp-fill-hover";
750
- var STAMP_FILL_PRESSED = "stamp-fill-pressed";
748
+ var STAMP_FILL = "stamp-0";
749
+ var STAMP_FILL_HOVER = "stamp-1";
750
+ var STAMP_FILL_PRESSED = "stamp-2";
751
751
  var STAMP_EDGE = "stamp-edge";
752
752
  var STAMP_ON = "stamp-on";
753
- var STAMP_STATE_LEAVES = {
754
- [STAMP_FILL]: "stamp/fill",
755
- [STAMP_FILL_HOVER]: "stamp/fill-hover",
756
- [STAMP_FILL_PRESSED]: "stamp/fill-pressed",
757
- [STAMP_EDGE]: "stamp/edge",
758
- [STAMP_ON]: "stamp/on"
759
- };
760
- var STAMP_LEAF = {
761
- FILL: STAMP_STATE_LEAVES[STAMP_FILL],
762
- FILL_HOVER: STAMP_STATE_LEAVES[STAMP_FILL_HOVER],
763
- FILL_PRESSED: STAMP_STATE_LEAVES[STAMP_FILL_PRESSED],
764
- EDGE: STAMP_STATE_LEAVES[STAMP_EDGE],
765
- ON: STAMP_STATE_LEAVES[STAMP_ON]
766
- };
767
753
  var cssVarName = (path) => `--${path.join("-")}`;
768
754
  var figmaPathOf = (path) => path.join("/");
769
- var familyPath = (family, leaf) => [family, ...(STAMP_STATE_LEAVES[leaf] ?? leaf).split("/")];
755
+ var familyPath = (family, leaf) => [family, leaf];
770
756
  var LINK_GROUP = "link";
771
757
  var LINK_POSTURES = ["default", "inverse"];
772
758
  var LINK_STATES = ["enabled", "hover", "pressed"];
773
- var linkPath = (posture, state) => [LINK_GROUP, posture, state];
759
+ var LINK_STEP = { enabled: 0, hover: 1, pressed: 2 };
760
+ var linkLeaf = (posture, state) => `${posture}-${LINK_STEP[state]}`;
761
+ var linkPath = (posture, state) => [LINK_GROUP, linkLeaf(posture, state)];
774
762
  var ABSOLUTE_GROUP = "absolute";
775
763
  var absolutePath = (family) => [ABSOLUTE_GROUP, family];
776
764
  var COLOR_GROUP = "color";
@@ -2143,6 +2131,20 @@ function ctaDarkDualGateL(base, darkH, onFillTextIsWhite, enforce, coLc, capLoL,
2143
2131
  const lLc = findLForBlackTextLc(startL, C2, darkH, coLc + APCA_SOLVE_MARGIN_LC, capHiL);
2144
2132
  return Math.max(l45, Math.min(lLc, capHiL));
2145
2133
  }
2134
+ function stepKeepsPole(mode, white, chromaAt, H2, law) {
2135
+ if (mode === "light") return void 0;
2136
+ if (law.enforceLc !== void 0) {
2137
+ const bar = law.enforceLc;
2138
+ const lcAt = white ? whiteTextLcAt : blackTextLcAt;
2139
+ return (L) => lcAt(L, chromaAt(L), H2) >= bar;
2140
+ }
2141
+ if (law.ratioFloor === void 0) return void 0;
2142
+ const floor = law.ratioFloor, poleY = white ? 1 : 0;
2143
+ return (L) => {
2144
+ const C2 = clampChromaToGamut(L, chromaAt(L), H2);
2145
+ return Math.min(legalRatio(L, C2, H2, poleY), contrastRatio(shippedY(L, C2, H2), poleY)) >= floor;
2146
+ };
2147
+ }
2146
2148
  function onFillIsWhiteDarkAt(L, C2, H2, enforce, ratioFloor) {
2147
2149
  return onTextIsWhite(apcaYAt(L, C2, H2), L, C2, H2, enforce, ratioFloor);
2148
2150
  }
@@ -2425,7 +2427,8 @@ function resolveRamp(hex, mode, spec, opts) {
2425
2427
  const enforceLc = spec.ons.onFill.enforceLc;
2426
2428
  const coLc = ctx.opts?.apcaClearance && enforceLc === void 0 ? ctx.opts.apcaClearanceLc ?? spec.ons.onFill.coEnforceLc : void 0;
2427
2429
  const onFloor = enforceLc !== void 0 ? void 0 : spec.ons.onFill.ratioFloor;
2428
- let cta, ctaHover, ctaPressed, onFillIsWhite;
2430
+ let cta, onFillIsWhite;
2431
+ let stateC, stateH;
2429
2432
  if (mode === "light") {
2430
2433
  onFillIsWhite = onFillIsWhiteLight(ctx, enforceLc !== void 0 ? false : onFillEnforce);
2431
2434
  let light9L = enforceLc !== void 0 ? ctaLightLApca(ctx, onFillIsWhite, onFillEnforce, enforceLc) : ctaLightL(ctx, onFillIsWhite, onFillEnforce);
@@ -2457,21 +2460,14 @@ function resolveRamp(hex, mode, spec, opts) {
2457
2460
  }
2458
2461
  }
2459
2462
  cta = emitRole(STAMP_FILL, light9L, ctaCFor(light9L) * ctaCMul, ctaH);
2460
- const hCFor = (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
2461
- const pCFor = (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
2462
- const hL = stateFillL(light9L, "light", 1);
2463
- const pL = stateFillL(light9L, "light", 2);
2464
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, hCFor(hL), ctaH);
2465
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, pCFor(pL), ctaH);
2466
- if (light9L !== ctx.scaleL) {
2467
- cta.enforced = true;
2468
- ctaHover.enforced = true;
2469
- ctaPressed.enforced = true;
2470
- }
2463
+ stateC = {
2464
+ hover: (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul,
2465
+ pressed: (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul
2466
+ };
2467
+ stateH = ctaH;
2468
+ if (light9L !== ctx.scaleL) cta.enforced = true;
2471
2469
  if (repelled) {
2472
2470
  cta.repelled = true;
2473
- ctaHover.repelled = true;
2474
- ctaPressed.repelled = true;
2475
2471
  onFillIsWhite = onFillIsWhiteDarkAt(
2476
2472
  cta.L,
2477
2473
  cta.C,
@@ -2484,50 +2480,29 @@ function resolveRamp(hex, mode, spec, opts) {
2484
2480
  const d = dctx;
2485
2481
  const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
2486
2482
  const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
2487
- const dTrio = (baseL) => [stateFillL(baseL, "dark", 1), stateFillL(baseL, "dark", 2)];
2483
+ stateC = { hover: dCFor, pressed: dCFor };
2484
+ stateH = ctx.darkCtaH;
2488
2485
  cta = emitRole(STAMP_FILL, cta9L, dCFor(cta9L), ctx.darkCtaH);
2489
- {
2490
- const [hL, pL] = dTrio(cta9L);
2491
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2492
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2493
- }
2494
2486
  onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
2495
2487
  const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
2496
2488
  if (enforcedL !== null) {
2497
2489
  cta = emitRole(STAMP_FILL, enforcedL, dCFor(enforcedL), ctx.darkCtaH);
2498
- const [hL, pL] = dTrio(enforcedL);
2499
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2500
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2501
2490
  cta.enforced = true;
2502
- ctaHover.enforced = true;
2503
- ctaPressed.enforced = true;
2504
2491
  }
2505
2492
  if (coLc !== void 0) {
2506
2493
  const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
2507
2494
  const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
2508
2495
  if (Math.abs(clearedL - cta.L) > 1e-9) {
2509
2496
  cta = emitRole(STAMP_FILL, clearedL, dCFor(clearedL), ctx.darkCtaH);
2510
- const [hL, pL] = dTrio(clearedL);
2511
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2512
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2513
2497
  cta.enforced = true;
2514
- ctaHover.enforced = true;
2515
- ctaPressed.enforced = true;
2516
2498
  }
2517
2499
  }
2518
2500
  if (ctx.opts?.ctaSolve) {
2519
2501
  const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
2520
2502
  if (exitL !== null) {
2521
2503
  cta = emitRole(STAMP_FILL, exitL, dCFor(exitL), ctx.darkCtaH);
2522
- const [hL, pL] = dTrio(exitL);
2523
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2524
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2525
2504
  cta.enforced = true;
2526
- ctaHover.enforced = true;
2527
- ctaPressed.enforced = true;
2528
2505
  cta.repelled = true;
2529
- ctaHover.repelled = true;
2530
- ctaPressed.repelled = true;
2531
2506
  onFillIsWhite = onFillIsWhiteDarkAt(
2532
2507
  cta.L,
2533
2508
  cta.C,
@@ -2543,6 +2518,19 @@ function resolveRamp(hex, mode, spec, opts) {
2543
2518
  const other = legalRatio(cta.L, cta.C, cta.H, onFillIsWhite ? 0 : 1);
2544
2519
  if (chosen < onFloor && other >= onFloor) onFillIsWhite = !onFillIsWhite;
2545
2520
  }
2521
+ const keepsPole = stepKeepsPole(mode, onFillIsWhite, stateC.pressed, stateH, { ratioFloor: onFloor, enforceLc });
2522
+ const hL = stateFillL(cta.L, mode, 1, keepsPole);
2523
+ const pL = stateFillL(cta.L, mode, 2, keepsPole);
2524
+ const ctaHover = emitRole(STAMP_FILL_HOVER, hL, stateC.hover(hL), stateH);
2525
+ const ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, stateC.pressed(pL), stateH);
2526
+ if (cta.enforced) {
2527
+ ctaHover.enforced = true;
2528
+ ctaPressed.enforced = true;
2529
+ }
2530
+ if (cta.repelled) {
2531
+ ctaHover.repelled = true;
2532
+ ctaPressed.repelled = true;
2533
+ }
2546
2534
  return { mode, seed, stops, roles: { cta, ctaHover, ctaPressed }, ons: { onFillIsWhite } };
2547
2535
  }
2548
2536
 
@@ -2625,18 +2613,18 @@ function generateNeutralScale(brandH, level = "default", contrastProfile) {
2625
2613
  if (popLift > 1e-6) {
2626
2614
  scale.ctaDark = makeStop(9, scale.dark[3].L + popLift, scale.dark[3].C, scale.dark[3].H);
2627
2615
  }
2628
- const nState = (stop, mode, rest, k) => {
2629
- const L = stateFillL(rest.L, mode, k);
2630
- return makeStop(stop, L, curve(L, mode), rest.H);
2631
- };
2632
- scale.ctaHover = nState(10, "light", scale.cta, 1);
2633
- scale.ctaPressed = nState(11, "light", scale.cta, 2);
2634
- scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1);
2635
- scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2);
2636
2616
  const onEnforce = contrastProfile !== "apca";
2637
2617
  const onFloor = 4.5;
2638
2618
  scale.onFillTextIsWhite = onTextIsWhite(apcaY(scale.cta.r, scale.cta.g, scale.cta.b), scale.cta.L, scale.cta.C, scale.cta.H, onEnforce, onFloor);
2639
2619
  scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
2620
+ const nState = (stop, mode, rest, k, white) => {
2621
+ const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
2622
+ return makeStop(stop, L, curve(L, mode), rest.H);
2623
+ };
2624
+ scale.ctaHover = nState(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
2625
+ scale.ctaPressed = nState(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
2626
+ scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
2627
+ scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
2640
2628
  return scale;
2641
2629
  }
2642
2630
  function generateSubtleSecondary(hex, opts) {
@@ -2649,10 +2637,6 @@ function generateSubtleSecondary(hex, opts) {
2649
2637
  });
2650
2638
  const asCta = (stop, src) => makeStop(stop, src.L, src.C, src.H);
2651
2639
  const mk = (stop, L, mode) => makeStop(stop, L, curve(L, mode), scale.brandH);
2652
- const st = (stop, mode, rest, k) => {
2653
- const L = stateFillL(rest.L, mode, k);
2654
- return makeStop(stop, L, curve(L, mode), rest.H);
2655
- };
2656
2640
  if (opts?.ctaL) {
2657
2641
  scale.cta = mk(9, opts.ctaL.light, "light");
2658
2642
  scale.ctaDark = mk(9, opts.ctaL.dark, "dark");
@@ -2660,14 +2644,18 @@ function generateSubtleSecondary(hex, opts) {
2660
2644
  scale.cta = asCta(9, scale.light[3]);
2661
2645
  scale.ctaDark = asCta(9, scale.dark[3]);
2662
2646
  }
2663
- scale.ctaHover = st(10, "light", scale.cta, 1);
2664
- scale.ctaPressed = st(11, "light", scale.cta, 2);
2665
- scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1);
2666
- scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2);
2667
2647
  const onEnforce = opts?.contrastProfile !== "apca";
2668
2648
  const onFloor = 4.5;
2669
2649
  scale.onFillTextIsWhite = onTextIsWhite(apcaY(scale.cta.r, scale.cta.g, scale.cta.b), scale.cta.L, scale.cta.C, scale.cta.H, onEnforce, onFloor);
2670
2650
  scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
2651
+ const st = (stop, mode, rest, k, white) => {
2652
+ const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
2653
+ return makeStop(stop, L, curve(L, mode), rest.H);
2654
+ };
2655
+ scale.ctaHover = st(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
2656
+ scale.ctaPressed = st(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
2657
+ scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
2658
+ scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
2671
2659
  return scale;
2672
2660
  }
2673
2661
 
@@ -2761,20 +2749,21 @@ var SCALE = {
2761
2749
  // ("heavy-emphasis": "high" and "low" are plane words in the community plugin and
2762
2750
  // "strong" carries "on")
2763
2751
  "pen-70": { req: "heavy-emphasis text, inverted backgrounds", contrast: AA_BODY, claim: claimOf("pen"), theming: solved, collides: true },
2764
- // the stamp family. "CTA" stays in these bodies on purpose: it is not a token label, so
2752
+ // the stamp rows, each a position: the fill at rest and at one and two steps, the edge,
2753
+ // the on-text. No body names a state; a consumer's semantic layer names the states and
2754
+ // aliases the steps. "CTA" stays in these bodies on purpose: it is not a token label, so
2765
2755
  // it floods nothing, and a designer's "cta" query lands on these rows.
2766
2756
  // the usage lines are the owner's guidance, for the document rendering: the fill is
2767
2757
  // misused as a standalone color in the wild
2768
- "stamp/fill": {
2769
- req: "CTAs",
2758
+ "stamp-0": {
2759
+ req: "CTAs, the fill at rest",
2770
2760
  theming: (f) => `fully re-solved per theme and family${COLOR_WORD[f]}`,
2771
- use: "primary button fills; optional for avatars, badges and other elements that carry text. Not a standalone color: it has no contrast guarantee of its own, only its on-text has one. Use it alone or with its hover and pressed states. Always bordered with its edge"
2761
+ use: "primary button fills; optional for avatars, badges and other elements that carry text. Not a standalone color: it has no contrast guarantee of its own, only its on-text has one. Use it alone or with the two steps above it. Always bordered with its edge"
2772
2762
  },
2773
- "stamp/fill-hover": { req: "CTA pointer-over state", theming: (f) => `follows its rest fill${COLOR_WORD[f]}`, use: "the pointer-over state of the fill, with the fill; for nothing else. Always bordered with the edge" },
2774
- "stamp/fill-pressed": { req: "CTA pressed state", theming: (f) => `follows its rest fill${COLOR_WORD[f]}`, use: "the pressed state of the fill, with the fill; for nothing else. Always bordered with the edge" },
2775
- "stamp/edge": { req: "min APCA visibility", theming: (f) => `draws for CTAs that sit close to the page; strength per family tier${COLOR_WORD[f]}`, use: "the border of the fill and its states, always rendered with them and never without them (it resolves to transparent unless the fill sits close to the page); for nothing else" },
2776
- // ("fill" is a label word and foreign here)
2777
- "stamp/on": { req: "text over the CTA color", contrast: `${AA_BODY} over its CTA`, theming: (f) => `whichever pole passes; quiet CTAs take the soft pole${COLOR_WORD[f]}`, use: "the only text color over the fill; never elsewhere" }
2763
+ "stamp-1": { req: "CTAs, the fill one step from rest", theming: (f) => `follows the fill at rest${COLOR_WORD[f]}`, use: "the fill one step from rest, with the fill at rest; for nothing else. Always bordered with the edge" },
2764
+ "stamp-2": { req: "CTAs, the fill two steps from rest", theming: (f) => `follows the fill at rest${COLOR_WORD[f]}`, use: "the fill two steps from rest, with the fill at rest; for nothing else. Always bordered with the edge" },
2765
+ "stamp-edge": { req: "min APCA visibility", theming: (f) => `draws for CTAs that sit close to the page; strength per family tier${COLOR_WORD[f]}`, use: "the border of the fill at every step, always rendered with it and never without it (it resolves to transparent unless the fill sits close to the page); for nothing else" },
2766
+ "stamp-on": { req: "text over the CTA color", contrast: `${AA_BODY} over its CTA`, theming: (f) => `whichever pole passes; quiet CTAs take the soft pole${COLOR_WORD[f]}`, use: "the only text color over the fill at every step; never elsewhere" }
2778
2767
  };
2779
2768
  var NEUTRAL_ONLY = {
2780
2769
  // the pole is measured against the neutral's own stops only; as text, it is the on-color
@@ -2783,15 +2772,15 @@ var NEUTRAL_ONLY = {
2783
2772
  // the literal pole carries no tint, so no theming line
2784
2773
  "pen-100": { req: "max-emphasis text", contrast: AA_BODY, claim: () => `${AA_BODY} on every paper and chalk of every family` }
2785
2774
  };
2786
- var LINK = (state, contrast) => ({
2787
- req: "links" + state,
2775
+ var LINK = (step, contrast) => ({
2776
+ req: "links" + step,
2788
2777
  contrast,
2789
2778
  claim: () => `${contrast} on every paper of the neutral`,
2790
2779
  theming: "rides the theme\u2019s link color; custom seed re-solves; overridable per theme",
2791
2780
  use: "hyperlinks; not a text-style CTA, which is the text stops read as states"
2792
2781
  });
2793
- var LINK_INVERSE = (state, contrast) => ({
2794
- req: "links over inverted backgrounds" + state,
2782
+ var LINK_INVERSE = (step, contrast) => ({
2783
+ req: "links over inverted backgrounds" + step,
2795
2784
  contrast,
2796
2785
  claim: () => `${contrast} on any family's pen-70`,
2797
2786
  theming: "same seed as the link, re-solved for inverted backgrounds; overridable per theme",
@@ -2800,12 +2789,12 @@ var LINK_INVERSE = (state, contrast) => ({
2800
2789
  var SYSTEM = {
2801
2790
  "absolute/brand": { req: "identity seed reference", theming: "the theme\u2019s own input, as given", use: "a reference value (logos, swatches of the input); never a UI color, it carries no contrast guarantee" },
2802
2791
  "absolute/brand-alt": { req: "identity seed reference", theming: "the theme\u2019s paired input, as given", use: "a reference value (logos, swatches of the input); never a UI color, it carries no contrast guarantee" },
2803
- "link/default/enabled": LINK("", AA_BODY),
2804
- "link/default/hover": LINK(" pointer-over", AA_BODY),
2805
- "link/default/pressed": LINK(" pressed", AA_BODY),
2806
- "link/inverse/enabled": LINK_INVERSE("", AA_BODY),
2807
- "link/inverse/hover": LINK_INVERSE(" pointer-over", AA_BODY),
2808
- "link/inverse/pressed": LINK_INVERSE(" pressed", AA_BODY)
2792
+ "link/default-0": LINK("", AA_BODY),
2793
+ "link/default-1": LINK(", one step from rest", AA_BODY),
2794
+ "link/default-2": LINK(", two steps from rest", AA_BODY),
2795
+ "link/inverse-0": LINK_INVERSE("", AA_BODY),
2796
+ "link/inverse-1": LINK_INVERSE(", one step from rest", AA_BODY),
2797
+ "link/inverse-2": LINK_INVERSE(", two steps from rest", AA_BODY)
2809
2798
  };
2810
2799
  var PREFIXES = [
2811
2800
  // community spellings first: the family word is a prefix of the community shapes
@@ -3242,11 +3231,12 @@ function escapeCtaFamily(nScale, mode, contrastProfile) {
3242
3231
  if (!strongText) throw new Error("escapeCtaFamily: the neutral scale has no ink-11 stop");
3243
3232
  const mk = (stop, L) => makeStop(stop, L, strongText.C, strongText.H);
3244
3233
  const cta = mk(9, strongText.L);
3245
- const ctaHover = mk(10, stateFillL(strongText.L, mode, 1));
3246
- const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2));
3247
3234
  const onEnforce = contrastProfile !== "apca";
3248
3235
  const onFloor = contrastProfile === "apca" ? void 0 : 4.5;
3249
3236
  const onFillIsWhite = onTextIsWhite(apcaY(cta.r, cta.g, cta.b), cta.L, cta.C, cta.H, onEnforce, onFloor);
3237
+ const keepsPole = stepKeepsPole(mode, onFillIsWhite, () => strongText.C, strongText.H, { ratioFloor: onFloor });
3238
+ const ctaHover = mk(10, stateFillL(strongText.L, mode, 1, keepsPole));
3239
+ const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2, keepsPole));
3250
3240
  return { cta, ctaHover, ctaPressed, onFillIsWhite };
3251
3241
  }
3252
3242
  var SECONDARY_DISTINCT_DELTA_E = 0.12;
@@ -3796,9 +3786,6 @@ function colorFromHexString(hex) {
3796
3786
  const h = hex.replace("#", "");
3797
3787
  return color(parseInt(h.slice(0, 2), 16) / 255, parseInt(h.slice(2, 4), 16) / 255, parseInt(h.slice(4, 6), 16) / 255);
3798
3788
  }
3799
- function bandedLeaf(flat) {
3800
- return STAMP_STATE_LEAVES[flat] ?? flat;
3801
- }
3802
3789
  function groupEntries(g2) {
3803
3790
  const entries = Object.entries(g2);
3804
3791
  const digitLeading = (k) => /^\d/.test(k);
@@ -3806,24 +3793,15 @@ function groupEntries(g2) {
3806
3793
  return entries.sort((a, b) => parseInt(b[0], 10) - parseInt(a[0], 10));
3807
3794
  return entries;
3808
3795
  }
3809
- function putLeaf(g2, flat, tok) {
3810
- const path = bandedLeaf(flat).split("/");
3811
- let cur = g2;
3812
- for (const seg of path.slice(0, -1)) {
3813
- if (!(seg in cur) || "$type" in cur[seg]) cur[seg] = {};
3814
- cur = cur[seg];
3815
- }
3816
- cur[path[path.length - 1]] = tok;
3817
- }
3818
3796
  function rampGroup(stops, onFillWhite, extra) {
3819
3797
  const g2 = {};
3820
3798
  const leaves = stops.map((s) => ({ name: stopTokenName(s.stop), tok: colorFromStop(s) })).sort((a, b) => tokenOrder(a.name) - tokenOrder(b.name));
3821
- for (const l of leaves) putLeaf(g2, l.name, l.tok);
3822
- if (extra?.cta) putLeaf(g2, STAMP_FILL, colorFromStop(extra.cta));
3823
- if (extra?.ctaHover) putLeaf(g2, STAMP_FILL_HOVER, colorFromStop(extra.ctaHover));
3824
- if (extra?.ctaPressed) putLeaf(g2, STAMP_FILL_PRESSED, colorFromStop(extra.ctaPressed));
3825
- if (extra?.cta) putLeaf(g2, STAMP_EDGE, extra.ctaBorder ?? TRANSPARENT_TOKEN);
3826
- putLeaf(g2, STAMP_ON, pole(onFillWhite));
3799
+ for (const l of leaves) g2[l.name] = l.tok;
3800
+ if (extra?.cta) g2[STAMP_FILL] = colorFromStop(extra.cta);
3801
+ if (extra?.ctaHover) g2[STAMP_FILL_HOVER] = colorFromStop(extra.ctaHover);
3802
+ if (extra?.ctaPressed) g2[STAMP_FILL_PRESSED] = colorFromStop(extra.ctaPressed);
3803
+ if (extra?.cta) g2[STAMP_EDGE] = extra.ctaBorder ?? TRANSPARENT_TOKEN;
3804
+ g2[STAMP_ON] = pole(onFillWhite);
3827
3805
  return g2;
3828
3806
  }
3829
3807
  function themeToFigma(r, input) {
@@ -3862,16 +3840,18 @@ function themeToFigma(r, input) {
3862
3840
  if (input.secondaryStyle === "outline" && input.secondary) {
3863
3841
  const s8 = secondary[mode].find((s) => s.stop === 8);
3864
3842
  const s9 = secondary[mode].find((s) => s.stop === 9);
3865
- putLeaf(secondaryGroup, STAMP_FILL, TRANSPARENT_TOKEN);
3843
+ secondaryGroup[STAMP_FILL] = TRANSPARENT_TOKEN;
3866
3844
  if (s8) {
3867
3845
  const e = srgbEmitChannels(s8);
3868
- putLeaf(secondaryGroup, STAMP_FILL_HOVER, color(e.r, e.g, e.b, OUTLINE_HOVER_ALPHA));
3869
- putLeaf(secondaryGroup, STAMP_FILL_PRESSED, color(e.r, e.g, e.b, OUTLINE_PRESSED_ALPHA));
3870
- putLeaf(secondaryGroup, STAMP_EDGE, colorFromStop(s8));
3846
+ secondaryGroup[STAMP_FILL_HOVER] = color(e.r, e.g, e.b, OUTLINE_HOVER_ALPHA);
3847
+ secondaryGroup[STAMP_FILL_PRESSED] = color(e.r, e.g, e.b, OUTLINE_PRESSED_ALPHA);
3848
+ secondaryGroup[STAMP_EDGE] = colorFromStop(s8);
3871
3849
  }
3872
- if (s9) putLeaf(secondaryGroup, STAMP_ON, colorFromStop(s9));
3850
+ if (s9) secondaryGroup[STAMP_ON] = colorFromStop(s9);
3873
3851
  }
3874
- const softOnCta = (g3, white) => putLeaf(g3, STAMP_ON, pole(white, SOFT_ON_CTA_ALPHA[mode]));
3852
+ const softOnCta = (g3, white) => {
3853
+ g3[STAMP_ON] = pole(white, SOFT_ON_CTA_ALPHA[mode]);
3854
+ };
3875
3855
  softOnCta(neutralGroup, mode === "light" ? nScale.onFillTextIsWhite : nScale.onFillTextIsWhiteDark);
3876
3856
  if (input.secondary && input.secondaryStyle !== "outline" && softOnCtaPasses(input.secondary, mode))
3877
3857
  softOnCta(secondaryGroup, mode === "light" ? secondaryOnFillLight : secondaryOnFillDark);
@@ -3879,10 +3859,10 @@ function themeToFigma(r, input) {
3879
3859
  const esc = input.ctaEscape ? escapeCtaFamily(nScale, mode, input.contrastProfile) : null;
3880
3860
  if (esc) {
3881
3861
  for (const g3 of input.secondary ? [brandGroup] : [brandGroup, secondaryGroup]) {
3882
- putLeaf(g3, STAMP_FILL, colorFromStop(esc.cta));
3883
- putLeaf(g3, STAMP_FILL_HOVER, colorFromStop(esc.ctaHover));
3884
- putLeaf(g3, STAMP_FILL_PRESSED, colorFromStop(esc.ctaPressed));
3885
- putLeaf(g3, STAMP_ON, pole(esc.onFillIsWhite));
3862
+ g3[STAMP_FILL] = colorFromStop(esc.cta);
3863
+ g3[STAMP_FILL_HOVER] = colorFromStop(esc.ctaHover);
3864
+ g3[STAMP_FILL_PRESSED] = colorFromStop(esc.ctaPressed);
3865
+ g3[STAMP_ON] = pole(esc.onFillIsWhite);
3886
3866
  }
3887
3867
  }
3888
3868
  const scaleTextAt = (n) => {
@@ -3892,7 +3872,7 @@ function themeToFigma(r, input) {
3892
3872
  };
3893
3873
  const trio = (posture) => {
3894
3874
  const t = posture === "default" ? lt ? mode === "light" ? [lt.link, lt.linkHover, lt.linkPressed] : [lt.linkDark, lt.linkHoverDark, lt.linkPressedDark] : [scaleTextAt(9), scaleTextAt(10), scaleTextAt(11)] : mode === "light" ? [invLt.link, invLt.linkHover, invLt.linkPressed] : [invLt.linkDark, invLt.linkHoverDark, invLt.linkPressedDark];
3895
- return Object.fromEntries(LINK_STATES.map((state, i) => [state, colorFromStop(t[i])]));
3875
+ return Object.fromEntries(LINK_STATES.map((state, i) => [linkLeaf(posture, state), colorFromStop(t[i])]));
3896
3876
  };
3897
3877
  const g2 = {
3898
3878
  [CSS_FAMILY.neutral]: neutralGroup,
@@ -3907,7 +3887,7 @@ function themeToFigma(r, input) {
3907
3887
  ctaFamily(sig.scale, mode, role)
3908
3888
  );
3909
3889
  }
3910
- g2[LINK_GROUP] = { default: trio("default"), inverse: trio("inverse") };
3890
+ g2[LINK_GROUP] = { ...trio("default"), ...trio("inverse") };
3911
3891
  g2[ABSOLUTE_GROUP] = {
3912
3892
  [CSS_FAMILY.brandPrimary]: colorFromHexString(scale.identityHex ?? invSeed),
3913
3893
  [CSS_FAMILY.brandSecondary]: colorFromHexString((input.secondary ?? scale).identityHex ?? invSeed)
@@ -4008,6 +3988,7 @@ export {
4008
3988
  LINK_GROUP,
4009
3989
  LINK_POSTURES,
4010
3990
  LINK_STATES,
3991
+ LINK_STEP,
4011
3992
  MODE_SPECS,
4012
3993
  OFFSET_ALPHAS,
4013
3994
  OUTLINE_HOVER_ALPHA,
@@ -4022,9 +4003,7 @@ export {
4022
4003
  STAMP_FILL,
4023
4004
  STAMP_FILL_HOVER,
4024
4005
  STAMP_FILL_PRESSED,
4025
- STAMP_LEAF,
4026
4006
  STAMP_ON,
4027
- STAMP_STATE_LEAVES,
4028
4007
  SUBTLE_PASTEL_K,
4029
4008
  SUBTLE_TINT_MULT,
4030
4009
  absolutePath,
@@ -4045,6 +4024,7 @@ export {
4045
4024
  generateScale,
4046
4025
  generateSubtleSecondary,
4047
4026
  groupEntries,
4027
+ linkLeaf,
4048
4028
  linkPath,
4049
4029
  neutralCss,
4050
4030
  neutralTintHue,
@@ -10,4 +10,4 @@ export declare function medianLForArchetype(archetype: Archetype): number;
10
10
  export declare function hoverL(L: number): number;
11
11
  export declare function pressedL(L: number): number;
12
12
  export declare function stateStepL(restL: number, _mode: 'light' | 'dark', k: 1 | 2): number;
13
- export declare function stateFillL(restL: number, mode: 'light' | 'dark', k: 1 | 2): number;
13
+ export declare function stateFillL(restL: number, mode: 'light' | 'dark', k: 1 | 2, keepsPole?: (L: number) => boolean): number;
@@ -18,11 +18,11 @@ export type DtcgGroup = {
18
18
  export type DtcgDocument = DtcgGroup;
19
19
  /**
20
20
  * Every primitive's path, in document order: the neutral with its poles, the brand, the
21
- * alt, the four signals by role, the link trios, the seed absolutes. This roster is the
21
+ * alt, the four signals by role, the link rows, the seed absolutes. This roster is the
22
22
  * definition of what the engine emits; the emitters and the audit ride it.
23
23
  */
24
24
  export declare function tokenPaths(): TokenPath[];
25
- /** the path of a structured-emit name (`brand-stamp-fill`), or undefined when it is not a primitive */
25
+ /** the path of a structured-emit name (`brand-stamp-0`), or undefined when it is not a primitive */
26
26
  export declare function tokenPathOf(name: string): TokenPath | undefined;
27
27
  /** the description key of a structured-emit name: its slash-joined path */
28
28
  export declare function descriptionPathOf(name: string): string | undefined;
@@ -14,7 +14,6 @@ export type FigmaGroup = {
14
14
  [key: string]: FigmaLeaf | FigmaGroup;
15
15
  };
16
16
  export declare function groupEntries(g: FigmaGroup): Array<[string, FigmaLeaf | FigmaGroup]>;
17
- export declare function putLeaf(g: FigmaGroup, flat: string, tok: FigmaLeaf): void;
18
17
  export interface ThemeInput {
19
18
  secondary?: GeneratedScale | null;
20
19
  secondaryStyle?: SecondaryStyle;
@@ -133,6 +133,10 @@ export declare function ctaDarkDualGateL(base: {
133
133
  L: number;
134
134
  C: number;
135
135
  }, darkH: number, onFillTextIsWhite: boolean, enforce: boolean, coLc: number, capLoL: number, capHiL: number): number;
136
+ export declare function stepKeepsPole(mode: 'light' | 'dark', white: boolean, chromaAt: (L: number) => number, H: number, law: {
137
+ ratioFloor?: number;
138
+ enforceLc?: number;
139
+ }): ((L: number) => boolean) | undefined;
136
140
  export declare function onFillIsWhiteDarkAt(L: number, C: number, H: number, enforce: boolean, ratioFloor?: number): boolean;
137
141
  export interface BrandExitLanding {
138
142
  L: number;
@@ -2,29 +2,25 @@ export declare const SCALE_STOP_COUNT: number;
2
2
  export declare function stopTokenName(stop: number): string;
3
3
  export declare const PAPER_0 = "paper-0";
4
4
  export declare const PEN_100 = "pen-100";
5
- export declare const STAMP_FILL = "stamp-fill";
6
- export declare const STAMP_FILL_HOVER = "stamp-fill-hover";
7
- export declare const STAMP_FILL_PRESSED = "stamp-fill-pressed";
5
+ export declare const STAMP_FILL = "stamp-0";
6
+ export declare const STAMP_FILL_HOVER = "stamp-1";
7
+ export declare const STAMP_FILL_PRESSED = "stamp-2";
8
8
  export declare const STAMP_EDGE = "stamp-edge";
9
9
  export declare const STAMP_ON = "stamp-on";
10
- export declare const STAMP_STATE_LEAVES: Record<string, string>;
11
- export declare const STAMP_LEAF: {
12
- readonly FILL: string;
13
- readonly FILL_HOVER: string;
14
- readonly FILL_PRESSED: string;
15
- readonly EDGE: string;
16
- readonly ON: string;
17
- };
18
10
  export type TokenPath = readonly string[];
19
11
  export declare const cssVarName: (path: TokenPath) => string;
20
12
  export declare const figmaPathOf: (path: TokenPath) => string;
21
- /** a family row: a scale stop, a pole, or a stamp token (nested under stamp/) */
13
+ /** a family row: a scale stop, a pole, or a stamp row, flat in the family group */
22
14
  export declare const familyPath: (family: string, leaf: string) => string[];
23
15
  export declare const LINK_GROUP = "link";
24
16
  export declare const LINK_POSTURES: readonly ["default", "inverse"];
25
17
  export type LinkPosture = (typeof LINK_POSTURES)[number];
26
18
  export declare const LINK_STATES: readonly ["enabled", "hover", "pressed"];
27
19
  export type LinkState = (typeof LINK_STATES)[number];
20
+ /** the step each state is emitted as: rest 0, one step 1, two steps 2 */
21
+ export declare const LINK_STEP: Record<LinkState, number>;
22
+ /** the one word a link row has: its posture at its step (`default-1`) */
23
+ export declare const linkLeaf: (posture: LinkPosture, state: LinkState) => string;
28
24
  export declare const linkPath: (posture: LinkPosture, state: LinkState) => string[];
29
25
  export declare const ABSOLUTE_GROUP = "absolute";
30
26
  export declare const absolutePath: (family: string) => string[];
@@ -6,7 +6,7 @@ export { themeTokens, tokensFromEmission, readEmission, resolveReferences, type
6
6
  export { themeToFigma, groupEntries, type FigmaGroup, type FigmaColorToken, type FigmaLeaf, type ThemeInput, } from './engine/figmaRender';
7
7
  export { tokensToDtcg, tokenPathOf, descriptionPathOf, type DtcgDocument, type DtcgGroup, type DtcgToken, type DtcgColorValue, type DtcgOptions, } from './engine/dtcgRender';
8
8
  export { MODE_SPECS, type ModeSpec } from './engine/requirements/spec';
9
- export { stopTokenName, tokenOrder, SCALE_STOP_COUNT, PAPER_0, PEN_100, STAMP_FILL, STAMP_FILL_HOVER, STAMP_FILL_PRESSED, STAMP_EDGE, STAMP_ON, STAMP_STATE_LEAVES, STAMP_LEAF, cssVarName, figmaPathOf, familyPath, linkPath, absolutePath, LINK_GROUP, LINK_POSTURES, LINK_STATES, ABSOLUTE_GROUP, COLOR_GROUP, type TokenPath, type LinkPosture, type LinkState, } from './engine/tokenNames';
9
+ export { stopTokenName, tokenOrder, SCALE_STOP_COUNT, PAPER_0, PEN_100, STAMP_FILL, STAMP_FILL_HOVER, STAMP_FILL_PRESSED, STAMP_EDGE, STAMP_ON, cssVarName, figmaPathOf, familyPath, linkLeaf, linkPath, LINK_STEP, absolutePath, LINK_GROUP, LINK_POSTURES, LINK_STATES, ABSOLUTE_GROUP, COLOR_GROUP, type TokenPath, type LinkPosture, type LinkState, } from './engine/tokenNames';
10
10
  export { SIGNAL_EMIT_NAME } from './engine/signals';
11
11
  export { classifyArchetype, type Archetype } from './engine/archetypes';
12
12
  export { SIGNALS, type SignalDef } from './engine/signals';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okchroma",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
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": {