@aglyn/shared-ui-theme 1.0.0-beta.143

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.
Files changed (78) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +51 -0
  4. package/src/index.d.ts +34 -0
  5. package/src/index.js +50 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/host-theme-provider.d.ts +84 -0
  8. package/src/lib/components/host-theme-provider.js +146 -0
  9. package/src/lib/components/host-theme-provider.js.map +1 -0
  10. package/src/lib/components/theme-css-var-provider.d.ts +44 -0
  11. package/src/lib/components/theme-css-var-provider.js +79 -0
  12. package/src/lib/components/theme-css-var-provider.js.map +1 -0
  13. package/src/lib/console.theme.d.ts +34 -0
  14. package/src/lib/console.theme.js +598 -0
  15. package/src/lib/console.theme.js.map +1 -0
  16. package/src/lib/constants.d.ts +72 -0
  17. package/src/lib/constants.js +85 -0
  18. package/src/lib/constants.js.map +1 -0
  19. package/src/lib/hocs/create-with-emotion-client-cache.d.ts +30 -0
  20. package/src/lib/hocs/create-with-emotion-client-cache.js +53 -0
  21. package/src/lib/hocs/create-with-emotion-client-cache.js.map +1 -0
  22. package/src/lib/hocs/create-with-theme-provider.d.ts +86 -0
  23. package/src/lib/hocs/create-with-theme-provider.js +231 -0
  24. package/src/lib/hocs/create-with-theme-provider.js.map +1 -0
  25. package/src/lib/tenant.theme.d.ts +58 -0
  26. package/src/lib/tenant.theme.js +282 -0
  27. package/src/lib/tenant.theme.js.map +1 -0
  28. package/src/lib/theme.types.d.ts +26 -0
  29. package/src/lib/theme.types.js +18 -0
  30. package/src/lib/theme.types.js.map +1 -0
  31. package/src/lib/util/accent-text.d.ts +187 -0
  32. package/src/lib/util/accent-text.js +277 -0
  33. package/src/lib/util/accent-text.js.map +1 -0
  34. package/src/lib/util/accessible-shade.d.ts +61 -0
  35. package/src/lib/util/accessible-shade.js +117 -0
  36. package/src/lib/util/accessible-shade.js.map +1 -0
  37. package/src/lib/util/color-scheme-hint.d.ts +68 -0
  38. package/src/lib/util/color-scheme-hint.js +69 -0
  39. package/src/lib/util/color-scheme-hint.js.map +1 -0
  40. package/src/lib/util/create-responsive-theme.d.ts +71 -0
  41. package/src/lib/util/create-responsive-theme.js +287 -0
  42. package/src/lib/util/create-responsive-theme.js.map +1 -0
  43. package/src/lib/util/emotion-cache.d.ts +81 -0
  44. package/src/lib/util/emotion-cache.js +79 -0
  45. package/src/lib/util/emotion-cache.js.map +1 -0
  46. package/src/lib/util/generate-component-class-keys.d.ts +41 -0
  47. package/src/lib/util/generate-component-class-keys.js +53 -0
  48. package/src/lib/util/generate-component-class-keys.js.map +1 -0
  49. package/src/lib/util/host-theme.d.ts +92 -0
  50. package/src/lib/util/host-theme.js +340 -0
  51. package/src/lib/util/host-theme.js.map +1 -0
  52. package/src/lib/util/layered-emotion-cache.d.ts +78 -0
  53. package/src/lib/util/layered-emotion-cache.js +90 -0
  54. package/src/lib/util/layered-emotion-cache.js.map +1 -0
  55. package/src/lib/util/merge-sx-props.d.ts +22 -0
  56. package/src/lib/util/merge-sx-props.js +27 -0
  57. package/src/lib/util/merge-sx-props.js.map +1 -0
  58. package/src/lib/util/scheme-route-segment.d.ts +95 -0
  59. package/src/lib/util/scheme-route-segment.js +101 -0
  60. package/src/lib/util/scheme-route-segment.js.map +1 -0
  61. package/src/lib/util/theme-editor-defaults.d.ts +41 -0
  62. package/src/lib/util/theme-editor-defaults.js +49 -0
  63. package/src/lib/util/theme-editor-defaults.js.map +1 -0
  64. package/src/lib/util/theme-editor-fields.d.ts +315 -0
  65. package/src/lib/util/theme-editor-fields.js +714 -0
  66. package/src/lib/util/theme-editor-fields.js.map +1 -0
  67. package/src/lib/util/theme-mode-cookie.d.ts +48 -0
  68. package/src/lib/util/theme-mode-cookie.js +48 -0
  69. package/src/lib/util/theme-mode-cookie.js.map +1 -0
  70. package/src/vendor/emotion.d.ts +18 -0
  71. package/src/vendor/emotion.js +22 -0
  72. package/src/vendor/emotion.js.map +1 -0
  73. package/src/vendor/jss.d.ts +18 -0
  74. package/src/vendor/jss.js +20 -0
  75. package/src/vendor/jss.js.map +1 -0
  76. package/src/vendor/mui.d.ts +282 -0
  77. package/src/vendor/mui.js +20 -0
  78. package/src/vendor/mui.js.map +1 -0
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ export { };
17
+
18
+ //# sourceMappingURL=theme.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/shared/ui/theme/src/lib/theme.types.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport interface IActionStates {\n main: string\n hover: string\n active: string\n focus: string\n}\n\nexport interface ColorPropOverrides {\n tertiary: true\n surface: true\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GASD,WAGC"}
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { Palette, Theme } from '../../vendor/mui';
18
+ /**
19
+ * The palette slot that carries "this accent, rendered AS TEXT on the
20
+ * scheme's own surfaces" (AGL-1293 / AGL-1297).
21
+ *
22
+ * `dark` is deliberately overloaded rather than a new slot being invented:
23
+ * `ensureAccessibleShades` already guarantees the value of this slot clears
24
+ * {@link AA_TEXT_CONTRAST} against `background.default` and
25
+ * `background.paper` in whichever direction the scheme needs (darker in
26
+ * light, LIGHTER in dark), so every tenant palette carries an accessible
27
+ * value here whether or not its author thought about one. A second slot
28
+ * would have to be whitelisted in `host-theme.ts`, authored per host, and
29
+ * kept in sync — three new ways for a palette to be missing it.
30
+ *
31
+ * `console.theme.ts` authors the shade explicitly for both schemes rather
32
+ * than relying on that repair, so the value a link resolves to is readable
33
+ * in the palette instead of recomputed at boot.
34
+ */
35
+ export declare const ACCENT_TEXT_SHADE: "dark";
36
+ /** WCAG AA contrast bar for non-text UI (borders, icon fills, indicators). */
37
+ export declare const AA_NON_TEXT_CONTRAST = 3;
38
+ /**
39
+ * The accent-as-text color for `color`, as a value safe to hand to a
40
+ * `styleOverrides` root.
41
+ *
42
+ * Wired into `MuiButton`'s `--variant-textColor` / `--variant-outlinedColor`
43
+ * and `MuiLink`'s `color` in `console.theme.ts`. Those three are the places
44
+ * MUI paints an accent at normal text size, where `main` owes 4.5:1 and the
45
+ * brand blue delivers 2.43:1 on white. Everything that paints the accent as
46
+ * a FILL, a BORDER or an INDICATOR — `--variant-containedBg`,
47
+ * `--variant-outlinedBorder`, the Tabs indicator — keeps `main`: those owe
48
+ * 3:1, and the brand color is meant to be seen there.
49
+ *
50
+ * Returns a CSS variable reference (`var(--mui-palette-primary-dark)`) on a
51
+ * CSS-vars theme and a literal on a single-mode theme. That distinction is
52
+ * load-bearing: `components` are evaluated ONCE against the root theme, so
53
+ * baking a light-scheme literal would freeze it into dark mode.
54
+ *
55
+ * @param theme the active MUI theme (CSS-vars or single-mode)
56
+ * @param color a palette key — `'primary'`, `'error'`, … Anything without a
57
+ * PaletteColor shape (`'inherit'`, `'textPrimary'`, `undefined`) returns
58
+ * `undefined`.
59
+ */
60
+ export declare function accentTextColor(theme: Theme | undefined, color: string | undefined): string | undefined;
61
+ /**
62
+ * The accent shade a FILLED surface can paint under the accent's own ink —
63
+ * the counterpart to {@link accentTextColor}, and a different value for a
64
+ * different question (AGL-2704).
65
+ *
66
+ * {@link ACCENT_TEXT_SHADE} answers "this accent, READ ON the page", so it
67
+ * inverts direction per scheme: darker than `main` in light, LIGHTER in
68
+ * dark. That makes it wrong as a fill in exactly one scheme. The console's
69
+ * `primary.dark` is `#0077ad` in light, where white on it measures 4.95:1,
70
+ * and `#4dc8ff` in dark, where the same white measures 1.91:1 — below the
71
+ * 2.43:1 the brand fill it replaced already scored.
72
+ *
73
+ * This shade answers "this accent, PAINTED UNDER its own `contrastText`"
74
+ * instead. The constraint is the ink, not the page, so the walk starts at
75
+ * `main` and darkens until the ink clears the bar — and the answer is the
76
+ * SAME in both schemes, which is the property a token reference needs. An
77
+ * `sx` value resolves against whichever scheme is active, so a fill token
78
+ * that only holds in one of them cannot be authored safely.
79
+ *
80
+ * Takes a resolved `Palette`, never `theme.vars`: the walk decomposes real
81
+ * colors, and a CSS-variable mirror hands it `var(--mui-palette-…)`.
82
+ *
83
+ * @param palette a resolved (non-`vars`) palette
84
+ * @param color a palette key — `'primary'`, `'error'`, …
85
+ * @param minContrast the bar the ink must clear. Default
86
+ * {@link AA_TEXT_CONTRAST}.
87
+ */
88
+ export declare function accentFillColor(palette: Palette | undefined, color: string | undefined, minContrast?: number): string | undefined;
89
+ /** One measured way a palette fails its accessibility contract. */
90
+ export type PaletteContrastViolation = {
91
+ /** Palette key — `'primary'`, `'error'`, … */
92
+ color: string;
93
+ /**
94
+ * `accentText` — the accent painted as text on the scheme's surfaces.
95
+ * `contrastText` — the foreground painted ON that accent.
96
+ * `accentFill` — that same foreground painted on the accent's TEXT shade,
97
+ * i.e. a surface filled with {@link ACCENT_TEXT_SHADE}. Off by default;
98
+ * see {@link AuditPaletteContrastOptions.roles}.
99
+ */
100
+ role: 'accentText' | 'contrastText' | 'accentFill';
101
+ /** The foreground colour that failed. */
102
+ value: string;
103
+ /** The background it failed against. */
104
+ against: string;
105
+ /** Measured WCAG ratio, 1–21. */
106
+ ratio: number;
107
+ /** The bar it had to clear. */
108
+ required: number;
109
+ /**
110
+ * Set only when this exact pairing matches a
111
+ * {@link DOCUMENTED_CONTRAST_EXCEPTIONS} entry: the human decision that
112
+ * knowingly accepts the measured ratio. Excluded from the default result;
113
+ * surfaced by `includeExempt`.
114
+ */
115
+ exemption?: string;
116
+ };
117
+ /**
118
+ * Pairings a human has knowingly SIGNED OFF below the bar — decided, not
119
+ * outstanding.
120
+ *
121
+ * The rest of what {@link auditPaletteContrast} reports is a FINDING: a
122
+ * measurement offered for a decision the account owner owns, not a defect queued for
123
+ * repair. Nothing in this module changes a rendered colour.
124
+ *
125
+ * This is not a suppression list and it must not become one. Each entry pins
126
+ * all four coordinates of one pairing — palette key, role, the exact
127
+ * foreground and the exact background — so it waives a decision, not a slot.
128
+ * Change the brand blue, change the white, or move the same white onto
129
+ * `secondary`, and none of them match: the audit reports it again, which is
130
+ * the point. A new entry needs a named person, a date, their words, and the
131
+ * measured ratio, the same as this one.
132
+ */
133
+ export declare const DOCUMENTED_CONTRAST_EXCEPTIONS: ReadonlyArray<{
134
+ color: string;
135
+ role: PaletteContrastViolation['role'];
136
+ value: string;
137
+ against: string;
138
+ reason: string;
139
+ }>;
140
+ export type AuditPaletteContrastOptions = {
141
+ /** Bar for foreground text. Default {@link AA_TEXT_CONTRAST}. */
142
+ minContrast?: number;
143
+ /** Restrict the audit to these palette keys. Default: all accents. */
144
+ colors?: ReadonlyArray<string>;
145
+ /**
146
+ * Include pairings matched by {@link DOCUMENTED_CONTRAST_EXCEPTIONS},
147
+ * carrying their `exemption`. Default `false`, so a signed-off decision does
148
+ * not read as an open defect. Pass `true` to see everything measured — the
149
+ * spec that proves the exception is still exactly 2.43:1 uses this, so the
150
+ * waiver documents a number rather than hiding one.
151
+ */
152
+ includeExempt?: boolean;
153
+ /**
154
+ * Which roles to measure. Default {@link DEFAULT_AUDITED_ROLES} — the two
155
+ * pairings every palette owes unconditionally.
156
+ *
157
+ * `accentFill` is opt-in because it is a question about a SURFACE, not
158
+ * about the palette: it asks what a fill painted with
159
+ * {@link ACCENT_TEXT_SHADE} does to the accent's own ink. In a dark scheme
160
+ * that shade is lighter than `main` by design, so every accent fails it —
161
+ * correctly, and uselessly for a palette that fills with nothing. Pass it
162
+ * where something actually does (AGL-2704: the marketing pricing page
163
+ * filled a nav CTA and a badge with `primary.dark`, which held in light at
164
+ * 4.95:1 and collapsed to 1.91:1 in dark).
165
+ */
166
+ roles?: ReadonlyArray<PaletteContrastViolation['role']>;
167
+ };
168
+ /** The roles {@link auditPaletteContrast} measures unless told otherwise. */
169
+ export declare const DEFAULT_AUDITED_ROLES: ReadonlyArray<PaletteContrastViolation['role']>;
170
+ /**
171
+ * Measures whether a palette actually keeps the AGL-1293 promise, rather
172
+ * than trusting that the derivation ran.
173
+ *
174
+ * This is the part that can go RED. `ensureAccessibleShades` only repairs
175
+ * shades it DERIVED — an explicitly authored `primary.dark`, or an accent
176
+ * whose AA bar is simply unreachable in the scheme's foreground direction
177
+ * (walking toward white on a mid-grey page tops out at 3.95:1), passes
178
+ * straight through. Those palettes are exactly the ones a customer can build
179
+ * in the theme editor, so the contract has to be checked, not assumed.
180
+ *
181
+ * Returns every violation with its measured ratio; an empty array means the
182
+ * palette is AA-clean for text.
183
+ */
184
+ export declare function auditPaletteContrast(palette: Palette | undefined, options?: AuditPaletteContrastOptions): PaletteContrastViolation[];
185
+ /** Human-readable one-liner per violation, for test output and dev warnings. */
186
+ export declare function formatPaletteContrastViolation(violation: PaletteContrastViolation): string;
187
+ export default accentTextColor;
@@ -0,0 +1,277 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { AA_TEXT_CONTRAST, accessibleShade, contrastRatio } from "./accessible-shade.js";
18
+ /**
19
+ * The palette slot that carries "this accent, rendered AS TEXT on the
20
+ * scheme's own surfaces" (AGL-1293 / AGL-1297).
21
+ *
22
+ * `dark` is deliberately overloaded rather than a new slot being invented:
23
+ * `ensureAccessibleShades` already guarantees the value of this slot clears
24
+ * {@link AA_TEXT_CONTRAST} against `background.default` and
25
+ * `background.paper` in whichever direction the scheme needs (darker in
26
+ * light, LIGHTER in dark), so every tenant palette carries an accessible
27
+ * value here whether or not its author thought about one. A second slot
28
+ * would have to be whitelisted in `host-theme.ts`, authored per host, and
29
+ * kept in sync — three new ways for a palette to be missing it.
30
+ *
31
+ * `console.theme.ts` authors the shade explicitly for both schemes rather
32
+ * than relying on that repair, so the value a link resolves to is readable
33
+ * in the palette instead of recomputed at boot.
34
+ */ export const ACCENT_TEXT_SHADE = 'dark';
35
+ /** WCAG AA contrast bar for non-text UI (borders, icon fills, indicators). */ export const AA_NON_TEXT_CONTRAST = 3;
36
+ /**
37
+ * The accent-as-text color for `color`, as a value safe to hand to a
38
+ * `styleOverrides` root.
39
+ *
40
+ * Wired into `MuiButton`'s `--variant-textColor` / `--variant-outlinedColor`
41
+ * and `MuiLink`'s `color` in `console.theme.ts`. Those three are the places
42
+ * MUI paints an accent at normal text size, where `main` owes 4.5:1 and the
43
+ * brand blue delivers 2.43:1 on white. Everything that paints the accent as
44
+ * a FILL, a BORDER or an INDICATOR — `--variant-containedBg`,
45
+ * `--variant-outlinedBorder`, the Tabs indicator — keeps `main`: those owe
46
+ * 3:1, and the brand color is meant to be seen there.
47
+ *
48
+ * Returns a CSS variable reference (`var(--mui-palette-primary-dark)`) on a
49
+ * CSS-vars theme and a literal on a single-mode theme. That distinction is
50
+ * load-bearing: `components` are evaluated ONCE against the root theme, so
51
+ * baking a light-scheme literal would freeze it into dark mode.
52
+ *
53
+ * @param theme the active MUI theme (CSS-vars or single-mode)
54
+ * @param color a palette key — `'primary'`, `'error'`, … Anything without a
55
+ * PaletteColor shape (`'inherit'`, `'textPrimary'`, `undefined`) returns
56
+ * `undefined`.
57
+ */ export function accentTextColor(theme, color) {
58
+ var _theme_vars;
59
+ var _source_palette;
60
+ if (!theme || !color) return undefined;
61
+ // `theme.vars` is the CSS-variable mirror of the palette; prefer it so the
62
+ // emitted value is a `var()` that follows the scheme.
63
+ const source = (_theme_vars = theme.vars) != null ? _theme_vars : theme;
64
+ const paletteColor = source == null ? void 0 : (_source_palette = source.palette) == null ? void 0 : _source_palette[color];
65
+ if (!paletteColor || typeof paletteColor !== 'object') return undefined;
66
+ const accent = paletteColor[ACCENT_TEXT_SHADE];
67
+ return typeof accent === 'string' ? accent : undefined;
68
+ }
69
+ /**
70
+ * The accent shade a FILLED surface can paint under the accent's own ink —
71
+ * the counterpart to {@link accentTextColor}, and a different value for a
72
+ * different question (AGL-2704).
73
+ *
74
+ * {@link ACCENT_TEXT_SHADE} answers "this accent, READ ON the page", so it
75
+ * inverts direction per scheme: darker than `main` in light, LIGHTER in
76
+ * dark. That makes it wrong as a fill in exactly one scheme. The console's
77
+ * `primary.dark` is `#0077ad` in light, where white on it measures 4.95:1,
78
+ * and `#4dc8ff` in dark, where the same white measures 1.91:1 — below the
79
+ * 2.43:1 the brand fill it replaced already scored.
80
+ *
81
+ * This shade answers "this accent, PAINTED UNDER its own `contrastText`"
82
+ * instead. The constraint is the ink, not the page, so the walk starts at
83
+ * `main` and darkens until the ink clears the bar — and the answer is the
84
+ * SAME in both schemes, which is the property a token reference needs. An
85
+ * `sx` value resolves against whichever scheme is active, so a fill token
86
+ * that only holds in one of them cannot be authored safely.
87
+ *
88
+ * Takes a resolved `Palette`, never `theme.vars`: the walk decomposes real
89
+ * colors, and a CSS-variable mirror hands it `var(--mui-palette-…)`.
90
+ *
91
+ * @param palette a resolved (non-`vars`) palette
92
+ * @param color a palette key — `'primary'`, `'error'`, …
93
+ * @param minContrast the bar the ink must clear. Default
94
+ * {@link AA_TEXT_CONTRAST}.
95
+ */ export function accentFillColor(palette, color, minContrast = AA_TEXT_CONTRAST) {
96
+ if (!palette || !color) return undefined;
97
+ const paletteColor = palette[color];
98
+ if (!paletteColor || typeof paletteColor !== 'object') return undefined;
99
+ const { main, contrastText } = paletteColor;
100
+ if (typeof main !== 'string' || typeof contrastText !== 'string') {
101
+ return undefined;
102
+ }
103
+ try {
104
+ return accessibleShade(main, [
105
+ contrastText
106
+ ], 'darken', {
107
+ minContrast
108
+ });
109
+ } catch (unused) {
110
+ // Unparseable color (a `color()` literal, an unresolved variable). A
111
+ // caller gets nothing rather than a guess, the same as `accentTextColor`.
112
+ return undefined;
113
+ }
114
+ }
115
+ /**
116
+ * Pairings a human has knowingly SIGNED OFF below the bar — decided, not
117
+ * outstanding.
118
+ *
119
+ * The rest of what {@link auditPaletteContrast} reports is a FINDING: a
120
+ * measurement offered for a decision the account owner owns, not a defect queued for
121
+ * repair. Nothing in this module changes a rendered colour.
122
+ *
123
+ * This is not a suppression list and it must not become one. Each entry pins
124
+ * all four coordinates of one pairing — palette key, role, the exact
125
+ * foreground and the exact background — so it waives a decision, not a slot.
126
+ * Change the brand blue, change the white, or move the same white onto
127
+ * `secondary`, and none of them match: the audit reports it again, which is
128
+ * the point. A new entry needs a named person, a date, their words, and the
129
+ * measured ratio, the same as this one.
130
+ */ export const DOCUMENTED_CONTRAST_EXCEPTIONS = [
131
+ {
132
+ // AGL-1293. Computing this slot for contrast resolves it to dark ink
133
+ // (8.65:1), which turns every filled primary button dark-on-blue. The
134
+ // tradeoff is to darken the brand or to keep it and accept sub-AA white,
135
+ // and the brand blue is fixed: white on `#00b0ff` is 2.43:1, below the
136
+ // 4.5:1 AA text bar and below the 3:1 non-text bar. Accepted knowingly.
137
+ //
138
+ // This is the ONE decided pairing. Everything else the audit reports —
139
+ // the five other authored sub-AA `contrastText` literals, and `#00b0ff`
140
+ // as text — is a finding, not a waiver, and stays undecided here.
141
+ color: 'primary',
142
+ role: 'contrastText',
143
+ value: '#FFFFFF',
144
+ against: '#00b0ff',
145
+ reason: 'AGL-1293 — the brand blue stays as authored, with white text (white on #00b0ff = 2.43:1, knowingly below AA)'
146
+ }
147
+ ];
148
+ /** Case-insensitive exact colour match; nothing fuzzy, nothing normalised away. */ function sameColor(a, b) {
149
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
150
+ }
151
+ /**
152
+ * The signed-off reason for this exact pairing, or `undefined`. Exact on all
153
+ * four coordinates — a near miss is not a match.
154
+ */ function documentedExemption(color, role, value, against) {
155
+ var _DOCUMENTED_CONTRAST_EXCEPTIONS_find;
156
+ return (_DOCUMENTED_CONTRAST_EXCEPTIONS_find = DOCUMENTED_CONTRAST_EXCEPTIONS.find((exception)=>exception.color === color && exception.role === role && sameColor(exception.value, value) && sameColor(exception.against, against))) == null ? void 0 : _DOCUMENTED_CONTRAST_EXCEPTIONS_find.reason;
157
+ }
158
+ const AUDITED_COLOR_KEYS = [
159
+ 'primary',
160
+ 'secondary',
161
+ 'tertiary',
162
+ 'error',
163
+ 'warning',
164
+ 'info',
165
+ 'success'
166
+ ];
167
+ /** The roles {@link auditPaletteContrast} measures unless told otherwise. */ export const DEFAULT_AUDITED_ROLES = [
168
+ 'accentText',
169
+ 'contrastText'
170
+ ];
171
+ /**
172
+ * Measures whether a palette actually keeps the AGL-1293 promise, rather
173
+ * than trusting that the derivation ran.
174
+ *
175
+ * This is the part that can go RED. `ensureAccessibleShades` only repairs
176
+ * shades it DERIVED — an explicitly authored `primary.dark`, or an accent
177
+ * whose AA bar is simply unreachable in the scheme's foreground direction
178
+ * (walking toward white on a mid-grey page tops out at 3.95:1), passes
179
+ * straight through. Those palettes are exactly the ones a customer can build
180
+ * in the theme editor, so the contract has to be checked, not assumed.
181
+ *
182
+ * Returns every violation with its measured ratio; an empty array means the
183
+ * palette is AA-clean for text.
184
+ */ export function auditPaletteContrast(palette, options = {}) {
185
+ var _palette_background, _palette_background1;
186
+ const { minContrast = AA_TEXT_CONTRAST, colors = AUDITED_COLOR_KEYS, includeExempt = false, roles = DEFAULT_AUDITED_ROLES } = options;
187
+ const measures = (role)=>roles.includes(role);
188
+ const violations = [];
189
+ /** Records a measured failure unless a documented decision waives it. */ const record = (violation)=>{
190
+ const exemption = documentedExemption(violation.color, violation.role, violation.value, violation.against);
191
+ if (exemption && !includeExempt) return;
192
+ violations.push(exemption ? _extends({}, violation, {
193
+ exemption
194
+ }) : violation);
195
+ };
196
+ if (!palette) return violations;
197
+ const indexed = palette;
198
+ const backgrounds = [
199
+ (_palette_background = palette.background) == null ? void 0 : _palette_background.default,
200
+ (_palette_background1 = palette.background) == null ? void 0 : _palette_background1.paper
201
+ ].filter((background)=>typeof background === 'string');
202
+ for (const key of colors){
203
+ const color = indexed[key];
204
+ if (!color || typeof color.main !== 'string') continue;
205
+ const accent = color[ACCENT_TEXT_SHADE];
206
+ for (const background of backgrounds){
207
+ if (!measures('accentText') || typeof accent !== 'string') continue;
208
+ try {
209
+ const ratio = contrastRatio(accent, background);
210
+ if (ratio < minContrast) {
211
+ record({
212
+ color: key,
213
+ role: 'accentText',
214
+ value: accent,
215
+ against: background,
216
+ ratio,
217
+ required: minContrast
218
+ });
219
+ }
220
+ } catch (unused) {
221
+ // Unparseable colour (CSS variable, color-mix()): not measurable
222
+ // here. Silence rather than a false violation — the caller audits a
223
+ // resolved palette, not a `vars` mirror.
224
+ }
225
+ }
226
+ if (typeof color.contrastText !== 'string') continue;
227
+ if (measures('contrastText')) {
228
+ try {
229
+ const ratio = contrastRatio(color.contrastText, color.main);
230
+ if (ratio < minContrast) {
231
+ record({
232
+ color: key,
233
+ role: 'contrastText',
234
+ value: color.contrastText,
235
+ against: color.main,
236
+ ratio,
237
+ required: minContrast
238
+ });
239
+ }
240
+ } catch (unused) {
241
+ // As above.
242
+ }
243
+ }
244
+ // The same ink, on a surface filled with the accent's TEXT shade. That
245
+ // shade is chosen to be READ ON the page, so in a dark scheme it sits
246
+ // lighter than `main` and cannot carry a light ink — which is invisible
247
+ // to the `contrastText` measure above, because that one only ever looks
248
+ // at `main`.
249
+ if (measures('accentFill') && typeof accent === 'string') {
250
+ try {
251
+ const ratio = contrastRatio(color.contrastText, accent);
252
+ if (ratio < minContrast) {
253
+ record({
254
+ color: key,
255
+ role: 'accentFill',
256
+ value: color.contrastText,
257
+ against: accent,
258
+ ratio,
259
+ required: minContrast
260
+ });
261
+ }
262
+ } catch (unused) {
263
+ // As above.
264
+ }
265
+ }
266
+ }
267
+ return violations;
268
+ }
269
+ /** Human-readable one-liner per violation, for test output and dev warnings. */ export function formatPaletteContrastViolation(violation) {
270
+ const measured = `${violation.color}.${violation.role} ${violation.value} on ${violation.against} is ${violation.ratio.toFixed(2)}:1, below ${violation.required}:1`;
271
+ // The waived line still carries its number, so a reader sees what was
272
+ // accepted rather than a bare "exempt".
273
+ return violation.exemption ? `${measured} — KNOWN EXCEPTION: ${violation.exemption}` : measured;
274
+ }
275
+ export default accentTextColor;
276
+
277
+ //# sourceMappingURL=accent-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/theme/src/lib/util/accent-text.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Palette, PaletteColor, Theme } from '../../vendor/mui'\nimport {\n AA_TEXT_CONTRAST,\n accessibleShade,\n contrastRatio,\n} from './accessible-shade'\n\n/**\n * The palette slot that carries \"this accent, rendered AS TEXT on the\n * scheme's own surfaces\" (AGL-1293 / AGL-1297).\n *\n * `dark` is deliberately overloaded rather than a new slot being invented:\n * `ensureAccessibleShades` already guarantees the value of this slot clears\n * {@link AA_TEXT_CONTRAST} against `background.default` and\n * `background.paper` in whichever direction the scheme needs (darker in\n * light, LIGHTER in dark), so every tenant palette carries an accessible\n * value here whether or not its author thought about one. A second slot\n * would have to be whitelisted in `host-theme.ts`, authored per host, and\n * kept in sync — three new ways for a palette to be missing it.\n *\n * `console.theme.ts` authors the shade explicitly for both schemes rather\n * than relying on that repair, so the value a link resolves to is readable\n * in the palette instead of recomputed at boot.\n */\nexport const ACCENT_TEXT_SHADE = 'dark' as const\n\n/** WCAG AA contrast bar for non-text UI (borders, icon fills, indicators). */\nexport const AA_NON_TEXT_CONTRAST = 3\n\n/**\n * The accent-as-text color for `color`, as a value safe to hand to a\n * `styleOverrides` root.\n *\n * Wired into `MuiButton`'s `--variant-textColor` / `--variant-outlinedColor`\n * and `MuiLink`'s `color` in `console.theme.ts`. Those three are the places\n * MUI paints an accent at normal text size, where `main` owes 4.5:1 and the\n * brand blue delivers 2.43:1 on white. Everything that paints the accent as\n * a FILL, a BORDER or an INDICATOR — `--variant-containedBg`,\n * `--variant-outlinedBorder`, the Tabs indicator — keeps `main`: those owe\n * 3:1, and the brand color is meant to be seen there.\n *\n * Returns a CSS variable reference (`var(--mui-palette-primary-dark)`) on a\n * CSS-vars theme and a literal on a single-mode theme. That distinction is\n * load-bearing: `components` are evaluated ONCE against the root theme, so\n * baking a light-scheme literal would freeze it into dark mode.\n *\n * @param theme the active MUI theme (CSS-vars or single-mode)\n * @param color a palette key — `'primary'`, `'error'`, … Anything without a\n * PaletteColor shape (`'inherit'`, `'textPrimary'`, `undefined`) returns\n * `undefined`.\n */\nexport function accentTextColor(\n theme: Theme | undefined,\n color: string | undefined,\n): string | undefined {\n if (!theme || !color) return undefined\n // `theme.vars` is the CSS-variable mirror of the palette; prefer it so the\n // emitted value is a `var()` that follows the scheme.\n const source = ((theme as unknown as { vars?: { palette?: unknown } }).vars ??\n theme) as { palette?: Record<string, unknown> }\n const paletteColor = source?.palette?.[color] as\n Record<string, string> | undefined\n if (!paletteColor || typeof paletteColor !== 'object') return undefined\n const accent = paletteColor[ACCENT_TEXT_SHADE]\n return typeof accent === 'string' ? accent : undefined\n}\n\n/**\n * The accent shade a FILLED surface can paint under the accent's own ink —\n * the counterpart to {@link accentTextColor}, and a different value for a\n * different question (AGL-2704).\n *\n * {@link ACCENT_TEXT_SHADE} answers \"this accent, READ ON the page\", so it\n * inverts direction per scheme: darker than `main` in light, LIGHTER in\n * dark. That makes it wrong as a fill in exactly one scheme. The console's\n * `primary.dark` is `#0077ad` in light, where white on it measures 4.95:1,\n * and `#4dc8ff` in dark, where the same white measures 1.91:1 — below the\n * 2.43:1 the brand fill it replaced already scored.\n *\n * This shade answers \"this accent, PAINTED UNDER its own `contrastText`\"\n * instead. The constraint is the ink, not the page, so the walk starts at\n * `main` and darkens until the ink clears the bar — and the answer is the\n * SAME in both schemes, which is the property a token reference needs. An\n * `sx` value resolves against whichever scheme is active, so a fill token\n * that only holds in one of them cannot be authored safely.\n *\n * Takes a resolved `Palette`, never `theme.vars`: the walk decomposes real\n * colors, and a CSS-variable mirror hands it `var(--mui-palette-…)`.\n *\n * @param palette a resolved (non-`vars`) palette\n * @param color a palette key — `'primary'`, `'error'`, …\n * @param minContrast the bar the ink must clear. Default\n * {@link AA_TEXT_CONTRAST}.\n */\nexport function accentFillColor(\n palette: Palette | undefined,\n color: string | undefined,\n minContrast: number = AA_TEXT_CONTRAST,\n): string | undefined {\n if (!palette || !color) return undefined\n const paletteColor = (palette as unknown as Record<string, unknown>)[\n color\n ] as Record<string, string> | undefined\n if (!paletteColor || typeof paletteColor !== 'object') return undefined\n const { main, contrastText } = paletteColor\n if (typeof main !== 'string' || typeof contrastText !== 'string') {\n return undefined\n }\n try {\n return accessibleShade(main, [contrastText], 'darken', { minContrast })\n } catch {\n // Unparseable color (a `color()` literal, an unresolved variable). A\n // caller gets nothing rather than a guess, the same as `accentTextColor`.\n return undefined\n }\n}\n\n/** One measured way a palette fails its accessibility contract. */\nexport type PaletteContrastViolation = {\n /** Palette key — `'primary'`, `'error'`, … */\n color: string\n /**\n * `accentText` — the accent painted as text on the scheme's surfaces.\n * `contrastText` — the foreground painted ON that accent.\n * `accentFill` — that same foreground painted on the accent's TEXT shade,\n * i.e. a surface filled with {@link ACCENT_TEXT_SHADE}. Off by default;\n * see {@link AuditPaletteContrastOptions.roles}.\n */\n role: 'accentText' | 'contrastText' | 'accentFill'\n /** The foreground colour that failed. */\n value: string\n /** The background it failed against. */\n against: string\n /** Measured WCAG ratio, 1–21. */\n ratio: number\n /** The bar it had to clear. */\n required: number\n /**\n * Set only when this exact pairing matches a\n * {@link DOCUMENTED_CONTRAST_EXCEPTIONS} entry: the human decision that\n * knowingly accepts the measured ratio. Excluded from the default result;\n * surfaced by `includeExempt`.\n */\n exemption?: string\n}\n\n/**\n * Pairings a human has knowingly SIGNED OFF below the bar — decided, not\n * outstanding.\n *\n * The rest of what {@link auditPaletteContrast} reports is a FINDING: a\n * measurement offered for a decision the account owner owns, not a defect queued for\n * repair. Nothing in this module changes a rendered colour.\n *\n * This is not a suppression list and it must not become one. Each entry pins\n * all four coordinates of one pairing — palette key, role, the exact\n * foreground and the exact background — so it waives a decision, not a slot.\n * Change the brand blue, change the white, or move the same white onto\n * `secondary`, and none of them match: the audit reports it again, which is\n * the point. A new entry needs a named person, a date, their words, and the\n * measured ratio, the same as this one.\n */\nexport const DOCUMENTED_CONTRAST_EXCEPTIONS: ReadonlyArray<{\n color: string\n role: PaletteContrastViolation['role']\n value: string\n against: string\n reason: string\n}> = [\n {\n // AGL-1293. Computing this slot for contrast resolves it to dark ink\n // (8.65:1), which turns every filled primary button dark-on-blue. The\n // tradeoff is to darken the brand or to keep it and accept sub-AA white,\n // and the brand blue is fixed: white on `#00b0ff` is 2.43:1, below the\n // 4.5:1 AA text bar and below the 3:1 non-text bar. Accepted knowingly.\n //\n // This is the ONE decided pairing. Everything else the audit reports —\n // the five other authored sub-AA `contrastText` literals, and `#00b0ff`\n // as text — is a finding, not a waiver, and stays undecided here.\n color: 'primary',\n role: 'contrastText',\n value: '#FFFFFF',\n against: '#00b0ff',\n reason:\n 'AGL-1293 — the brand blue stays as authored, with white text (white on #00b0ff = 2.43:1, knowingly below AA)',\n },\n]\n\n/** Case-insensitive exact colour match; nothing fuzzy, nothing normalised away. */\nfunction sameColor(a: string, b: string) {\n return a.trim().toLowerCase() === b.trim().toLowerCase()\n}\n\n/**\n * The signed-off reason for this exact pairing, or `undefined`. Exact on all\n * four coordinates — a near miss is not a match.\n */\nfunction documentedExemption(\n color: string,\n role: PaletteContrastViolation['role'],\n value: string,\n against: string,\n): string | undefined {\n return DOCUMENTED_CONTRAST_EXCEPTIONS.find(\n (exception) =>\n exception.color === color &&\n exception.role === role &&\n sameColor(exception.value, value) &&\n sameColor(exception.against, against),\n )?.reason\n}\n\nconst AUDITED_COLOR_KEYS = [\n 'primary',\n 'secondary',\n 'tertiary',\n 'error',\n 'warning',\n 'info',\n 'success',\n] as const\n\nexport type AuditPaletteContrastOptions = {\n /** Bar for foreground text. Default {@link AA_TEXT_CONTRAST}. */\n minContrast?: number\n /** Restrict the audit to these palette keys. Default: all accents. */\n colors?: ReadonlyArray<string>\n /**\n * Include pairings matched by {@link DOCUMENTED_CONTRAST_EXCEPTIONS},\n * carrying their `exemption`. Default `false`, so a signed-off decision does\n * not read as an open defect. Pass `true` to see everything measured — the\n * spec that proves the exception is still exactly 2.43:1 uses this, so the\n * waiver documents a number rather than hiding one.\n */\n includeExempt?: boolean\n /**\n * Which roles to measure. Default {@link DEFAULT_AUDITED_ROLES} — the two\n * pairings every palette owes unconditionally.\n *\n * `accentFill` is opt-in because it is a question about a SURFACE, not\n * about the palette: it asks what a fill painted with\n * {@link ACCENT_TEXT_SHADE} does to the accent's own ink. In a dark scheme\n * that shade is lighter than `main` by design, so every accent fails it —\n * correctly, and uselessly for a palette that fills with nothing. Pass it\n * where something actually does (AGL-2704: the marketing pricing page\n * filled a nav CTA and a badge with `primary.dark`, which held in light at\n * 4.95:1 and collapsed to 1.91:1 in dark).\n */\n roles?: ReadonlyArray<PaletteContrastViolation['role']>\n}\n\n/** The roles {@link auditPaletteContrast} measures unless told otherwise. */\nexport const DEFAULT_AUDITED_ROLES: ReadonlyArray<\n PaletteContrastViolation['role']\n> = ['accentText', 'contrastText']\n\n/**\n * Measures whether a palette actually keeps the AGL-1293 promise, rather\n * than trusting that the derivation ran.\n *\n * This is the part that can go RED. `ensureAccessibleShades` only repairs\n * shades it DERIVED — an explicitly authored `primary.dark`, or an accent\n * whose AA bar is simply unreachable in the scheme's foreground direction\n * (walking toward white on a mid-grey page tops out at 3.95:1), passes\n * straight through. Those palettes are exactly the ones a customer can build\n * in the theme editor, so the contract has to be checked, not assumed.\n *\n * Returns every violation with its measured ratio; an empty array means the\n * palette is AA-clean for text.\n */\nexport function auditPaletteContrast(\n palette: Palette | undefined,\n options: AuditPaletteContrastOptions = {},\n): PaletteContrastViolation[] {\n const {\n minContrast = AA_TEXT_CONTRAST,\n colors = AUDITED_COLOR_KEYS,\n includeExempt = false,\n roles = DEFAULT_AUDITED_ROLES,\n } = options\n const measures = (role: PaletteContrastViolation['role']) =>\n roles.includes(role)\n const violations: PaletteContrastViolation[] = []\n /** Records a measured failure unless a documented decision waives it. */\n const record = (violation: PaletteContrastViolation) => {\n const exemption = documentedExemption(\n violation.color,\n violation.role,\n violation.value,\n violation.against,\n )\n if (exemption && !includeExempt) return\n violations.push(exemption ? { ...violation, exemption } : violation)\n }\n if (!palette) return violations\n const indexed = palette as unknown as Record<string, PaletteColor | undefined>\n const backgrounds = [\n palette.background?.default,\n palette.background?.paper,\n ].filter((background): background is string => typeof background === 'string')\n\n for (const key of colors) {\n const color = indexed[key]\n if (!color || typeof color.main !== 'string') continue\n const accent = (color as unknown as Record<string, string>)[\n ACCENT_TEXT_SHADE\n ]\n for (const background of backgrounds) {\n if (!measures('accentText') || typeof accent !== 'string') continue\n try {\n const ratio = contrastRatio(accent, background)\n if (ratio < minContrast) {\n record({\n color: key,\n role: 'accentText',\n value: accent,\n against: background,\n ratio,\n required: minContrast,\n })\n }\n } catch {\n // Unparseable colour (CSS variable, color-mix()): not measurable\n // here. Silence rather than a false violation — the caller audits a\n // resolved palette, not a `vars` mirror.\n }\n }\n if (typeof color.contrastText !== 'string') continue\n if (measures('contrastText')) {\n try {\n const ratio = contrastRatio(color.contrastText, color.main)\n if (ratio < minContrast) {\n record({\n color: key,\n role: 'contrastText',\n value: color.contrastText,\n against: color.main,\n ratio,\n required: minContrast,\n })\n }\n } catch {\n // As above.\n }\n }\n // The same ink, on a surface filled with the accent's TEXT shade. That\n // shade is chosen to be READ ON the page, so in a dark scheme it sits\n // lighter than `main` and cannot carry a light ink — which is invisible\n // to the `contrastText` measure above, because that one only ever looks\n // at `main`.\n if (measures('accentFill') && typeof accent === 'string') {\n try {\n const ratio = contrastRatio(color.contrastText, accent)\n if (ratio < minContrast) {\n record({\n color: key,\n role: 'accentFill',\n value: color.contrastText,\n against: accent,\n ratio,\n required: minContrast,\n })\n }\n } catch {\n // As above.\n }\n }\n }\n return violations\n}\n\n/** Human-readable one-liner per violation, for test output and dev warnings. */\nexport function formatPaletteContrastViolation(\n violation: PaletteContrastViolation,\n): string {\n const measured = `${violation.color}.${violation.role} ${violation.value} on ${\n violation.against\n } is ${violation.ratio.toFixed(2)}:1, below ${violation.required}:1`\n // The waived line still carries its number, so a reader sees what was\n // accepted rather than a bare \"exempt\".\n return violation.exemption\n ? `${measured} — KNOWN EXCEPTION: ${violation.exemption}`\n : measured\n}\n\nexport default accentTextColor\n"],"names":["AA_TEXT_CONTRAST","accessibleShade","contrastRatio","ACCENT_TEXT_SHADE","AA_NON_TEXT_CONTRAST","accentTextColor","theme","color","source","undefined","vars","paletteColor","palette","accent","accentFillColor","minContrast","main","contrastText","DOCUMENTED_CONTRAST_EXCEPTIONS","role","value","against","reason","sameColor","a","b","trim","toLowerCase","documentedExemption","find","exception","AUDITED_COLOR_KEYS","DEFAULT_AUDITED_ROLES","auditPaletteContrast","options","colors","includeExempt","roles","measures","includes","violations","record","violation","exemption","push","indexed","backgrounds","background","default","paper","filter","key","ratio","required","formatPaletteContrastViolation","measured","toFixed"],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /** Ramp direction for a shade walk. */
18
+ export type ShadeDirection = 'darken' | 'lighten';
19
+ /** WCAG 2.x AA contrast bar for normal-size text. */
20
+ export declare const AA_TEXT_CONTRAST = 4.5;
21
+ export type AccessibleShadeOptions = {
22
+ /** Contrast every background must clear. Default {@link AA_TEXT_CONTRAST}. */
23
+ minContrast?: number;
24
+ /** HSL lightness moved per iteration (0–1 scale). Default 0.01. */
25
+ step?: number;
26
+ /**
27
+ * Iteration cap. The default covers the full lightness range at the
28
+ * default step with headroom, so the walk always terminates at a pole
29
+ * before hitting it in practice.
30
+ */
31
+ maxSteps?: number;
32
+ };
33
+ /**
34
+ * Exact WCAG relative luminance. MUI's `getLuminance` truncates to three
35
+ * digits, which is enough slop to misclassify a borderline shade either side
36
+ * of the 4.5:1 bar — the whole reason this module exists — so the math here
37
+ * keeps full precision. Alpha is ignored (this pipeline derives opaque
38
+ * tokens), matching MUI's behaviour for rgba inputs.
39
+ */
40
+ export declare function relativeLuminance(color: string): number;
41
+ /** Exact WCAG contrast ratio between two colors, 1–21. */
42
+ export declare function contrastRatio(colorA: string, colorB: string): number;
43
+ /** True when `foreground` clears `minContrast` against EVERY background. */
44
+ export declare function meetsContrast(foreground: string, backgrounds: ReadonlyArray<string>, minContrast?: number): boolean;
45
+ /**
46
+ * Nearest shade of `color`, walking HSL lightness in `direction` only, that
47
+ * clears `minContrast` against every background (AGL-1297). Hue and
48
+ * saturation are held fixed so the result stays the same colour, just
49
+ * deeper or lighter.
50
+ *
51
+ * - If `color` already clears the bar, it is returned UNCHANGED —
52
+ * byte-identical, so a value that was fine yesterday stays byte-stable.
53
+ * - The walk is one-directional: pass `'darken'` for a foreground on light
54
+ * backgrounds, `'lighten'` for a foreground on dark backgrounds.
55
+ * - If the bar is unreachable in that direction (e.g. lightening against a
56
+ * mid-grey), the walk stops at the pole (black/white) or the iteration
57
+ * cap and returns that best-effort value rather than looping or
58
+ * reversing direction.
59
+ */
60
+ export declare function accessibleShade(color: string, backgrounds: ReadonlyArray<string>, direction: ShadeDirection, options?: AccessibleShadeOptions): string;
61
+ export default accessibleShade;