okchroma 0.3.1 → 0.5.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/README.md +3 -3
- package/dist-lib/index.cjs +107 -69
- package/dist-lib/index.mjs +107 -69
- package/dist-lib/types/engine/collision.d.ts +3 -3
- package/dist-lib/types/engine/colorEngine.d.ts +1 -1
- package/dist-lib/types/engine/cssRender.d.ts +17 -2
- package/dist-lib/types/engine/figmaRender.d.ts +8 -3
- package/dist-lib/types/engine/requirements/spec.d.ts +2 -2
- package/dist-lib/types/engine/tokenNames.d.ts +1 -1
- package/dist-lib/types/index.d.ts +3 -3
- package/package.json +1 -1
- package/tokens/semantic.css +90 -97
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@ light and dark token system whose contrast requirements are solved during genera
|
|
|
5
5
|
then emits it as CSS custom properties or Figma variables.
|
|
6
6
|
|
|
7
7
|
Every family (neutral, brand, brand-alt, critical, warning, positive, info) carries the
|
|
8
|
-
same scale: 3 papers, 4
|
|
8
|
+
same scale: 3 papers, 4 chalks, 1 highlighter, 1 pencil, 2 pens, plus the stamp (the
|
|
9
9
|
solid fill with its hover and pressed states, its edge, and its text). The neutral adds
|
|
10
10
|
the two poles. Light and dark resolve together and ship on the same names, so a token is
|
|
11
11
|
mapped once and holds for any brand.
|
|
12
12
|
|
|
13
|
-
Contrast is built into the math, not checked afterwards:
|
|
14
|
-
3:1 (the non-text bar), pencil at 4.5:1, pen at 4.5:1 on every paper and
|
|
13
|
+
Contrast is built into the math, not checked afterwards: highlighter reads on every paper at
|
|
14
|
+
3:1 (the non-text bar), pencil at 4.5:1, pen at 4.5:1 on every paper and chalk in
|
|
15
15
|
both directions; the stamp's text passes 4.5:1 on its fill. APCA is used once, as a booster
|
|
16
16
|
that nudges the stamp fill until its text reads at Lc 65. Each claim, its scope, and the audit that
|
|
17
17
|
proves it: [Guarantees](https://egerrity.github.io/okchroma/#/docs/guarantees).
|
package/dist-lib/index.cjs
CHANGED
|
@@ -24,8 +24,10 @@ __export(index_exports, {
|
|
|
24
24
|
DEFAULT_LINK_HEX: () => DEFAULT_LINK_HEX,
|
|
25
25
|
DISABLED_OPACITY: () => DISABLED_OPACITY,
|
|
26
26
|
EXT_KEY: () => EXT_KEY,
|
|
27
|
+
INTERACTION_RUNGS: () => INTERACTION_RUNGS,
|
|
27
28
|
MODE_SPECS: () => MODE_SPECS,
|
|
28
29
|
OFFSET_ALPHAS: () => OFFSET_ALPHAS,
|
|
30
|
+
OPACITY_RUNGS: () => OPACITY_RUNGS,
|
|
29
31
|
OUTLINE_HOVER_ALPHA: () => OUTLINE_HOVER_ALPHA,
|
|
30
32
|
PAPER_0: () => PAPER_0,
|
|
31
33
|
PEN_100: () => PEN_100,
|
|
@@ -59,6 +61,10 @@ __export(index_exports, {
|
|
|
59
61
|
generateScale: () => generateScale,
|
|
60
62
|
generateSubtleSecondary: () => generateSubtleSecondary,
|
|
61
63
|
neutralCss: () => neutralCss,
|
|
64
|
+
neutralTintHue: () => neutralTintHue,
|
|
65
|
+
opacityLeafName: () => opacityLeafName,
|
|
66
|
+
opacityTokenPath: () => opacityTokenPath,
|
|
67
|
+
opacityVarName: () => opacityVarName,
|
|
62
68
|
pageStopFor: () => pageStopFor,
|
|
63
69
|
parseToken: () => parseToken,
|
|
64
70
|
resolveBrand: () => resolveBrand,
|
|
@@ -397,7 +403,7 @@ var NEUTRAL_TINT_POINTS = {
|
|
|
397
403
|
[0.95, 0.895],
|
|
398
404
|
// paper-3 ─┘
|
|
399
405
|
[0.924, 1],
|
|
400
|
-
// wash-4 ─┐ peak: the mid-
|
|
406
|
+
// wash-4 ─┐ peak: the mid-chalk band, the neutral's most-branded moment
|
|
401
407
|
[0.892, 1],
|
|
402
408
|
// wash-5 ─┘
|
|
403
409
|
[0.852, 0.842],
|
|
@@ -405,7 +411,7 @@ var NEUTRAL_TINT_POINTS = {
|
|
|
405
411
|
[0.801, 0.653],
|
|
406
412
|
// wash-7 │
|
|
407
413
|
[0.738, 0.505],
|
|
408
|
-
//
|
|
414
|
+
// highlighter-26
|
|
409
415
|
[0.6, 0.4],
|
|
410
416
|
// knee (ex-scaffold) the drop-off: hue fades as you climb
|
|
411
417
|
[0.56, 0.347],
|
|
@@ -431,7 +437,7 @@ var NEUTRAL_TINT_POINTS = {
|
|
|
431
437
|
[0.42, 0.653],
|
|
432
438
|
// wash-7
|
|
433
439
|
[0.55, 0.505],
|
|
434
|
-
//
|
|
440
|
+
// highlighter-26
|
|
435
441
|
[0.6, 0.4],
|
|
436
442
|
// knee (ex-scaffold)
|
|
437
443
|
[0.64, 0.347],
|
|
@@ -806,18 +812,18 @@ var SHARED_NAMES = {
|
|
|
806
812
|
2: "paper-3",
|
|
807
813
|
// stop 3 renamed wash-3 → paper-3 (owner 2026-07-24, elevation round): it is a
|
|
808
814
|
// PLANE in both themes (light sink / dark pop), so it belongs to the surface band.
|
|
809
|
-
// NAME ONLY — generation is index-keyed and unchanged; the
|
|
810
|
-
// (collision.ts
|
|
815
|
+
// NAME ONLY — generation is index-keyed and unchanged; the chalk-collision machinery
|
|
816
|
+
// (collision.ts CHALK_STOPS) deliberately still spans stops 3–7. (The requirement
|
|
811
817
|
// spec's `group` label followed the band to 'paper' on 2026-08-31 — labels only,
|
|
812
818
|
// the spans never moved; see spec.ts groupOf.)
|
|
813
819
|
// Stage B (owner 2026-08-07, names only): relabeled to the ROOT_L-derived digit,
|
|
814
820
|
// paper-5 — same band, same index.
|
|
815
821
|
3: "paper-5",
|
|
816
|
-
4: "
|
|
817
|
-
5: "
|
|
818
|
-
6: "
|
|
819
|
-
7: "
|
|
820
|
-
8: "
|
|
822
|
+
4: "chalk-8",
|
|
823
|
+
5: "chalk-11",
|
|
824
|
+
6: "chalk-15",
|
|
825
|
+
7: "chalk-20",
|
|
826
|
+
8: "highlighter-26",
|
|
821
827
|
// THE PEN BAND (C49, owner 2026-08-05): the first text stop + emphasis fill (the
|
|
822
828
|
// 2026-07-29 highlight collapse — the old highlight-9 and on-highlight are
|
|
823
829
|
// deleted; this stop carries both jobs, its on-color a paper token), the between
|
|
@@ -869,13 +875,12 @@ var SYSTEM_LEAF = {
|
|
|
869
875
|
},
|
|
870
876
|
ALPHA: {
|
|
871
877
|
TRANSPARENT: "system/alpha/transparent",
|
|
872
|
-
SCRIM: "system/alpha/abs-black-060",
|
|
873
878
|
INK: "system/alpha/ink",
|
|
874
879
|
// the DIRECTIONAL ladders (owner 2026-08-31, renaming the 2026-08-18 bare rungs
|
|
875
880
|
// and the 2026-08-29 inverse ladder): away-from-bg is the page-polarity pole
|
|
876
881
|
// (black in light, white in dark), toward-bg the flipped pole — STATE LAYERS for
|
|
877
882
|
// INVERTED grounds, whose polarity is opposite the page's. (Never call toward-bg
|
|
878
|
-
// a "
|
|
883
|
+
// a "chalk": that word belongs to the tinted band stops.) The reversal lives in
|
|
879
884
|
// the engine: a consumer on an inverted ground names one token and never picks a
|
|
880
885
|
// pole. Colored fills never ride these; their states stay engine-solved stops.
|
|
881
886
|
// Rungs are two-digit percent, harmonizing with shadow-04/08/12.
|
|
@@ -888,6 +893,19 @@ var SYSTEM_LEAF = {
|
|
|
888
893
|
SHADOW_04: "system/alpha/shadow-04",
|
|
889
894
|
SHADOW_08: "system/alpha/shadow-08",
|
|
890
895
|
SHADOW_12: "system/alpha/shadow-12"
|
|
896
|
+
},
|
|
897
|
+
// the OPACITY LADDER's rows: bare numbers, never colors. `alpha` above is the group
|
|
898
|
+
// of colors that carry transparency; `opacity` is the numbers they compose with
|
|
899
|
+
// (values in cssRender OPACITY_RUNGS; the ext plugin zones these under utility/).
|
|
900
|
+
OPACITY: {
|
|
901
|
+
"004": "system/opacity/004",
|
|
902
|
+
"008": "system/opacity/008",
|
|
903
|
+
"012": "system/opacity/012",
|
|
904
|
+
"016": "system/opacity/016",
|
|
905
|
+
"024": "system/opacity/024",
|
|
906
|
+
"032": "system/opacity/032",
|
|
907
|
+
"048": "system/opacity/048",
|
|
908
|
+
"064": "system/opacity/064"
|
|
891
909
|
}
|
|
892
910
|
};
|
|
893
911
|
var SURFACE_PLANE_LAW = {
|
|
@@ -920,14 +938,14 @@ var P_FIXED = { hue: "warm-torsion", L: "fixed", chroma: "ladder" };
|
|
|
920
938
|
var P_TEXT = { hue: "warm-torsion", L: "perceptual", chroma: "brand" };
|
|
921
939
|
var S8 = { metric: "wcag", against: "paper-5", target: STOP_8_NONTEXT_CONTRAST, level: "AA" };
|
|
922
940
|
var T9 = { metric: "wcag", against: "paper-5", target: PENCIL_9_CONTRAST, level: "AA" };
|
|
923
|
-
var T10 = { metric: "wcag", against: "
|
|
941
|
+
var T10 = { metric: "wcag", against: "chalk-20", target: PEN_10_CONTRAST, level: "AA" };
|
|
924
942
|
var T11 = { metric: "wcag", against: "paper-5", target: PEN_11_CONTRAST_FLOOR, level: "AAA" };
|
|
925
943
|
var ONS = { onFill: { metric: "apca-pole", enforce: true, ratioFloor: 4.5, coEnforceLc: 65 } };
|
|
926
944
|
var LIGHT = {
|
|
927
945
|
stops: [
|
|
928
946
|
// paper-0 (paper-0 pre-Stage-B): the resolved ladder extreme — in light it genuinely is white (rootL 1.0, zero chroma)
|
|
929
947
|
{ stop: 0, rootL: 1, group: groupOf(0), produce: { hue: "warm-drift", L: "fixed", chroma: "ladder" }, satFraction: SCALE_C_LIGHT[0].sat, baseC: SCALE_C_LIGHT[0].base },
|
|
930
|
-
// paper/highlighter
|
|
948
|
+
// paper/chalk/highlighter-26: perceptual ladder/envelope blend on the geometric ROOT_L_LIGHT scaffold. Separation
|
|
931
949
|
// falls out of the shape (see above) — no min-separation require. Only stop 8 carries a require: the WCAG
|
|
932
950
|
// 3:1 vs the resolved paper-3 (paper-2) (re-solves automatically since it references paper-3 (paper-2)).
|
|
933
951
|
...[1, 2, 3, 4, 5, 6, 7, 8].map((stop) => ({
|
|
@@ -958,7 +976,7 @@ var DARK = {
|
|
|
958
976
|
// paper-0 (paper-0 pre-Stage-B): the resolved ladder extreme — one seam BELOW paper-1 (paper-1), deep and brand-tinted, never the
|
|
959
977
|
// absolute void (the old hard-coded #000000 was "too much"). Lift applies like the rest of the scale.
|
|
960
978
|
{ stop: 0, rootL: PAPER0_DARK_ROOT_L, group: groupOf(0), produce: P_LIFT, satFraction: SCALE_C_DARK[0].sat },
|
|
961
|
-
// paper/
|
|
979
|
+
// paper/chalk 1–7: perceptual on the dark scaffold. stop 8: FIXED at the hand-placed scaffold BUT with the
|
|
962
980
|
// 3:1 non-text require DECLARED (the Stage-5 flip, owner-approved) — the blue-recede failure is prevented
|
|
963
981
|
// BY RULE, not by patch. The require now genuinely PLACES the stop rather than catching a few low-luminance
|
|
964
982
|
// hues: on the delta-carry path it solves from the sentinel every time, and the old claim that "most hues
|
|
@@ -1903,7 +1921,7 @@ function p2Diff(a, b) {
|
|
|
1903
1921
|
var DEFAULT_APCA_LC_MAP = { 3: 30, 4.5: 75, 6.5: 85, 7: 90 };
|
|
1904
1922
|
function toApca(req, lcMap) {
|
|
1905
1923
|
if (req.metric !== "wcag") return req;
|
|
1906
|
-
if (req.against === "
|
|
1924
|
+
if (req.against === "chalk-20") return { metric: "apca", against: "paper-3", targetLc: 85 };
|
|
1907
1925
|
const targetLc = lcMap[req.target];
|
|
1908
1926
|
if (targetLc === void 0) throw new Error(`apca profile: no Lc mapping for wcag target ${req.target}`);
|
|
1909
1927
|
return { metric: "apca", against: req.against, targetLc };
|
|
@@ -2389,7 +2407,7 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2389
2407
|
if (!ref) throw new Error(`stop ${forWhom}: require against stop ${stopNum} but it is not resolved yet`);
|
|
2390
2408
|
return ref;
|
|
2391
2409
|
};
|
|
2392
|
-
const AGAINST_STOP = { "paper-1": 1, "paper-3": 2, "paper-5": 3, "
|
|
2410
|
+
const AGAINST_STOP = { "paper-1": 1, "paper-3": 2, "paper-5": 3, "chalk-20": 7 };
|
|
2393
2411
|
const declaredAnchor = (req) => req.metric === "min-separation" ? 1 : AGAINST_STOP[req.against] ?? 2;
|
|
2394
2412
|
const wcagAnchorStop = (req, stop) => stop >= 9 && declaredAnchor(req) <= 3 ? 3 : declaredAnchor(req);
|
|
2395
2413
|
const textGround = ctx.opts?.textGround?.[mode];
|
|
@@ -2503,13 +2521,13 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2503
2521
|
const own = contrastRatio(y2, anchorShipY);
|
|
2504
2522
|
return paperBound ? Math.min(own, contrastRatio(y2, crossBoundY)) : own;
|
|
2505
2523
|
};
|
|
2506
|
-
const
|
|
2507
|
-
const
|
|
2524
|
+
const chalkBoundY = ctx.opts?.crossChalkBoundY && sp.stop >= 10 && !textGround ? ctx.opts.crossChalkBoundY[mode] : void 0;
|
|
2525
|
+
const chalkRatio = (L) => chalkBoundY === void 0 ? Infinity : contrastRatio(shippedY(L, placed.C, placed.H), chalkBoundY);
|
|
2508
2526
|
const paperBoundY = ctx.opts?.crossPaperBoundY && sp.stop >= 8 && !textGround ? ctx.opts.crossPaperBoundY[mode] : void 0;
|
|
2509
2527
|
const paperBar = sp.stop === 8 ? STOP_8_NONTEXT_CONTRAST : sp.stop === 9 ? PENCIL_9_CONTRAST : PEN_10_CONTRAST;
|
|
2510
2528
|
const paperRatio = (L) => paperBoundY === void 0 ? Infinity : contrastRatio(shippedY(L, placed.C, placed.H), paperBoundY);
|
|
2511
2529
|
const target = sp.require.target;
|
|
2512
|
-
const clears = (L) => shipRatio(L) >= target &&
|
|
2530
|
+
const clears = (L) => shipRatio(L) >= target && chalkRatio(L) >= PEN_10_CONTRAST && paperRatio(L) >= paperBar;
|
|
2513
2531
|
if (!clears(placed.L)) {
|
|
2514
2532
|
const away = shippedY(placed.L, placed.C, placed.H) < anchorShipY ? -1 : 1;
|
|
2515
2533
|
let L2 = placed.L;
|
|
@@ -2525,7 +2543,7 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2525
2543
|
shipRatio(placed.L)
|
|
2526
2544
|
);
|
|
2527
2545
|
if (got < sp.require.target - 1e-3) unresolvable = `stop ${sp.stop}: contrast ${got.toFixed(2)} < required ${sp.require.target}`;
|
|
2528
|
-
else if (
|
|
2546
|
+
else if (chalkRatio(placed.L) < PEN_10_CONTRAST - 1e-3) unresolvable = `stop ${sp.stop}: contrast ${chalkRatio(placed.L).toFixed(2)} < required ${PEN_10_CONTRAST} on the worst chromatic chalk-20`;
|
|
2529
2547
|
else if (paperRatio(placed.L) < paperBar - 1e-3) unresolvable = `stop ${sp.stop}: contrast ${paperRatio(placed.L).toFixed(2)} < required ${paperBar} on the worst chromatic paper-5`;
|
|
2530
2548
|
} else if (sp.require?.metric === "apca") {
|
|
2531
2549
|
const ga = apcaGroundOf(sp.require, sp.stop);
|
|
@@ -2717,6 +2735,12 @@ function generateScale(hex, scaleName, forcedArchetype, opts) {
|
|
|
2717
2735
|
pen100Dark: penPole(dark, 1)
|
|
2718
2736
|
};
|
|
2719
2737
|
}
|
|
2738
|
+
function neutralTintHue(primaryH, source, secondaryH, customHex) {
|
|
2739
|
+
if (source === "secondary" && secondaryH !== void 0) return secondaryH;
|
|
2740
|
+
if (source === "custom" && customHex && /^#?[0-9a-fA-F]{6}$/.test(customHex))
|
|
2741
|
+
return hexToOklch(customHex.startsWith("#") ? customHex : `#${customHex}`).H;
|
|
2742
|
+
return primaryH;
|
|
2743
|
+
}
|
|
2720
2744
|
function generateNeutralScale(brandH, level = "default", contrastProfile) {
|
|
2721
2745
|
const h = (brandH % 360 + 360) % 360;
|
|
2722
2746
|
const { r, g: g2, b } = oklchToSrgbUnclamped(0.5, 6e-3, h);
|
|
@@ -2727,9 +2751,9 @@ function generateNeutralScale(brandH, level = "default", contrastProfile) {
|
|
|
2727
2751
|
chromaCurve: curve,
|
|
2728
2752
|
enforceOnFillContrast: true,
|
|
2729
2753
|
contrastProfile,
|
|
2730
|
-
// T13: the neutral's pens clear every chromatic family's
|
|
2754
|
+
// T13: the neutral's pens clear every chromatic family's chalk-20 (the symmetric
|
|
2731
2755
|
// pen guarantee); the chromatic ramps clear the neutral's through NEUTRAL_W80_WORST_SHIP_Y
|
|
2732
|
-
|
|
2756
|
+
crossChalkBoundY: CHROMATIC_W80_WORST_SHIP_Y,
|
|
2733
2757
|
crossPaperBoundY: CHROMATIC_P3_WORST_SHIP_Y
|
|
2734
2758
|
});
|
|
2735
2759
|
const asCta = (stop, src) => makeStop(stop, src.L, src.C, src.H);
|
|
@@ -2826,7 +2850,7 @@ var COLOR_WORD = {
|
|
|
2826
2850
|
[FAMILY.info]: " (blue)"
|
|
2827
2851
|
};
|
|
2828
2852
|
var PAPER = { req: "backgrounds, inverted text", theming: (f) => TINT[f], collides: true };
|
|
2829
|
-
var
|
|
2853
|
+
var CHALK = { req: "decorative borders, inverted text, illos, signal hierarchy", theming: (f) => TINT[f], collides: true };
|
|
2830
2854
|
var solved = (f) => `${TINT[f]}; re-solved to clear its floor`;
|
|
2831
2855
|
var OVERLAY = {
|
|
2832
2856
|
req: "translucent backgrounds that hold their reading on any paper",
|
|
@@ -2840,13 +2864,13 @@ var SCALE = {
|
|
|
2840
2864
|
"paper-97-overlay": OVERLAY,
|
|
2841
2865
|
"paper-5": { req: "backgrounds, inverted text", theming: (f) => `${TINT[f]}. Worst background text stops must clear.`, collides: true },
|
|
2842
2866
|
"paper-95-overlay": OVERLAY,
|
|
2843
|
-
"
|
|
2844
|
-
"
|
|
2845
|
-
"
|
|
2846
|
-
"
|
|
2867
|
+
"chalk-8": CHALK,
|
|
2868
|
+
"chalk-11": CHALK,
|
|
2869
|
+
"chalk-15": CHALK,
|
|
2870
|
+
"chalk-20": CHALK,
|
|
2847
2871
|
// ("icons" KEPT through the 2026-08-28 on-flood strip (owner): an "icon" query
|
|
2848
|
-
// landing on the
|
|
2849
|
-
"
|
|
2872
|
+
// landing on the highlighter rows is worth its ic-ON-s noise — the one surviving body carrier)
|
|
2873
|
+
"highlighter-26": { req: "focus rings, icons, large text, translucent state layers over any ground", contrast: AA_LARGE, theming: solved, collides: true },
|
|
2850
2874
|
"pencil-47": { req: "regular text, inverted backgrounds", contrast: AA_BODY, theming: solved, collides: true },
|
|
2851
2875
|
"pen-58": { req: "regular text, inverted backgrounds", contrast: AA_BODY, theming: solved, collides: true },
|
|
2852
2876
|
// ("high-emphasis" reworded 2026-08-12: the surface planes took low/high as label
|
|
@@ -2867,6 +2891,8 @@ var ABS = { req: "extreme poles for CTAs, aliased global endpoints" };
|
|
|
2867
2891
|
var OFFSET = { req: "min APCA visibility", theming: "offsets CTAs in themes where they sit close to the page" };
|
|
2868
2892
|
var OFFSET_INVERSE = { req: "state layers over inverted backgrounds", theming: "the offset ladder with its pole flipped, so inverted grounds keep the same rungs" };
|
|
2869
2893
|
var SHADOW = { req: "drop shadows" };
|
|
2894
|
+
var OPACITY_RUNG = { req: "translucency weight for any color", theming: "a bare number; a translucent row composes its color with one rung" };
|
|
2895
|
+
var OPACITY_STATE = { req: "translucency weight for any color; the state layer weight of the non-text stop over any ground", theming: "a bare number; a translucent row composes its color with one rung" };
|
|
2870
2896
|
var PLANE = (req) => ({ req, theming: "aliased to the gray ramp" });
|
|
2871
2897
|
var LINK = (state, contrast) => ({
|
|
2872
2898
|
req: "links" + state,
|
|
@@ -2888,8 +2914,17 @@ var SYSTEM = {
|
|
|
2888
2914
|
// ("companion" reworded 2026-08-28: compani-ON fed the "on" flood)
|
|
2889
2915
|
"system/abs-alt": { req: "identity seed reference", theming: "the theme\u2019s paired input, as given" },
|
|
2890
2916
|
"system/alpha/transparent": { req: "aliased off-states" },
|
|
2891
|
-
//
|
|
2892
|
-
|
|
2917
|
+
// the OPACITY LADDER: bare numbers a translucent color row composes with (the
|
|
2918
|
+
// scrim is the absolute black at the top rung). No conformance line of its own:
|
|
2919
|
+
// the state-layer claim rides the non-text stop these weights apply to
|
|
2920
|
+
"system/opacity/004": OPACITY_RUNG,
|
|
2921
|
+
"system/opacity/008": OPACITY_STATE,
|
|
2922
|
+
"system/opacity/012": OPACITY_STATE,
|
|
2923
|
+
"system/opacity/016": OPACITY_STATE,
|
|
2924
|
+
"system/opacity/024": OPACITY_STATE,
|
|
2925
|
+
"system/opacity/032": OPACITY_STATE,
|
|
2926
|
+
"system/opacity/048": OPACITY_RUNG,
|
|
2927
|
+
"system/opacity/064": OPACITY_RUNG,
|
|
2893
2928
|
// ("fills" reworded 2026-08-18: fill became a label word and is foreign here;
|
|
2894
2929
|
// "on-color" reworded 2026-08-28: the bare on fed the "on" flood)
|
|
2895
2930
|
"system/alpha/ink": { req: "soft text pole for quiet CTAs" },
|
|
@@ -2897,7 +2932,7 @@ var SYSTEM = {
|
|
|
2897
2932
|
"system/alpha/away-from-bg/08": OFFSET,
|
|
2898
2933
|
"system/alpha/away-from-bg/16": OFFSET,
|
|
2899
2934
|
// the inverse ladder: the offset rungs with the pole flipped per mode, for
|
|
2900
|
-
//
|
|
2935
|
+
// chalks and edges over inverted backgrounds ("interaction" avoided in the
|
|
2901
2936
|
// bodies: acti-ON would feed the "on" flood)
|
|
2902
2937
|
"system/alpha/toward-bg/06": OFFSET_INVERSE,
|
|
2903
2938
|
"system/alpha/toward-bg/08": OFFSET_INVERSE,
|
|
@@ -2964,15 +2999,15 @@ function checkCollision(brand, signalScale, signalDef, mode) {
|
|
|
2964
2999
|
collides: hueGate && deltaE <= threshold
|
|
2965
3000
|
};
|
|
2966
3001
|
}
|
|
2967
|
-
var
|
|
3002
|
+
var HUE_COLLISION_CHALK_DEG = 15;
|
|
2968
3003
|
var HUE_COLLISION_MIN_V = 0.5;
|
|
2969
3004
|
var SECONDARY_NOTE_MIN_V = HUE_NOISE_C / VIVID_C;
|
|
2970
|
-
var
|
|
2971
|
-
function
|
|
3005
|
+
var CHALK_STOPS = [3, 4, 5, 6, 7];
|
|
3006
|
+
function chalkProximity(brand, sig, mode) {
|
|
2972
3007
|
const b = mode === "light" ? brand.light : brand.dark;
|
|
2973
3008
|
const s = mode === "light" ? sig.light : sig.dark;
|
|
2974
3009
|
let dH = Infinity, dE = Infinity;
|
|
2975
|
-
for (const k of
|
|
3010
|
+
for (const k of CHALK_STOPS) {
|
|
2976
3011
|
const bs = b.find((x2) => x2.stop === k), ss = s.find((x2) => x2.stop === k);
|
|
2977
3012
|
if (!bs || !ss) continue;
|
|
2978
3013
|
dH = Math.min(dH, hueDistance(bs.H, ss.H));
|
|
@@ -2981,15 +3016,15 @@ function highlighterProximity(brand, sig, mode) {
|
|
|
2981
3016
|
return { dH, dE };
|
|
2982
3017
|
}
|
|
2983
3018
|
function checkHueCollision(brand, signalScale, signalDef, opts) {
|
|
2984
|
-
const l =
|
|
2985
|
-
const d =
|
|
3019
|
+
const l = chalkProximity(brand, signalScale, "light");
|
|
3020
|
+
const d = chalkProximity(brand, signalScale, "dark");
|
|
2986
3021
|
const vividness = Math.min(1, brand.brandC / VIVID_C);
|
|
2987
3022
|
return {
|
|
2988
3023
|
signal: signalDef.name,
|
|
2989
|
-
|
|
2990
|
-
|
|
3024
|
+
dHueChalk: { light: l.dH, dark: d.dH },
|
|
3025
|
+
chalkDeltaE: { light: l.dE, dark: d.dE },
|
|
2991
3026
|
vividness,
|
|
2992
|
-
collides: vividness >= (opts?.minV ?? HUE_COLLISION_MIN_V) && Math.min(l.dH, d.dH) <=
|
|
3027
|
+
collides: vividness >= (opts?.minV ?? HUE_COLLISION_MIN_V) && Math.min(l.dH, d.dH) <= HUE_COLLISION_CHALK_DEG
|
|
2993
3028
|
};
|
|
2994
3029
|
}
|
|
2995
3030
|
var YELLOW_SPLIT_H = 96;
|
|
@@ -3382,7 +3417,7 @@ function resolveTheme(input) {
|
|
|
3382
3417
|
for (const def of SIGNALS2) {
|
|
3383
3418
|
if (adoptedForSecondary.has(def.name)) continue;
|
|
3384
3419
|
const h = checkHueCollision(scale2, effectiveOf(def.name), def, { minV: SECONDARY_NOTE_MIN_V });
|
|
3385
|
-
if (h.collides) out.push(wording(def.name, Math.min(h.
|
|
3420
|
+
if (h.collides) out.push(wording(def.name, Math.min(h.chalkDeltaE.light, h.chalkDeltaE.dark)));
|
|
3386
3421
|
}
|
|
3387
3422
|
return out;
|
|
3388
3423
|
};
|
|
@@ -3432,7 +3467,7 @@ function resolveTheme(input) {
|
|
|
3432
3467
|
derived: true,
|
|
3433
3468
|
notes: [
|
|
3434
3469
|
`secondary derived from the brand color (default model, seed ${liftedHex})`,
|
|
3435
|
-
...signalNotesFor(scale2, (name, dE) => `derived secondary sits on the ${name} signal's hue (
|
|
3470
|
+
...signalNotesFor(scale2, (name, dE) => `derived secondary sits on the ${name} signal's hue (chalk \u0394E ${dE.toFixed(3)}) \u2014 it tracks the brand color`)
|
|
3436
3471
|
],
|
|
3437
3472
|
distinctness: ctaDistinctness(primary.scale, scale2)
|
|
3438
3473
|
},
|
|
@@ -3457,7 +3492,7 @@ function resolveTheme(input) {
|
|
|
3457
3492
|
derived: false,
|
|
3458
3493
|
notes: [
|
|
3459
3494
|
`secondary keeps your color through the ramp; the cta is a tint of it (tint seed ${tintedHex})`,
|
|
3460
|
-
...signalNotesFor(scale2, (name, dE) => `secondary sits on the ${name} signal's hue (
|
|
3495
|
+
...signalNotesFor(scale2, (name, dE) => `secondary sits on the ${name} signal's hue (chalk \u0394E ${dE.toFixed(3)}) \u2014 it tracks your color`)
|
|
3461
3496
|
],
|
|
3462
3497
|
distinctness: distinctness2
|
|
3463
3498
|
},
|
|
@@ -3469,7 +3504,7 @@ function resolveTheme(input) {
|
|
|
3469
3504
|
const scale = rSec.scale;
|
|
3470
3505
|
const level = "standard";
|
|
3471
3506
|
mergeSecondarySignals(scale, input.secondaryHex);
|
|
3472
|
-
const secNotes = signalNotesFor(scale, (name, dE) => `secondary reads close to the ${name} signal (
|
|
3507
|
+
const secNotes = signalNotesFor(scale, (name, dE) => `secondary reads close to the ${name} signal (chalk \u0394E ${dE.toFixed(3)}) \u2014 your color ships untouched`);
|
|
3473
3508
|
const distinctness = ctaDistinctness(primary.scale, scale);
|
|
3474
3509
|
if (distinctness.close)
|
|
3475
3510
|
notes.push(`secondary reads close to the primary (\u0394E ${Math.min(distinctness.light, distinctness.dark).toFixed(2)}) \u2014 consider a more distinct color`);
|
|
@@ -3514,21 +3549,29 @@ var OFFSET_ALPHAS = { 6: 0.06, 8: 0.08, 16: 0.16 };
|
|
|
3514
3549
|
var ctaBorderRung = (prefix) => prefix === CSS_NEUTRAL ? 8 : prefix === CSS_SECONDARY ? 6 : 16;
|
|
3515
3550
|
var offsetLeafName = (rung) => String(rung).padStart(2, "0");
|
|
3516
3551
|
var offsetVarName = (rung) => `--alpha-away-from-bg-${offsetLeafName(rung)}`;
|
|
3552
|
+
var OPACITY_RUNGS = { 4: 0.04, 8: 0.08, 12: 0.12, 16: 0.16, 24: 0.24, 32: 0.32, 48: 0.48, 64: 0.64 };
|
|
3553
|
+
var INTERACTION_RUNGS = [8, 12, 16, 24, 32];
|
|
3554
|
+
var opacityLeafName = (rung) => String(rung).padStart(3, "0");
|
|
3555
|
+
var opacityVarName = (rung) => `--opacity-${opacityLeafName(rung)}`;
|
|
3556
|
+
var opacityTokenPath = (rung) => SYSTEM_LEAF.OPACITY[opacityLeafName(rung)];
|
|
3517
3557
|
var SHADOW_ALPHAS = {
|
|
3518
|
-
4: { light:
|
|
3519
|
-
8: { light:
|
|
3520
|
-
12: { light:
|
|
3558
|
+
4: { light: OPACITY_RUNGS[4], dark: OPACITY_RUNGS[32] },
|
|
3559
|
+
8: { light: OPACITY_RUNGS[8], dark: OPACITY_RUNGS[48] },
|
|
3560
|
+
12: { light: OPACITY_RUNGS[12], dark: OPACITY_RUNGS[64] }
|
|
3521
3561
|
};
|
|
3522
|
-
var SCRIM_ALPHA =
|
|
3562
|
+
var SCRIM_ALPHA = OPACITY_RUNGS[64];
|
|
3523
3563
|
var DISABLED_OPACITY = 0.38;
|
|
3524
3564
|
var TRANSPARENT_VAR = "--alpha-transparent";
|
|
3525
3565
|
var offsetRgba = (rung, mode) => mode === "light" ? `rgba(0, 0, 0, ${OFFSET_ALPHAS[rung]})` : `rgba(255, 255, 255, ${OFFSET_ALPHAS[rung]})`;
|
|
3526
3566
|
var alphaRootVars = (mode) => [
|
|
3527
3567
|
` ${TRANSPARENT_VAR}: transparent;`,
|
|
3568
|
+
// the opacity ladder, mode-invariant like transparent; repeated in the dark block to
|
|
3569
|
+
// keep the set together
|
|
3570
|
+
...Object.keys(OPACITY_RUNGS).map(Number).map((r) => ` ${opacityVarName(r)}: ${OPACITY_RUNGS[r]};`),
|
|
3528
3571
|
...Object.keys(OFFSET_ALPHAS).map(Number).sort((a, b) => a - b).map((r) => ` ${offsetVarName(r)}: ${offsetRgba(r, mode)};`),
|
|
3529
3572
|
// the toward-bg ladder (the 2026-08-29 inverse ladder, renamed 2026-08-31): the
|
|
3530
3573
|
// same rungs, pole flipped per mode — state layers for INVERTED grounds (never
|
|
3531
|
-
// called a "
|
|
3574
|
+
// called a "chalk" — that word is the tinted band's). One spelling with the Figma
|
|
3532
3575
|
// row: --alpha-toward-bg-06 ↔ system/alpha/toward-bg/06.
|
|
3533
3576
|
...Object.keys(OFFSET_ALPHAS).map(Number).sort((a, b) => a - b).map((r) => ` --alpha-toward-bg-${offsetLeafName(r)}: ${offsetRgba(r, mode === "light" ? "dark" : "light")};`)
|
|
3534
3577
|
];
|
|
@@ -3540,7 +3583,7 @@ function ctaNeedsBorder(s, mode, page) {
|
|
|
3540
3583
|
if (!page) return false;
|
|
3541
3584
|
return ctaPageLc(s, mode, page) < CTA_BORDER_LC_FLOOR;
|
|
3542
3585
|
}
|
|
3543
|
-
function brandKindBody(prefix, s, mode, page) {
|
|
3586
|
+
function brandKindBody(prefix, s, mode, page, on = prefix === CSS_NEUTRAL ? "soft" : "solid") {
|
|
3544
3587
|
const stops = mode === "light" ? s.light : s.dark;
|
|
3545
3588
|
const f = ctaFamilyOf(s, mode);
|
|
3546
3589
|
const onCta = mode === "light" ? s.onFillTextIsWhite : s.onFillTextIsWhiteDark;
|
|
@@ -3555,14 +3598,14 @@ function brandKindBody(prefix, s, mode, page) {
|
|
|
3555
3598
|
` --${prefix}-stamp-fill-pressed: ${stopHex(f.ctaPressed)};`,
|
|
3556
3599
|
` --${prefix}-stamp-edge: var(${border ? offsetVarName(ctaBorderRung(prefix)) : TRANSPARENT_VAR});`,
|
|
3557
3600
|
// the SOFT on-cta (owner 2026-08-04: "neutral cta on's should also be the alpha"): the
|
|
3558
|
-
// neutral's cta is the scale-fed
|
|
3601
|
+
// neutral's cta is the scale-fed CHALK-level fill, the system's other quiet cta, so its
|
|
3559
3602
|
// button text takes the pole AT ALPHA like the default-model secondary's — composited
|
|
3560
3603
|
// over whatever state the fill is in, so hover/pressed carry their own legibility. Same
|
|
3561
3604
|
// register both families (SOFT_ON_CTA_ALPHA), so both alias the ONE system/alpha/ink
|
|
3562
|
-
// primitive.
|
|
3563
|
-
//
|
|
3564
|
-
//
|
|
3565
|
-
|
|
3605
|
+
// primitive. The neutral's is unconditional (the parameter default); the secondary's is
|
|
3606
|
+
// the caller's per-mode verdict. Loud fills (brand, signals, the cta escape) keep the
|
|
3607
|
+
// solid pole.
|
|
3608
|
+
on === "soft" ? ` --${prefix}-stamp-on: rgba(${onCta ? "255, 255, 255" : "0, 0, 0"}, ${SOFT_ON_CTA_ALPHA[mode]});` : ` --${prefix}-stamp-on: ${onColor(onCta)};`
|
|
3566
3609
|
];
|
|
3567
3610
|
}
|
|
3568
3611
|
function ctaFamilyOf(s, mode) {
|
|
@@ -3629,7 +3672,8 @@ function signalsCss(contrastProfile) {
|
|
|
3629
3672
|
// the system alpha variables every family's cta-border aliases (owner 2026-07-29) — this is
|
|
3630
3673
|
// the engine's one global :root, the CSS counterpart of the Figma side's system/alpha/* rows.
|
|
3631
3674
|
// Emitted in BOTH blocks because the offset rungs are scheme-DIVERGENT (black in light,
|
|
3632
|
-
// white in dark); --alpha-transparent
|
|
3675
|
+
// white in dark); --alpha-transparent and the opacity ladder (mode-invariant) repeat
|
|
3676
|
+
// harmlessly and keep the set together.
|
|
3633
3677
|
...alphaRootVars("light"),
|
|
3634
3678
|
...lightBlocks,
|
|
3635
3679
|
`}`,
|
|
@@ -3678,8 +3722,9 @@ function brandCss(slug, displayName, r, secondary, noteSuffix = "", neutralLevel
|
|
|
3678
3722
|
alias("stamp-on")
|
|
3679
3723
|
];
|
|
3680
3724
|
};
|
|
3681
|
-
const
|
|
3682
|
-
const
|
|
3725
|
+
const secondaryOn = (mode) => secondary && secondaryStyle !== "outline" && softOnCtaPasses(secondary, mode) ? "soft" : "solid";
|
|
3726
|
+
const secondaryLight = secondary ? brandKindBody(CSS_SECONDARY, secondary, "light", page.light, secondaryOn("light")) : mirrorBody(CSS_SECONDARY, "light");
|
|
3727
|
+
const secondaryDark = secondary ? brandKindBody(CSS_SECONDARY, secondary, "dark", page.dark, secondaryOn("dark")) : mirrorBody(CSS_SECONDARY, "dark");
|
|
3683
3728
|
const brandIdentity = ` --${CSS_BRAND}-identity: ${scale.identityHex};`;
|
|
3684
3729
|
const secondaryIdentity = secondary ? ` --${CSS_SECONDARY}-identity: ${secondary.identityHex};` : ` --${CSS_SECONDARY}-identity: var(--${CSS_BRAND}-identity);`;
|
|
3685
3730
|
const p0hex = (s, fallback) => s ? stopHex(s) : fallback;
|
|
@@ -3713,12 +3758,6 @@ function brandCss(slug, displayName, r, secondary, noteSuffix = "", neutralLevel
|
|
|
3713
3758
|
` --${CSS_BRAND}-stamp-on: ${onColor(esc.onFillIsWhite)};`
|
|
3714
3759
|
];
|
|
3715
3760
|
};
|
|
3716
|
-
const softOnCta = (mode) => {
|
|
3717
|
-
if (!secondary || secondaryStyle === "outline") return [];
|
|
3718
|
-
if (!softOnCtaPasses(secondary, mode)) return [];
|
|
3719
|
-
const white = mode === "light" ? secondary.onFillTextIsWhite : secondary.onFillTextIsWhiteDark;
|
|
3720
|
-
return [` --${CSS_SECONDARY}-stamp-on: rgba(${white ? "255, 255, 255" : "0, 0, 0"}, ${SOFT_ON_CTA_ALPHA[mode]});`];
|
|
3721
|
-
};
|
|
3722
3761
|
const outline = (mode) => {
|
|
3723
3762
|
if (secondaryStyle !== "outline" || !secondary) return [];
|
|
3724
3763
|
const s8 = (mode === "light" ? secondary.light : secondary.dark).find((s) => s.stop === 8);
|
|
@@ -3762,7 +3801,6 @@ function brandCss(slug, displayName, r, secondary, noteSuffix = "", neutralLevel
|
|
|
3762
3801
|
...linkInverse("light"),
|
|
3763
3802
|
brandIdentity,
|
|
3764
3803
|
...secondaryLight,
|
|
3765
|
-
...softOnCta("light"),
|
|
3766
3804
|
...outline("light"),
|
|
3767
3805
|
secondaryIdentity,
|
|
3768
3806
|
...brandKindBody(CSS_NEUTRAL, nScale, "light", page.light),
|
|
@@ -3775,7 +3813,6 @@ function brandCss(slug, displayName, r, secondary, noteSuffix = "", neutralLevel
|
|
|
3775
3813
|
...link("dark"),
|
|
3776
3814
|
...linkInverse("dark"),
|
|
3777
3815
|
...secondaryDark,
|
|
3778
|
-
...softOnCta("dark"),
|
|
3779
3816
|
...outline("dark"),
|
|
3780
3817
|
...brandKindBody(CSS_NEUTRAL, nScale, "dark", page.dark),
|
|
3781
3818
|
...effOverrides.flatMap((o) => brandKindBody(SIGNAL_EMIT_NAME[o.name], o.scale, "dark", page.dark)),
|
|
@@ -3957,7 +3994,8 @@ function themeToFigma(r, input) {
|
|
|
3957
3994
|
if (tok && "$type" in tok) putLeaf(g2, path, tok);
|
|
3958
3995
|
}
|
|
3959
3996
|
putLeaf(g2, SYSTEM_LEAF.ALPHA.TRANSPARENT, pole(true, 0));
|
|
3960
|
-
|
|
3997
|
+
for (const rung of Object.keys(OPACITY_RUNGS).map(Number))
|
|
3998
|
+
putLeaf(g2, opacityTokenPath(rung), { $type: "number", $value: OPACITY_RUNGS[rung] });
|
|
3961
3999
|
putLeaf(g2, SYSTEM_LEAF.ALPHA.INK, pole(mode === "dark", SOFT_ON_CTA_ALPHA[mode]));
|
|
3962
4000
|
putLeaf(g2, SYSTEM_LEAF.ALPHA.AWAY_FROM_BG_06, OFFSET_TOKEN(6, mode));
|
|
3963
4001
|
putLeaf(g2, SYSTEM_LEAF.ALPHA.AWAY_FROM_BG_08, OFFSET_TOKEN(8, mode));
|