@memberjunction/theme-engine 0.0.0 → 5.50.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 +50 -43
- package/dist/color.d.ts +37 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +130 -0
- package/dist/color.js.map +1 -0
- package/dist/contrast.d.ts +49 -0
- package/dist/contrast.d.ts.map +1 -0
- package/dist/contrast.js +67 -0
- package/dist/contrast.js.map +1 -0
- package/dist/derive.d.ts +68 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +238 -0
- package/dist/derive.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/logo.d.ts +74 -0
- package/dist/logo.d.ts.map +1 -0
- package/dist/logo.js +137 -0
- package/dist/logo.js.map +1 -0
- package/dist/ramps.d.ts +39 -0
- package/dist/ramps.d.ts.map +1 -0
- package/dist/ramps.js +103 -0
- package/dist/ramps.js.map +1 -0
- package/dist/seeds.d.ts +53 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +41 -0
- package/dist/seeds.js.map +1 -0
- package/package.json +28 -7
package/README.md
CHANGED
|
@@ -1,45 +1,52 @@
|
|
|
1
1
|
# @memberjunction/theme-engine
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
3
|
+
Framework-agnostic derivation of the full MemberJunction `--mj-*` design-token
|
|
4
|
+
contract from a small set of brand **seeds**. Zero runtime dependencies, so the same
|
|
5
|
+
code runs in the Angular theme builder (live preview) and in a future server-side
|
|
6
|
+
overlay endpoint.
|
|
7
|
+
|
|
8
|
+
A theme is a **brand**; light/dark is the user's mode layered under it. The entity
|
|
9
|
+
stores ~8 seeds — not tokens — and the full contract (OKLCH ramps, semantic tokens,
|
|
10
|
+
state families, dark re-point, chart palette, shadows, radii) is derived at load.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { derive, emitOverlayCss, MJ_DEFAULT_SEEDS } from '@memberjunction/theme-engine';
|
|
16
|
+
|
|
17
|
+
const theme = derive({ primary: '#7c3aed', accent: '#22d3ee', radius: 12 });
|
|
18
|
+
|
|
19
|
+
theme.tokens.light; // resolved semantic tokens (hex) for light mode
|
|
20
|
+
theme.tokens.dark; // resolved semantic tokens (hex) for dark mode
|
|
21
|
+
theme.contrast; // per-mode + hover WCAG report, with clamp suggestions
|
|
22
|
+
emitOverlayCss('acme', theme); // CSS scoped to [data-theme-overlay="acme"]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Seeds
|
|
26
|
+
|
|
27
|
+
| Seed | Meaning | Default |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `primary` | primary brand hue anchor (→ brand-500) | required |
|
|
30
|
+
| `accent` | accent hue anchor (→ accent-400) | `primary` |
|
|
31
|
+
| `tertiary` | tertiary hue anchor (→ tertiary-500) | `accent` |
|
|
32
|
+
| `neutralChroma` | brand-hue bleed into the gray stack, OKLCH chroma 0..~0.08 (G2) | `0.037` |
|
|
33
|
+
| `vibrancy` | global saturation multiplier for brand ramps | `1` |
|
|
34
|
+
| `radius` | base corner radius in px → `--mj-radius-md` | `8` |
|
|
35
|
+
| `depth` | brand-shadow intensity 0..1 (G3) | `1` |
|
|
36
|
+
| `fontFamily` / `fontFamilyMono` | type stacks (G1) | MJ Inter / JetBrains Mono |
|
|
37
|
+
| `vizPalette` | explicit categorical chart palette override (G4) | derived from brand hue |
|
|
38
|
+
|
|
39
|
+
## How derivation works
|
|
40
|
+
|
|
41
|
+
Each brand family (brand / accent / tertiary / neutral) carries a **measured OKLCH
|
|
42
|
+
shape** lifted from MJ's own default ramps. A seed supplies the hue and a chroma scale
|
|
43
|
+
relative to the family anchor; the shape is re-hued and its chroma rescaled. Feeding
|
|
44
|
+
`MJ_DEFAULT_SEEDS` therefore reproduces MJ's `_tokens.scss` within ~0.03 OKLab ΔE
|
|
45
|
+
(verified by the test suite), while any other hue inherits MJ's proven, perceptually
|
|
46
|
+
uniform lightness structure.
|
|
47
|
+
|
|
48
|
+
The emitted overlay only redefines the **primitive** ramps (plus viz, shadow, font,
|
|
49
|
+
radius). Every semantic token and the entire `[data-theme="dark"]` block in
|
|
50
|
+
`_tokens.scss` are `var(--mj-color-*)` references, so overriding primitives cascades
|
|
51
|
+
into both modes automatically — the dark re-point and hover families come for free,
|
|
52
|
+
with no parallel token mapping that could drift from the base stylesheet.
|
package/dist/color.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Color math for the theme engine: sRGB <-> OKLab <-> OKLCH
|
|
3
|
+
* conversions (Bjorn Ottosson's OKLab), gamut clamping, and WCAG 2.x contrast.
|
|
4
|
+
* Dependency-free so the derivation module stays importable in any runtime.
|
|
5
|
+
* @module @memberjunction/theme-engine
|
|
6
|
+
*/
|
|
7
|
+
/** A color in the OKLCH space. `l` 0..1, `c` >= 0 (chroma), `h` 0..360 degrees. */
|
|
8
|
+
export interface OKLCH {
|
|
9
|
+
l: number;
|
|
10
|
+
c: number;
|
|
11
|
+
h: number;
|
|
12
|
+
}
|
|
13
|
+
/** Parse `#rgb` / `#rrggbb` into 0..255 channels. Throws on anything else. */
|
|
14
|
+
export declare function parseHex(hex: string): {
|
|
15
|
+
r: number;
|
|
16
|
+
g: number;
|
|
17
|
+
b: number;
|
|
18
|
+
};
|
|
19
|
+
/** Convert a hex color to OKLCH. */
|
|
20
|
+
export declare function hexToOKLCH(hex: string): OKLCH;
|
|
21
|
+
/**
|
|
22
|
+
* Convert OKLCH to a hex string. If the color falls outside the sRGB gamut we
|
|
23
|
+
* reduce chroma toward the achromatic axis (preserving L and H) until it fits,
|
|
24
|
+
* then hard-clamp any residual channel error. This keeps hues perceptually
|
|
25
|
+
* stable instead of the hue-shifting that naive per-channel clamping causes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function oklchToHex(color: OKLCH): string;
|
|
28
|
+
/** WCAG 2.x relative luminance (0..1) of a hex color. */
|
|
29
|
+
export declare function relativeLuminance(hex: string): number;
|
|
30
|
+
/** WCAG 2.x contrast ratio (1..21) between two hex colors. */
|
|
31
|
+
export declare function contrastRatio(a: string, b: string): number;
|
|
32
|
+
/** Perceptual distance (Euclidean in OKLab) between two hex colors. */
|
|
33
|
+
export declare function deltaEOK(a: string, b: string): number;
|
|
34
|
+
/** Mix two hex colors in linear-light space; `t` 0..1 is the weight of `b`.
|
|
35
|
+
* A non-finite `t` is treated as 0 (returns `a`) rather than emitting "#NaN…". */
|
|
36
|
+
export declare function mixHex(a: string, b: string, t: number): string;
|
|
37
|
+
//# sourceMappingURL=color.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../src/color.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,mFAAmF;AACnF,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAWD,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAOzE;AA6CD,oCAAoC;AACpC,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAM7C;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAiC/C;AAED,yDAAyD;AACzD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED,8DAA8D;AAC9D,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1D;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED;mFACmF;AACnF,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAO9D"}
|
package/dist/color.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Color math for the theme engine: sRGB <-> OKLab <-> OKLCH
|
|
3
|
+
* conversions (Bjorn Ottosson's OKLab), gamut clamping, and WCAG 2.x contrast.
|
|
4
|
+
* Dependency-free so the derivation module stays importable in any runtime.
|
|
5
|
+
* @module @memberjunction/theme-engine
|
|
6
|
+
*/
|
|
7
|
+
const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
|
|
8
|
+
/** Parse `#rgb` / `#rrggbb` into 0..255 channels. Throws on anything else. */
|
|
9
|
+
export function parseHex(hex) {
|
|
10
|
+
const m = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(hex.trim());
|
|
11
|
+
if (!m)
|
|
12
|
+
throw new Error(`Not a hex color: "${hex}"`);
|
|
13
|
+
let h = m[1];
|
|
14
|
+
if (h.length === 3)
|
|
15
|
+
h = h.split('').map((c) => c + c).join('');
|
|
16
|
+
const n = parseInt(h, 16);
|
|
17
|
+
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
18
|
+
}
|
|
19
|
+
const toHex2 = (n) => Math.round(clamp01(n) * 255).toString(16).padStart(2, '0');
|
|
20
|
+
/** sRGB gamma-encoded channel (0..1) -> linear. */
|
|
21
|
+
const srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
22
|
+
/** Linear channel (0..1) -> sRGB gamma-encoded. */
|
|
23
|
+
const linearToSrgb = (c) => c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
24
|
+
function hexToLinear(hex) {
|
|
25
|
+
const { r, g, b } = parseHex(hex);
|
|
26
|
+
return { r: srgbToLinear(r / 255), g: srgbToLinear(g / 255), b: srgbToLinear(b / 255) };
|
|
27
|
+
}
|
|
28
|
+
function linearToOKLab(rgb) {
|
|
29
|
+
const l = 0.4122214708 * rgb.r + 0.5363325363 * rgb.g + 0.0514459929 * rgb.b;
|
|
30
|
+
const m = 0.2119034982 * rgb.r + 0.6806995451 * rgb.g + 0.1073969566 * rgb.b;
|
|
31
|
+
const s = 0.0883024619 * rgb.r + 0.2817188376 * rgb.g + 0.6299787005 * rgb.b;
|
|
32
|
+
const l_ = Math.cbrt(l);
|
|
33
|
+
const m_ = Math.cbrt(m);
|
|
34
|
+
const s_ = Math.cbrt(s);
|
|
35
|
+
return {
|
|
36
|
+
L: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
|
|
37
|
+
a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
|
|
38
|
+
b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function okLabToLinear(L, a, b) {
|
|
42
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
43
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
44
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
45
|
+
const l = l_ * l_ * l_;
|
|
46
|
+
const m = m_ * m_ * m_;
|
|
47
|
+
const s = s_ * s_ * s_;
|
|
48
|
+
return {
|
|
49
|
+
r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
50
|
+
g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
51
|
+
b: -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Convert a hex color to OKLCH. */
|
|
55
|
+
export function hexToOKLCH(hex) {
|
|
56
|
+
const { L, a, b } = linearToOKLab(hexToLinear(hex));
|
|
57
|
+
const c = Math.sqrt(a * a + b * b);
|
|
58
|
+
let h = (Math.atan2(b, a) * 180) / Math.PI;
|
|
59
|
+
if (h < 0)
|
|
60
|
+
h += 360;
|
|
61
|
+
return { l: L, c, h };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Convert OKLCH to a hex string. If the color falls outside the sRGB gamut we
|
|
65
|
+
* reduce chroma toward the achromatic axis (preserving L and H) until it fits,
|
|
66
|
+
* then hard-clamp any residual channel error. This keeps hues perceptually
|
|
67
|
+
* stable instead of the hue-shifting that naive per-channel clamping causes.
|
|
68
|
+
*/
|
|
69
|
+
export function oklchToHex(color) {
|
|
70
|
+
// Public surface: non-finite components (NaN/±Infinity from upstream math on
|
|
71
|
+
// degenerate inputs) would otherwise propagate into a "#NaNNaNNaN" string.
|
|
72
|
+
const l = Number.isFinite(color.l) ? color.l : 0;
|
|
73
|
+
let c = Number.isFinite(color.c) ? Math.max(0, color.c) : 0;
|
|
74
|
+
const h = Number.isFinite(color.h) ? color.h : 0;
|
|
75
|
+
const hRad = (h * Math.PI) / 180;
|
|
76
|
+
const cos = Math.cos(hRad);
|
|
77
|
+
const sin = Math.sin(hRad);
|
|
78
|
+
const fits = (chroma) => {
|
|
79
|
+
const rgb = okLabToLinear(l, chroma * cos, chroma * sin);
|
|
80
|
+
const eps = 1e-4;
|
|
81
|
+
if (rgb.r >= -eps && rgb.r <= 1 + eps && rgb.g >= -eps && rgb.g <= 1 + eps && rgb.b >= -eps && rgb.b <= 1 + eps) {
|
|
82
|
+
return rgb;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
let rgb = fits(c);
|
|
87
|
+
if (!rgb) {
|
|
88
|
+
// Binary-search the largest in-gamut chroma at this L/H.
|
|
89
|
+
let lo = 0;
|
|
90
|
+
let hi = c;
|
|
91
|
+
for (let i = 0; i < 24; i++) {
|
|
92
|
+
const mid = (lo + hi) / 2;
|
|
93
|
+
if (fits(mid))
|
|
94
|
+
lo = mid;
|
|
95
|
+
else
|
|
96
|
+
hi = mid;
|
|
97
|
+
}
|
|
98
|
+
c = lo;
|
|
99
|
+
rgb = okLabToLinear(l, c * cos, c * sin);
|
|
100
|
+
}
|
|
101
|
+
return `#${toHex2(linearToSrgb(rgb.r))}${toHex2(linearToSrgb(rgb.g))}${toHex2(linearToSrgb(rgb.b))}`;
|
|
102
|
+
}
|
|
103
|
+
/** WCAG 2.x relative luminance (0..1) of a hex color. */
|
|
104
|
+
export function relativeLuminance(hex) {
|
|
105
|
+
const { r, g, b } = parseHex(hex);
|
|
106
|
+
const lin = (v) => srgbToLinear(v / 255);
|
|
107
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
108
|
+
}
|
|
109
|
+
/** WCAG 2.x contrast ratio (1..21) between two hex colors. */
|
|
110
|
+
export function contrastRatio(a, b) {
|
|
111
|
+
const la = relativeLuminance(a);
|
|
112
|
+
const lb = relativeLuminance(b);
|
|
113
|
+
const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
|
|
114
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
115
|
+
}
|
|
116
|
+
/** Perceptual distance (Euclidean in OKLab) between two hex colors. */
|
|
117
|
+
export function deltaEOK(a, b) {
|
|
118
|
+
const A = linearToOKLab(hexToLinear(a));
|
|
119
|
+
const B = linearToOKLab(hexToLinear(b));
|
|
120
|
+
return Math.sqrt((A.L - B.L) ** 2 + (A.a - B.a) ** 2 + (A.b - B.b) ** 2);
|
|
121
|
+
}
|
|
122
|
+
/** Mix two hex colors in linear-light space; `t` 0..1 is the weight of `b`.
|
|
123
|
+
* A non-finite `t` is treated as 0 (returns `a`) rather than emitting "#NaN…". */
|
|
124
|
+
export function mixHex(a, b, t) {
|
|
125
|
+
const A = hexToLinear(a);
|
|
126
|
+
const B = hexToLinear(b);
|
|
127
|
+
const k = clamp01(Number.isFinite(t) ? t : 0);
|
|
128
|
+
return `#${toHex2(linearToSrgb(A.r + (B.r - A.r) * k))}${toHex2(linearToSrgb(A.g + (B.g - A.g) * k))}${toHex2(linearToSrgb(A.b + (B.b - A.b) * k))}`;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=color.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color.js","sourceRoot":"","sources":["../src/color.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Per-mode accessibility validation (proposal 8 / 16.4.4). Runs WCAG
|
|
3
|
+
* contrast on the brand-critical foreground/background pairs — including hover
|
|
4
|
+
* states — and, when a pair fails, computes a concrete clamp: the nearest lightness
|
|
5
|
+
* for the offending color that reaches the target ratio.
|
|
6
|
+
* @module @memberjunction/theme-engine
|
|
7
|
+
*/
|
|
8
|
+
/** WCAG AA thresholds. Normal text 4.5:1; large text / UI affordances 3:1. */
|
|
9
|
+
export declare const AA_TEXT = 4.5;
|
|
10
|
+
export declare const AA_LARGE = 3;
|
|
11
|
+
/** One evaluated contrast pair. */
|
|
12
|
+
export interface ContrastCheck {
|
|
13
|
+
/** Human-readable pair name, e.g. "text-on-primary (hover)". */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Foreground hex. */
|
|
16
|
+
fg: string;
|
|
17
|
+
/** Background hex. */
|
|
18
|
+
bg: string;
|
|
19
|
+
/** Achieved ratio (1..21). */
|
|
20
|
+
ratio: number;
|
|
21
|
+
/** Required ratio for this pair. */
|
|
22
|
+
required: number;
|
|
23
|
+
passes: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* When failing, a suggested replacement for `clampTarget` ('bg' or 'fg') that
|
|
26
|
+
* reaches `required` by shifting lightness, or null if unreachable in gamut.
|
|
27
|
+
*/
|
|
28
|
+
suggestion?: {
|
|
29
|
+
adjust: 'fg' | 'bg';
|
|
30
|
+
hex: string;
|
|
31
|
+
} | null;
|
|
32
|
+
}
|
|
33
|
+
/** Full report across both user modes. `passes` is true only if every pair passes. */
|
|
34
|
+
export interface ContrastReport {
|
|
35
|
+
light: ContrastCheck[];
|
|
36
|
+
dark: ContrastCheck[];
|
|
37
|
+
passes: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** A pair to evaluate; `clamp` names which side to move if it fails. */
|
|
40
|
+
export interface PairSpec {
|
|
41
|
+
name: string;
|
|
42
|
+
fg: string;
|
|
43
|
+
bg: string;
|
|
44
|
+
required: number;
|
|
45
|
+
clamp: 'fg' | 'bg';
|
|
46
|
+
}
|
|
47
|
+
/** Build and evaluate the report from resolved per-mode token maps. */
|
|
48
|
+
export declare function buildContrastReport(light: Record<string, string>, dark: Record<string, string>): ContrastReport;
|
|
49
|
+
//# sourceMappingURL=contrast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contrast.d.ts","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,8EAA8E;AAC9E,eAAO,MAAM,OAAO,MAAM,CAAC;AAC3B,eAAO,MAAM,QAAQ,IAAI,CAAC;AAE1B,mCAAmC;AACnC,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,sBAAsB;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,sBAAsB;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE;QAAE,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1D;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,wEAAwE;AACxE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,IAAI,GAAG,IAAI,CAAC;CACpB;AA2CD,uEAAuE;AACvE,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,cAAc,CAahB"}
|
package/dist/contrast.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Per-mode accessibility validation (proposal 8 / 16.4.4). Runs WCAG
|
|
3
|
+
* contrast on the brand-critical foreground/background pairs — including hover
|
|
4
|
+
* states — and, when a pair fails, computes a concrete clamp: the nearest lightness
|
|
5
|
+
* for the offending color that reaches the target ratio.
|
|
6
|
+
* @module @memberjunction/theme-engine
|
|
7
|
+
*/
|
|
8
|
+
import { contrastRatio, hexToOKLCH, oklchToHex, relativeLuminance } from './color.js';
|
|
9
|
+
/** WCAG AA thresholds. Normal text 4.5:1; large text / UI affordances 3:1. */
|
|
10
|
+
export const AA_TEXT = 4.5;
|
|
11
|
+
export const AA_LARGE = 3;
|
|
12
|
+
/**
|
|
13
|
+
* Search OKLCH lightness of `color` (away from `other`'s luminance) for the nearest
|
|
14
|
+
* value that reaches `target` contrast against `other`. Returns null if unreachable.
|
|
15
|
+
*/
|
|
16
|
+
function clampLightness(color, other, target) {
|
|
17
|
+
const base = hexToOKLCH(color);
|
|
18
|
+
// Move away from the other color's luminance: if we're already the darker of the
|
|
19
|
+
// pair, get darker still; otherwise get lighter. (Moving toward it lowers contrast.)
|
|
20
|
+
const goDarker = relativeLuminance(color) <= relativeLuminance(other);
|
|
21
|
+
const steps = 100;
|
|
22
|
+
for (let i = 1; i <= steps; i++) {
|
|
23
|
+
const l = goDarker ? base.l - (base.l * i) / steps : base.l + ((1 - base.l) * i) / steps;
|
|
24
|
+
const candidate = oklchToHex({ l, c: base.c, h: base.h });
|
|
25
|
+
if (contrastRatio(candidate, other) >= target)
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** Evaluate one list of pairs. */
|
|
31
|
+
function evaluate(pairs) {
|
|
32
|
+
return pairs.map((p) => {
|
|
33
|
+
const ratio = contrastRatio(p.fg, p.bg);
|
|
34
|
+
const passes = ratio >= p.required;
|
|
35
|
+
const check = {
|
|
36
|
+
name: p.name,
|
|
37
|
+
fg: p.fg,
|
|
38
|
+
bg: p.bg,
|
|
39
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
40
|
+
required: p.required,
|
|
41
|
+
passes,
|
|
42
|
+
};
|
|
43
|
+
if (!passes) {
|
|
44
|
+
const moving = p.clamp === 'fg' ? p.fg : p.bg;
|
|
45
|
+
const against = p.clamp === 'fg' ? p.bg : p.fg;
|
|
46
|
+
const hex = clampLightness(moving, against, p.required);
|
|
47
|
+
check.suggestion = hex ? { adjust: p.clamp, hex } : null;
|
|
48
|
+
}
|
|
49
|
+
return check;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Build and evaluate the report from resolved per-mode token maps. */
|
|
53
|
+
export function buildContrastReport(light, dark) {
|
|
54
|
+
const pairsFor = (t) => [
|
|
55
|
+
{ name: 'text-on-primary', fg: t['--mj-brand-on-primary'], bg: t['--mj-brand-primary'], required: AA_LARGE, clamp: 'bg' },
|
|
56
|
+
{ name: 'text-on-primary (hover)', fg: t['--mj-brand-on-primary'], bg: t['--mj-brand-primary-hover'], required: AA_LARGE, clamp: 'bg' },
|
|
57
|
+
{ name: 'text-on-accent', fg: t['--mj-brand-on-accent'], bg: t['--mj-brand-accent'], required: AA_LARGE, clamp: 'bg' },
|
|
58
|
+
{ name: 'text-primary-on-surface', fg: t['--mj-text-primary'], bg: t['--mj-bg-surface'], required: AA_TEXT, clamp: 'fg' },
|
|
59
|
+
{ name: 'text-secondary-on-surface', fg: t['--mj-text-secondary'], bg: t['--mj-bg-surface'], required: AA_TEXT, clamp: 'fg' },
|
|
60
|
+
{ name: 'link-on-surface', fg: t['--mj-text-link'], bg: t['--mj-bg-surface'], required: AA_TEXT, clamp: 'fg' },
|
|
61
|
+
{ name: 'link-on-surface (hover)', fg: t['--mj-text-link-hover'], bg: t['--mj-bg-surface'], required: AA_TEXT, clamp: 'fg' },
|
|
62
|
+
];
|
|
63
|
+
const l = evaluate(pairsFor(light));
|
|
64
|
+
const d = evaluate(pairsFor(dark));
|
|
65
|
+
return { light: l, dark: d, passes: [...l, ...d].every((c) => c.passes) };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=contrast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contrast.js","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"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"}
|
package/dist/derive.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The single derivation function shared by save, live preview, and
|
|
3
|
+
* validation (org theming decision #4). Given ~8 brand seeds it produces the full
|
|
4
|
+
* --mj-* token contract: generated OKLCH ramps, resolved light/dark semantic maps,
|
|
5
|
+
* a brand-derived chart palette (G4), brand-tinted shadows (G3), the font token (G1),
|
|
6
|
+
* shape radii, an a11y contrast report, and the minimal CSS overlay to emit at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Overlay strategy: the emitted overlay only redefines the PRIMITIVE ramps (plus viz,
|
|
9
|
+
* shadow, font, radius). Every semantic token and the entire [data-theme="dark"] block
|
|
10
|
+
* in _tokens.scss are `var(--mj-color-*)` references, so overriding primitives cascades
|
|
11
|
+
* into both modes automatically — the dark re-point and hover families come for free,
|
|
12
|
+
* with no risk of a parallel mapping diverging from the base stylesheet.
|
|
13
|
+
* @module @memberjunction/theme-engine
|
|
14
|
+
*/
|
|
15
|
+
import { ContrastReport } from './contrast.js';
|
|
16
|
+
import { ResolvedSeeds, ThemeSeeds } from './seeds.js';
|
|
17
|
+
/** Fully derived theme: ramps, resolved per-mode maps, overlay vars, and a11y report. */
|
|
18
|
+
export interface DerivedTheme {
|
|
19
|
+
seeds: ResolvedSeeds;
|
|
20
|
+
/** Generated primitive ramps, keyed by full CSS var name (`--mj-color-brand-500`). */
|
|
21
|
+
primitives: Record<string, string>;
|
|
22
|
+
/** Resolved semantic tokens as hex, per user mode (the M2 token cache / preview feed). */
|
|
23
|
+
tokens: {
|
|
24
|
+
light: Record<string, string>;
|
|
25
|
+
dark: Record<string, string>;
|
|
26
|
+
};
|
|
27
|
+
/** Minimal token overrides emitted into the runtime overlay. */
|
|
28
|
+
overlayVars: Record<string, string>;
|
|
29
|
+
contrast: ContrastReport;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Derive the full theme from brand seeds. Feeding {@link MJ_DEFAULT_SEEDS} reproduces
|
|
33
|
+
* MJ's default _tokens.scss within perceptual tolerance.
|
|
34
|
+
*/
|
|
35
|
+
export declare function derive(seeds?: ThemeSeeds): DerivedTheme;
|
|
36
|
+
/** Optional advanced-customization layer applied on top of the seed-derived tokens. */
|
|
37
|
+
export interface OverlayOptions {
|
|
38
|
+
/** Per-token overrides merged over the derived overlay vars (override wins). Keys are
|
|
39
|
+
* `--mj-*` custom-property names; applied in the same mode-agnostic overlay block. */
|
|
40
|
+
overrides?: Record<string, string> | null;
|
|
41
|
+
/** Raw CSS appended after the token block, auto-scoped under the overlay selector via
|
|
42
|
+
* native CSS nesting so it only applies when this theme is active. Escape hatch. */
|
|
43
|
+
customCss?: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Serialize a derived theme into overlay CSS scoped to `[data-theme-overlay="<id>"]`.
|
|
47
|
+
* A single block suffices: it overrides primitives that _tokens.scss's semantic and
|
|
48
|
+
* dark blocks reference, so both modes re-point automatically.
|
|
49
|
+
*
|
|
50
|
+
* The optional advanced layer (decision: seed-first, with an escape hatch) is applied
|
|
51
|
+
* last: `overrides` merge over the derived vars; `customCss` is appended, wrapped in the
|
|
52
|
+
* overlay selector so raw rules stay scoped to this theme.
|
|
53
|
+
*/
|
|
54
|
+
export declare function emitOverlayCss(themeId: string, derived: DerivedTheme, options?: OverlayOptions): string;
|
|
55
|
+
/**
|
|
56
|
+
* Scope raw custom CSS under `selector`, but hoist at-rules that MUST live at the
|
|
57
|
+
* stylesheet top level (`@keyframes`, `@font-face`, `@property`) out of the wrapper —
|
|
58
|
+
* CSS nesting forbids them inside a style rule, so nesting them would silently break
|
|
59
|
+
* animations/fonts. Everything else is wrapped so it only applies to this theme.
|
|
60
|
+
*
|
|
61
|
+
* `@import` is STRIPPED entirely, not hoisted: a scoped theme overlay has no
|
|
62
|
+
* legitimate need to pull another stylesheet, an org-wide `@import` of a remote URL
|
|
63
|
+
* is a cross-origin request from every user's session (data-exfiltration surface),
|
|
64
|
+
* and a hoisted `@import` would be dead anyway — CSS ignores `@import` after any
|
|
65
|
+
* other rule, and the overlay Blob always begins with the token block.
|
|
66
|
+
*/
|
|
67
|
+
export declare function emitScopedCustomCss(selector: string, css: string): string;
|
|
68
|
+
//# sourceMappingURL=derive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.d.ts","sourceRoot":"","sources":["../src/derive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAuB,cAAc,EAAE,MAAM,eAAe,CAAC;AAUpE,OAAO,EAAoB,aAAa,EAAgB,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvF,yFAAyF;AACzF,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,aAAa,CAAC;IACrB,sFAAsF;IACtF,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,0FAA0F;IAC1F,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IACxE,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAwFD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,GAAE,UAA6B,GAAG,YAAY,CA4DzE;AAED,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;2FACuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC1C;yFACqF;IACrF,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,CAW3G;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQzE"}
|