@multiplatform.one/theme 7.28.1 → 7.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/theme/accentFloor.spec.ts +56 -0
- package/src/theme/chartPalette.spec.ts +27 -1
- package/src/theme/chartPalette.ts +39 -2
- package/src/theme/colorRules.spec.ts +29 -0
- package/src/theme/colorRules.ts +22 -0
- package/src/theme/createThemes.ts +158 -6
- package/src/theme/devtools/ThemeDevtoolsPanel.setMarkers.spec.tsx +91 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +15 -13
- package/src/theme/devtools/animationOptions.spec.ts +44 -0
- package/src/theme/devtools/animationOptions.ts +22 -0
- package/src/theme/readableColor.spec.ts +51 -1
- package/src/theme/readableColor.ts +65 -13
- package/src/theme/syntaxFloor.spec.ts +52 -0
- package/types/theme/chartPalette.d.ts +8 -0
- package/types/theme/chartPalette.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +9 -0
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/devtools/animationOptions.d.ts +9 -0
- package/types/theme/devtools/animationOptions.d.ts.map +1 -0
- package/types/theme/readableColor.d.ts +27 -7
- package/types/theme/readableColor.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/theme",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.29.1",
|
|
4
4
|
"description": "Tamagui theme system for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multiplatform",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@multiplatform.one/platform": "7.
|
|
48
|
-
"@multiplatform.one/store": "7.
|
|
47
|
+
"@multiplatform.one/platform": "7.29.1",
|
|
48
|
+
"@multiplatform.one/store": "7.29.1",
|
|
49
49
|
"@tamagui/animations-react-native": "2.7.6",
|
|
50
50
|
"@tamagui/colors": "2.7.6",
|
|
51
51
|
"@tamagui/config": "2.7.6",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"react-cookie": "^8.1.2"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@multiplatform.one/config": "7.
|
|
62
|
-
"@multiplatform.one/test-utils": "7.
|
|
61
|
+
"@multiplatform.one/config": "7.29.1",
|
|
62
|
+
"@multiplatform.one/test-utils": "7.29.1",
|
|
63
63
|
"@tamagui/animations-css": "2.7.6",
|
|
64
64
|
"@tamagui/animations-reanimated": "2.7.6",
|
|
65
65
|
"@tamagui/font-inter": "2.7.6",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
3
|
+
import { createThemesBuilder } from "./createThemes";
|
|
4
|
+
import { defaultAccentTheme } from "./defaults/accent";
|
|
5
|
+
import { defaultBaseTheme } from "./defaults/base";
|
|
6
|
+
import { defaultBuilderOptions } from "./defaults/builderOptions";
|
|
7
|
+
|
|
8
|
+
const themes = createThemesBuilder(
|
|
9
|
+
defaultBaseTheme,
|
|
10
|
+
defaultAccentTheme,
|
|
11
|
+
defaultBuilderOptions,
|
|
12
|
+
).themes() as Record<string, Record<string, string>>;
|
|
13
|
+
|
|
14
|
+
const hex = (raw: string) => {
|
|
15
|
+
const value = normalizeToHex(raw);
|
|
16
|
+
if (!value) throw new Error(`not an opaque colour: ${raw}`);
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
const ratio = (a: string, b: string) =>
|
|
20
|
+
contrastRatio(relativeLuminance(hex(a)), relativeLuminance(hex(b)));
|
|
21
|
+
|
|
22
|
+
describe("MPO-448 / MPO-449: the built accent pair holds Axiom 12", () => {
|
|
23
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
24
|
+
const theme = themes[scheme];
|
|
25
|
+
|
|
26
|
+
it(`${scheme} $accentBackground carries AA text with its better anchor and 3:1 on the page`, () => {
|
|
27
|
+
const fill = theme.accentBackground;
|
|
28
|
+
expect(
|
|
29
|
+
Math.max(ratio(fill, theme.color1), ratio(fill, theme.color12)),
|
|
30
|
+
).toBeGreaterThanOrEqual(4.5);
|
|
31
|
+
expect(ratio(fill, theme.color1)).toBeGreaterThanOrEqual(3);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it(`${scheme} $accentColor reads at 4.5:1 on $color1 through $color3`, () => {
|
|
35
|
+
for (const surface of [theme.color1, theme.color2, theme.color3]) {
|
|
36
|
+
expect(ratio(theme.accentColor, surface)).toBeGreaterThanOrEqual(4.5);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
it("light keeps the builder's pair; dark takes the nearest steps that hold", () => {
|
|
42
|
+
expect(hex(themes.light.accentBackground)).toBe("#634fc4");
|
|
43
|
+
expect(hex(themes.light.accentColor)).toBe("#3d2e8a");
|
|
44
|
+
expect(themes.dark.accentBackground).toBe(themes.dark.accent9);
|
|
45
|
+
expect(themes.dark.accentColor).toBe(themes.dark.accent11);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("the intent children carry the corrected pair and the inverse component themes keep theirs", () => {
|
|
49
|
+
for (const name of ["dark_warning", "dark_error", "dark_success"]) {
|
|
50
|
+
expect(themes[name].accentBackground).toBe(themes.dark.accentBackground);
|
|
51
|
+
expect(themes[name].accentColor).toBe(themes.dark.accentColor);
|
|
52
|
+
}
|
|
53
|
+
expect(hex(themes.dark_Tooltip.accentBackground)).toBe("#46349d");
|
|
54
|
+
expect(hex(themes.dark_Tooltip.accentColor)).toBe("#7766cc");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Colors from "@tamagui/colors";
|
|
2
2
|
import { themes as stockThemes } from "@tamagui/themes";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { resolveChartPalette } from "./chartPalette";
|
|
4
|
+
import { accentIdentityCandidates, resolveChartPalette } from "./chartPalette";
|
|
5
5
|
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
6
6
|
import { tintHueNames } from "./createThemes";
|
|
7
7
|
|
|
@@ -280,3 +280,29 @@ describe("resolveChartPalette", () => {
|
|
|
280
280
|
expect(a).toEqual(b);
|
|
281
281
|
});
|
|
282
282
|
});
|
|
283
|
+
|
|
284
|
+
describe("chart identity under the held dark accent (MPO-448)", () => {
|
|
285
|
+
const darkAccentSolids = [35, 38, 41, 43, 46, 49, 52, 54, 57, 60].map(
|
|
286
|
+
(lightness) => `hsla(250, 50%, ${lightness}%, 1)`,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
it("keeps the accent channel on the neighbouring step when the held fill misses the mark floor", () => {
|
|
290
|
+
const identitySolid = darkAccentSolids[8];
|
|
291
|
+
const palette = resolveChartPalette({
|
|
292
|
+
scheme: "dark",
|
|
293
|
+
identitySolid,
|
|
294
|
+
identityCandidates: [
|
|
295
|
+
...accentIdentityCandidates(identitySolid, darkAccentSolids),
|
|
296
|
+
"hsla(270, 6%, 54%, 1)",
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
expect(palette.single).toBe(darkAccentSolids[9]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("orders the accent solids nearest-first and skips what does not parse", () => {
|
|
303
|
+
expect(
|
|
304
|
+
accentIdentityCandidates("#6d5bc8", ["#3c2d86", undefined, "#7766cc", "#634fc4"]),
|
|
305
|
+
).toEqual(["#634fc4", "#7766cc", "#3c2d86"]);
|
|
306
|
+
expect(accentIdentityCandidates(undefined, ["#7766cc"])).toEqual([]);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -226,6 +226,31 @@ export function resolveChartPalette(input: ResolveChartPaletteInput): ChartPalet
|
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* The accent solids ordered nearest-first in luminance to `identity`, so a
|
|
231
|
+
* chart identity that misses the 3:1 mark floor on the reference cards falls
|
|
232
|
+
* to the next accent step before a neutral one. `$accentBackground` is held
|
|
233
|
+
* to the text floor (MPO-448), which in dark lands it one step deeper than
|
|
234
|
+
* the marks need; the chart keeps the accent channel on the step beside it.
|
|
235
|
+
*/
|
|
236
|
+
export function accentIdentityCandidates(
|
|
237
|
+
identity: string | undefined,
|
|
238
|
+
solids: readonly (string | undefined)[],
|
|
239
|
+
): string[] {
|
|
240
|
+
const identityHex = identity ? normalizeToHex(identity) : null;
|
|
241
|
+
if (!identityHex) return [];
|
|
242
|
+
const identityLum = relativeLuminance(identityHex);
|
|
243
|
+
return solids
|
|
244
|
+
.flatMap((solid) => {
|
|
245
|
+
const hex = solid ? normalizeToHex(solid) : null;
|
|
246
|
+
return solid && hex
|
|
247
|
+
? [{ solid, distance: Math.abs(relativeLuminance(hex) - identityLum) }]
|
|
248
|
+
: [];
|
|
249
|
+
})
|
|
250
|
+
.sort((a, b) => a.distance - b.distance)
|
|
251
|
+
.map(({ solid }) => solid);
|
|
252
|
+
}
|
|
253
|
+
|
|
229
254
|
/**
|
|
230
255
|
* Canonical chart palette for the active Tamagui theme.
|
|
231
256
|
*
|
|
@@ -246,8 +271,20 @@ export function useChartPalette(): ChartPalette {
|
|
|
246
271
|
const identitySolid = isTint ? (color9 ?? accent) : (accent ?? color9);
|
|
247
272
|
const color10 = theme.color10?.val;
|
|
248
273
|
const color11 = theme.color11?.val;
|
|
274
|
+
const accentSolids = Array.from({ length: 10 }, (_, index) => theme[`accent${index + 1}`]?.val);
|
|
275
|
+
const accentKey = accentSolids.join("|");
|
|
249
276
|
return useMemo(
|
|
250
|
-
() =>
|
|
251
|
-
|
|
277
|
+
() =>
|
|
278
|
+
resolveChartPalette({
|
|
279
|
+
scheme,
|
|
280
|
+
identitySolid,
|
|
281
|
+
identityCandidates: [
|
|
282
|
+
...(isTint ? [] : accentIdentityCandidates(identitySolid, accentSolids)),
|
|
283
|
+
color10,
|
|
284
|
+
color11,
|
|
285
|
+
],
|
|
286
|
+
}),
|
|
287
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
288
|
+
[scheme, identitySolid, isTint, accentKey, color10, color11],
|
|
252
289
|
);
|
|
253
290
|
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
assertContrast,
|
|
5
5
|
contrastRatio,
|
|
6
6
|
findReadableStep,
|
|
7
|
+
holdInkFloor,
|
|
7
8
|
measureContrast,
|
|
8
9
|
minContrastRatio,
|
|
9
10
|
normalizeColorScale,
|
|
@@ -551,3 +552,31 @@ describe("intent color11 on the base page surfaces (MPO-40)", () => {
|
|
|
551
552
|
]);
|
|
552
553
|
});
|
|
553
554
|
});
|
|
555
|
+
|
|
556
|
+
describe("holdInkFloor (MPO-448)", () => {
|
|
557
|
+
const onFill = (ink: string, fill: string) =>
|
|
558
|
+
contrastRatio(
|
|
559
|
+
relativeLuminance(normalizeToHex(ink)!),
|
|
560
|
+
relativeLuminance(normalizeToHex(fill)!),
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
it("keeps an ink that already clears the floor", () => {
|
|
564
|
+
expect(holdInkFloor("#6d5bc8", "#f0eff1")).toBe("#f0eff1");
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("hands a literal bar the pole when both dark anchors miss (#cc4e00: 4.03 / 3.93)", () => {
|
|
568
|
+
const ink = holdInkFloor("#cc4e00", "#161519");
|
|
569
|
+
expect(ink).toBe("#000000");
|
|
570
|
+
expect(onFill(ink, "#cc4e00")).toBeGreaterThanOrEqual(aaTextContrastRatio);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("hands a dark step-8 chip the white pole over its step-12 ink (3.88:1)", () => {
|
|
574
|
+
const ink = holdInkFloor("#2870bd", "#c2e6ff");
|
|
575
|
+
expect(ink).toBe("#ffffff");
|
|
576
|
+
expect(onFill(ink, "#2870bd")).toBeGreaterThanOrEqual(aaTextContrastRatio);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("returns the ink untouched when a colour cannot be parsed", () => {
|
|
580
|
+
expect(holdInkFloor("var(--x)", "#161519")).toBe("#161519");
|
|
581
|
+
});
|
|
582
|
+
});
|
package/src/theme/colorRules.ts
CHANGED
|
@@ -217,6 +217,28 @@ export function pickReadableForeground(
|
|
|
217
217
|
: candidateB;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Axiom 12 backstop for ink on a fill: `ink` when it clears `floor` on
|
|
222
|
+
* `fill`, otherwise the pole (`#000000` / `#ffffff`) that reads better on it,
|
|
223
|
+
* when that pole beats `ink`. A mid-luminance fill, such as a literal
|
|
224
|
+
* `#cc4e00` bar or a Radix step-8 chip in dark, can sit where neither scheme
|
|
225
|
+
* anchor clears AA while a pole does. Returns `ink` unchanged when a color
|
|
226
|
+
* fails to parse.
|
|
227
|
+
*/
|
|
228
|
+
export function holdInkFloor(fill: string, ink: string, floor = aaTextContrastRatio): string {
|
|
229
|
+
const fillHex = normalizeToHex(fill);
|
|
230
|
+
const inkHex = normalizeToHex(ink);
|
|
231
|
+
if (!fillHex || !inkHex) return ink;
|
|
232
|
+
const fillLum = relativeLuminance(fillHex);
|
|
233
|
+
const inkRatio = contrastRatio(relativeLuminance(inkHex), fillLum);
|
|
234
|
+
if (inkRatio >= floor) return ink;
|
|
235
|
+
const onWhite = contrastRatio(1, fillLum);
|
|
236
|
+
const onBlack = contrastRatio(0, fillLum);
|
|
237
|
+
const poleRatio = Math.max(onWhite, onBlack);
|
|
238
|
+
if (poleRatio <= inkRatio) return ink;
|
|
239
|
+
return onWhite >= onBlack ? "#ffffff" : "#000000";
|
|
240
|
+
}
|
|
241
|
+
|
|
220
242
|
/**
|
|
221
243
|
* Given a 12-color scale (hex strings), a background step, and a starting
|
|
222
244
|
* text step, scan towards `preferredDir` for the first step whose contrast
|
|
@@ -4,6 +4,13 @@ import {
|
|
|
4
4
|
type CreateThemesProps,
|
|
5
5
|
} from "@tamagui/theme-builder";
|
|
6
6
|
import { themes as tamaguiThemes } from "@tamagui/themes";
|
|
7
|
+
import {
|
|
8
|
+
aaTextContrastRatio,
|
|
9
|
+
contrastRatio,
|
|
10
|
+
minContrastRatio,
|
|
11
|
+
normalizeToHex,
|
|
12
|
+
relativeLuminance,
|
|
13
|
+
} from "./colorRules";
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
16
|
* MPO-40. Radix aims a scale's step 11 at the AA text floor against ITS OWN
|
|
@@ -245,6 +252,147 @@ function neutralizeSemanticEntryText(
|
|
|
245
252
|
return out;
|
|
246
253
|
}
|
|
247
254
|
|
|
255
|
+
function luminanceOf(color: string | undefined): number | null {
|
|
256
|
+
const hex = color ? normalizeToHex(color) : null;
|
|
257
|
+
return hex ? relativeLuminance(hex) : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The accent step nearest in luminance to `preferred` that passes `holds`,
|
|
262
|
+
* or `preferred` itself when it already passes or nothing does.
|
|
263
|
+
*/
|
|
264
|
+
function nearestHoldingStep(
|
|
265
|
+
preferred: string,
|
|
266
|
+
steps: (string | undefined)[],
|
|
267
|
+
holds: (lum: number) => boolean,
|
|
268
|
+
): string {
|
|
269
|
+
const preferredLum = luminanceOf(preferred);
|
|
270
|
+
if (preferredLum === null || holds(preferredLum)) return preferred;
|
|
271
|
+
let best = preferred;
|
|
272
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
273
|
+
for (const step of steps) {
|
|
274
|
+
const lum = luminanceOf(step);
|
|
275
|
+
if (step === undefined || lum === null || !holds(lum)) continue;
|
|
276
|
+
const distance = Math.abs(lum - preferredLum);
|
|
277
|
+
if (distance < bestDistance) {
|
|
278
|
+
bestDistance = distance;
|
|
279
|
+
best = step;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return best;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Axiom 12 LEGIBLE FLOOR on the scheme's accent pair (MPO-448, MPO-449). The
|
|
287
|
+
* theme-builder reads `accentBackground` and `accentColor` off fixed palette
|
|
288
|
+
* slots, and the house dark accent ramp puts both on the wrong side of the
|
|
289
|
+
* floor: the fill (`#7766cc`) carries text at 4.02:1 against either scheme
|
|
290
|
+
* anchor, and the ink (`#46349d`) reads 1.74:1 on the dark page. LC-05 keeps
|
|
291
|
+
* the channel and frees the step, so each token walks to the nearest accent
|
|
292
|
+
* step that holds its floor and stays put where it already does (light).
|
|
293
|
+
*
|
|
294
|
+
* - `accentBackground` is a text-bearing solid: its better anchor (`$color1`
|
|
295
|
+
* or `$color12`) clears 4.5:1 on it, and it keeps the 3:1 graphic tier
|
|
296
|
+
* against the page, because dots, edges and bars paint it bare.
|
|
297
|
+
* - `accentColor` is accent ink: 4.5:1 on the page and the first raised
|
|
298
|
+
* surface (`$color1`-`$color3`).
|
|
299
|
+
*
|
|
300
|
+
* Every theme of the scheme that inherited the base pair (the intent
|
|
301
|
+
* children) takes the corrected pair; one that set its own (the inverse
|
|
302
|
+
* component themes) keeps it.
|
|
303
|
+
*/
|
|
304
|
+
function holdAccentFloor(
|
|
305
|
+
themes: Record<string, Record<string, string>>,
|
|
306
|
+
): Record<string, Record<string, string>> {
|
|
307
|
+
const out: Record<string, Record<string, string>> = { ...themes };
|
|
308
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
309
|
+
const base = themes[scheme];
|
|
310
|
+
if (!base?.accentBackground || !base.accentColor) continue;
|
|
311
|
+
const paper = luminanceOf(base.color1);
|
|
312
|
+
const ink = luminanceOf(base.color12);
|
|
313
|
+
const surfaces = [base.color1, base.color2, base.color3].map(luminanceOf);
|
|
314
|
+
if (paper === null || ink === null || surfaces.some((lum) => lum === null)) continue;
|
|
315
|
+
const steps = Array.from({ length: 12 }, (_, index) => base[`accent${index + 1}`]);
|
|
316
|
+
const fill = nearestHoldingStep(
|
|
317
|
+
base.accentBackground,
|
|
318
|
+
steps.slice(0, 10),
|
|
319
|
+
(lum) =>
|
|
320
|
+
Math.max(contrastRatio(lum, paper), contrastRatio(lum, ink)) >= aaTextContrastRatio &&
|
|
321
|
+
contrastRatio(lum, paper) >= minContrastRatio,
|
|
322
|
+
);
|
|
323
|
+
const accentInk = nearestHoldingStep(base.accentColor, steps, (lum) =>
|
|
324
|
+
surfaces.every((surface) => contrastRatio(lum, surface as number) >= aaTextContrastRatio),
|
|
325
|
+
);
|
|
326
|
+
if (fill === base.accentBackground && accentInk === base.accentColor) continue;
|
|
327
|
+
for (const [name, theme] of Object.entries(themes)) {
|
|
328
|
+
if (name !== scheme && !name.startsWith(`${scheme}_`)) continue;
|
|
329
|
+
const inheritsFill = theme.accentBackground === base.accentBackground;
|
|
330
|
+
const inheritsInk = theme.accentColor === base.accentColor;
|
|
331
|
+
if (!inheritsFill && !inheritsInk) continue;
|
|
332
|
+
out[name] = {
|
|
333
|
+
...theme,
|
|
334
|
+
...(inheritsFill ? { accentBackground: fill } : null),
|
|
335
|
+
...(inheritsInk ? { accentColor: accentInk } : null),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The hues the code syntax palette paints (MPO-468). */
|
|
343
|
+
const syntaxInkHues = ["purple", "green", "blue", "orange", "red"] as const;
|
|
344
|
+
|
|
345
|
+
function mixHex(from: string, to: string, share: number): string {
|
|
346
|
+
const channel = (offset: number) =>
|
|
347
|
+
Math.round(
|
|
348
|
+
Number.parseInt(from.slice(offset, offset + 2), 16) * (1 - share) +
|
|
349
|
+
Number.parseInt(to.slice(offset, offset + 2), 16) * share,
|
|
350
|
+
)
|
|
351
|
+
.toString(16)
|
|
352
|
+
.padStart(2, "0");
|
|
353
|
+
return `#${channel(1)}${channel(3)}${channel(5)}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Axiom 12 LEGIBLE FLOOR on code syntax ink (MPO-468). Radix aims a hue's
|
|
358
|
+
* step 11 at 4.5:1 on its own steps 1-2, but code paints on the raised
|
|
359
|
+
* `$color3`, where light `green11`, `blue11` and `orange11` measure 4.22,
|
|
360
|
+
* 4.27 and 4.04:1. Every theme that carries the hue ramps gets a
|
|
361
|
+
* `syntax<Hue>` ink: step 11, walked toward step 12 in twentieths until it
|
|
362
|
+
* clears 4.5:1 on that theme's `$color3` and on the hue's step-3 wash, which
|
|
363
|
+
* diff lines paint. Dark keeps step 11; a theme where no step holds gets no
|
|
364
|
+
* token and inherits its parent's.
|
|
365
|
+
*/
|
|
366
|
+
function holdSyntaxFloor(
|
|
367
|
+
themes: Record<string, Record<string, string>>,
|
|
368
|
+
): Record<string, Record<string, string>> {
|
|
369
|
+
const out: Record<string, Record<string, string>> = { ...themes };
|
|
370
|
+
for (const [name, theme] of Object.entries(themes)) {
|
|
371
|
+
const surface = luminanceOf(theme.color3);
|
|
372
|
+
if (surface === null) continue;
|
|
373
|
+
const ink: Record<string, string> = {};
|
|
374
|
+
for (const hue of syntaxInkHues) {
|
|
375
|
+
const from = theme[`${hue}11`] ? normalizeToHex(theme[`${hue}11`]) : null;
|
|
376
|
+
const to = theme[`${hue}12`] ? normalizeToHex(theme[`${hue}12`]) : null;
|
|
377
|
+
const wash = luminanceOf(theme[`${hue}3`]);
|
|
378
|
+
if (!from || !to || wash === null) continue;
|
|
379
|
+
for (let step = 0; step <= 20; step++) {
|
|
380
|
+
const candidate = mixHex(from, to, step / 20);
|
|
381
|
+
const lum = relativeLuminance(candidate);
|
|
382
|
+
if (
|
|
383
|
+
contrastRatio(lum, surface) >= aaTextContrastRatio &&
|
|
384
|
+
contrastRatio(lum, wash) >= aaTextContrastRatio
|
|
385
|
+
) {
|
|
386
|
+
ink[`syntax${hue[0].toUpperCase()}${hue.slice(1)}`] = candidate;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (Object.keys(ink).length > 0) out[name] = { ...theme, ...ink };
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
248
396
|
const defaultLightShadows: Shadows = {
|
|
249
397
|
shadow1: "rgba(0,0,0,0.12)",
|
|
250
398
|
shadow2: "rgba(0,0,0,0.16)",
|
|
@@ -436,12 +584,16 @@ export function createThemesBuilder(
|
|
|
436
584
|
const initialTheme = tamaguiCreateThemes(
|
|
437
585
|
createThemesProps as Parameters<typeof tamaguiCreateThemes>[0],
|
|
438
586
|
);
|
|
439
|
-
const themes =
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
587
|
+
const themes = holdSyntaxFloor(
|
|
588
|
+
holdAccentFloor(
|
|
589
|
+
neutralizeSemanticEntryText(
|
|
590
|
+
neutralizeTintText({
|
|
591
|
+
...grayTheme,
|
|
592
|
+
...tamaguiThemes,
|
|
593
|
+
...initialTheme,
|
|
594
|
+
}),
|
|
595
|
+
),
|
|
596
|
+
),
|
|
445
597
|
);
|
|
446
598
|
return {
|
|
447
599
|
themes() {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPO-463: the panel marks a knob "set" from its own session state, so every
|
|
3
|
+
* remount showed every knob unset while the knobs stayed applied. core's
|
|
4
|
+
* TanstackDevtools keys the shell on the resolved scheme, so a Scheme click
|
|
5
|
+
* remounts it; a reload does the same. The applied knobs live in the
|
|
6
|
+
* `mp.ov.*` cookies, so a fresh mount must read its markers back from them.
|
|
7
|
+
*/
|
|
8
|
+
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
|
9
|
+
import { CookiesProvider } from "react-cookie";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
11
|
+
import type { ThemeName } from "@tamagui/web";
|
|
12
|
+
import { cookieOverridesPrefix, cookiePreset, persistPresetOverrides } from "../cookies";
|
|
13
|
+
import { defaultKnobs } from "../knobs";
|
|
14
|
+
import { clearPresets, registerPreset } from "../shared";
|
|
15
|
+
import { ThemeDevtoolsPanel } from "./ThemeDevtoolsPanel";
|
|
16
|
+
|
|
17
|
+
function clearThemeCookies() {
|
|
18
|
+
for (const key of [
|
|
19
|
+
cookiePreset,
|
|
20
|
+
...Array.from({ length: 10 }, (_, i) => `${cookieOverridesPrefix}.${i}`),
|
|
21
|
+
]) {
|
|
22
|
+
document.cookie = `${key}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Shell({ scheme }: { scheme: "light" | "dark" }) {
|
|
27
|
+
return (
|
|
28
|
+
<CookiesProvider>
|
|
29
|
+
<ThemeDevtoolsPanel key={scheme} devtoolsTheme={scheme} />
|
|
30
|
+
</CookiesProvider>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pressed(label: string, title: string) {
|
|
35
|
+
return within(screen.getByRole("group", { name: label }))
|
|
36
|
+
.getByTitle(title)
|
|
37
|
+
.getAttribute("aria-pressed");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(clearThemeCookies);
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
cleanup();
|
|
43
|
+
clearThemeCookies();
|
|
44
|
+
clearPresets();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("ThemeDevtoolsPanel set markers survive a remount (MPO-463)", () => {
|
|
48
|
+
it("a reload over mp.ov.0 marks the persisted knob set and leaves the rest unset", () => {
|
|
49
|
+
persistPresetOverrides({ fillStyle: "outlined" });
|
|
50
|
+
render(<Shell scheme="light" />);
|
|
51
|
+
expect(pressed("Fill", "outlined")).toBe("true");
|
|
52
|
+
expect(pressed("Fill", "unset (inherit)")).toBe("false");
|
|
53
|
+
expect(pressed("Size", "unset (inherit)")).toBe("true");
|
|
54
|
+
expect(pressed("Radius", "unset (inherit)")).toBe("true");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("a knob set in the panel is still marked after the shell remounts it", () => {
|
|
58
|
+
const shell = render(<Shell scheme="light" />);
|
|
59
|
+
fireEvent.click(within(screen.getByRole("group", { name: "Fill" })).getByTitle("outlined"));
|
|
60
|
+
expect(pressed("Fill", "outlined")).toBe("true");
|
|
61
|
+
|
|
62
|
+
shell.rerender(<Shell scheme="dark" />);
|
|
63
|
+
expect(pressed("Fill", "outlined")).toBe("true");
|
|
64
|
+
expect(pressed("Fill", "unset (inherit)")).toBe("false");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("a preset's own values read as inherited, only the knobs over it as set", () => {
|
|
68
|
+
registerPreset("mpo463", {
|
|
69
|
+
theme: "gray" as ThemeName,
|
|
70
|
+
knobs: { ...defaultKnobs, fillStyle: "outlined", size: "small" },
|
|
71
|
+
intents: {},
|
|
72
|
+
tints: [],
|
|
73
|
+
});
|
|
74
|
+
document.cookie = `${cookiePreset}=mpo463; path=/`;
|
|
75
|
+
persistPresetOverrides({ size: "large" });
|
|
76
|
+
render(<Shell scheme="light" />);
|
|
77
|
+
expect(pressed("Fill", "unset (inherit)")).toBe("true");
|
|
78
|
+
expect(pressed("Size", "large")).toBe("true");
|
|
79
|
+
expect(pressed("Size", "unset (inherit)")).toBe("false");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("a knob never set shows unset after a remount", () => {
|
|
83
|
+
const shell = render(<Shell scheme="light" />);
|
|
84
|
+
fireEvent.click(within(screen.getByRole("group", { name: "Fill" })).getByTitle("outlined"));
|
|
85
|
+
|
|
86
|
+
shell.rerender(<Shell scheme="dark" />);
|
|
87
|
+
for (const label of ["Size", "Space", "Radius", "Border", "Elevation"]) {
|
|
88
|
+
expect(pressed(label, "unset (inherit)"), label).toBe("true");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -72,8 +72,8 @@ import {
|
|
|
72
72
|
FormAutofocus,
|
|
73
73
|
interactionStates,
|
|
74
74
|
} from "../knobs";
|
|
75
|
-
import { animationNames } from "../animations/index";
|
|
76
75
|
import { getPresetNames, getPreset } from "../shared";
|
|
76
|
+
import { extractAnimationNames } from "./animationOptions";
|
|
77
77
|
import { ColorLineVisualizer } from "./ColorLineVisualizer";
|
|
78
78
|
import { buildSnapshot } from "./buildSnapshot";
|
|
79
79
|
import {
|
|
@@ -100,16 +100,6 @@ import {
|
|
|
100
100
|
formAutofocusIcon as _formAutofocusIcon,
|
|
101
101
|
} from "./knobIcons";
|
|
102
102
|
|
|
103
|
-
function extractAnimationNames(tamaguiConfig: unknown): string[] {
|
|
104
|
-
const config = tamaguiConfig as Record<string, unknown> | null;
|
|
105
|
-
const anims = config?.animations as Record<string, unknown> | undefined;
|
|
106
|
-
if (anims) {
|
|
107
|
-
const keys = Object.keys(anims).filter((key) => key !== "animations" && !key.startsWith("_"));
|
|
108
|
-
if (keys.length > 0) return keys;
|
|
109
|
-
}
|
|
110
|
-
return animationNames as string[];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
103
|
// ---------------------------------------------------------------------------
|
|
114
104
|
// Constants
|
|
115
105
|
// ---------------------------------------------------------------------------
|
|
@@ -573,6 +563,8 @@ function IconGroup<V extends string | number | boolean>({
|
|
|
573
563
|
}) {
|
|
574
564
|
return (
|
|
575
565
|
<div
|
|
566
|
+
role="group"
|
|
567
|
+
aria-label={label}
|
|
576
568
|
style={{ ...styles.card, position: "relative" as const }}
|
|
577
569
|
onMouseEnter={(e) => {
|
|
578
570
|
const element = (e.currentTarget as HTMLElement).querySelector(
|
|
@@ -606,6 +598,7 @@ function IconGroup<V extends string | number | boolean>({
|
|
|
606
598
|
}}
|
|
607
599
|
onClick={onUnset}
|
|
608
600
|
title="unset (inherit)"
|
|
601
|
+
aria-pressed={value === undefined}
|
|
609
602
|
>
|
|
610
603
|
↺
|
|
611
604
|
</button>
|
|
@@ -619,6 +612,7 @@ function IconGroup<V extends string | number | boolean>({
|
|
|
619
612
|
style={styles.iconBtn(active)}
|
|
620
613
|
onClick={() => onChange(opt)}
|
|
621
614
|
title={String(opt)}
|
|
615
|
+
aria-pressed={active}
|
|
622
616
|
>
|
|
623
617
|
{icon(opt, active, isDark)}
|
|
624
618
|
</button>
|
|
@@ -901,8 +895,16 @@ export function ThemeDevtoolsPanel({
|
|
|
901
895
|
[tamaguiConfig],
|
|
902
896
|
);
|
|
903
897
|
|
|
904
|
-
//
|
|
905
|
-
|
|
898
|
+
// Knobs the user has set. Seeded from the knobs that differ from what the
|
|
899
|
+
// active preset supplies, which is what the mp.ov.* cookies hold: core's
|
|
900
|
+
// TanstackDevtools keys the shell on the scheme, so a Scheme click remounts
|
|
901
|
+
// this panel, and a reload does the same.
|
|
902
|
+
const [overrides, setOverrides] = useState<Set<string>>(() => {
|
|
903
|
+
const presetKnobs = activePresetName ? getPreset(activePresetName)?.knobs : undefined;
|
|
904
|
+
return new Set(
|
|
905
|
+
knobKeys.filter((key) => knobs[key] !== (presetKnobs?.[key] ?? defaultKnobs[key])),
|
|
906
|
+
);
|
|
907
|
+
});
|
|
906
908
|
const [pinnedKnobs, setPinnedKnobs] = useState<Set<string>>(() => {
|
|
907
909
|
try {
|
|
908
910
|
const raw = storage.getItem(pinnedKnobsStorageKey);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { animationNames, animations } from "../animations/css";
|
|
3
|
+
import { extractAnimationNames } from "./animationOptions";
|
|
4
|
+
|
|
5
|
+
describe("extractAnimationNames (MPO-447)", () => {
|
|
6
|
+
it("lists the registered animations of a createTamagui config, not the driver's API", () => {
|
|
7
|
+
const names = extractAnimationNames({ animations });
|
|
8
|
+
expect(names).toEqual([...animationNames]);
|
|
9
|
+
for (const member of [
|
|
10
|
+
"usePresence",
|
|
11
|
+
"ResetPresence",
|
|
12
|
+
"inputStyle",
|
|
13
|
+
"outputStyle",
|
|
14
|
+
"useAnimatedNumber",
|
|
15
|
+
"useAnimations",
|
|
16
|
+
]) {
|
|
17
|
+
expect(names).not.toContain(member);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("offers exactly the house names on the house driver", () => {
|
|
22
|
+
expect(extractAnimationNames({ animations })).toEqual([
|
|
23
|
+
"bouncy",
|
|
24
|
+
"lazy",
|
|
25
|
+
"slow",
|
|
26
|
+
"medium",
|
|
27
|
+
"quick",
|
|
28
|
+
"tooltip",
|
|
29
|
+
"snappy",
|
|
30
|
+
"gentle",
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("reads an app's own driver names when it registers different ones", () => {
|
|
35
|
+
const driver = { ...animations, animations: { fast: "ease-in 50ms", calm: "ease-in 900ms" } };
|
|
36
|
+
expect(extractAnimationNames({ animations: driver })).toEqual(["fast", "calm"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("falls back to the house names without a config or without named animations", () => {
|
|
40
|
+
expect(extractAnimationNames(undefined)).toEqual([...animationNames]);
|
|
41
|
+
expect(extractAnimationNames({})).toEqual([...animationNames]);
|
|
42
|
+
expect(extractAnimationNames({ animations: { animations: {} } })).toEqual([...animationNames]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { animationNames } from "../animations/index";
|
|
2
|
+
|
|
3
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === "object" && value !== null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The animation names a Tamagui config registers, for the panel's Animation
|
|
9
|
+
* select. `createTamagui()` stores the resolved driver at `config.animations`,
|
|
10
|
+
* and every driver `createAnimations()` builds keeps its named animations one
|
|
11
|
+
* level down, on `driver.animations`. The driver's own members (`usePresence`,
|
|
12
|
+
* `useAnimations`, `inputStyle`, ...) are API, not animations (MPO-447).
|
|
13
|
+
*/
|
|
14
|
+
export function extractAnimationNames(tamaguiConfig: unknown): string[] {
|
|
15
|
+
const driver = isRecord(tamaguiConfig) ? tamaguiConfig.animations : undefined;
|
|
16
|
+
const named = isRecord(driver) ? driver.animations : undefined;
|
|
17
|
+
if (isRecord(named)) {
|
|
18
|
+
const keys = Object.keys(named);
|
|
19
|
+
if (keys.length > 0) return keys;
|
|
20
|
+
}
|
|
21
|
+
return [...animationNames];
|
|
22
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
resolveAaSolidFill,
|
|
4
|
+
resolveAccentTintedSurface,
|
|
5
|
+
resolveLegibleInkOn,
|
|
6
|
+
} from "./readableColor";
|
|
3
7
|
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
4
8
|
|
|
5
9
|
// The stock accent ramp (defaults/accent.ts) as the theme resolves it.
|
|
@@ -166,3 +170,49 @@ describe("resolveAaSolidFill", () => {
|
|
|
166
170
|
).toBe("$accentBackground");
|
|
167
171
|
});
|
|
168
172
|
});
|
|
173
|
+
|
|
174
|
+
describe("resolveLegibleInkOn (MPO-448)", () => {
|
|
175
|
+
it("keeps the component's ink while it clears AA on the fill", () => {
|
|
176
|
+
expect(
|
|
177
|
+
resolveLegibleInkOn({
|
|
178
|
+
fill: "#eeecf9",
|
|
179
|
+
preferred: "#66606c",
|
|
180
|
+
paper: lightPaper,
|
|
181
|
+
ink: lightInk,
|
|
182
|
+
}),
|
|
183
|
+
).toBe("#66606c");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("hands a muted title on the dark accent tint to the readable anchor (4.05:1)", () => {
|
|
187
|
+
expect(
|
|
188
|
+
resolveLegibleInkOn({
|
|
189
|
+
fill: darkAccent[0],
|
|
190
|
+
preferred: "#a09ba6",
|
|
191
|
+
paper: darkPaper,
|
|
192
|
+
ink: darkInk,
|
|
193
|
+
}),
|
|
194
|
+
).toBe(darkInk);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("falls through the anchors to a pole when neither reads (dark blue chip)", () => {
|
|
198
|
+
const picked = resolveLegibleInkOn({
|
|
199
|
+
fill: "#2870bd",
|
|
200
|
+
preferred: "#c2e6ff",
|
|
201
|
+
paper: darkPaper,
|
|
202
|
+
ink: darkInk,
|
|
203
|
+
});
|
|
204
|
+
expect(picked).toBe("#ffffff");
|
|
205
|
+
expect(contrastRatio(lum(picked!), lum("#2870bd"))).toBeGreaterThanOrEqual(4.5);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("returns the preferred ink when there is no fill to read against", () => {
|
|
209
|
+
expect(
|
|
210
|
+
resolveLegibleInkOn({
|
|
211
|
+
fill: undefined,
|
|
212
|
+
preferred: "#a09ba6",
|
|
213
|
+
paper: darkPaper,
|
|
214
|
+
ink: darkInk,
|
|
215
|
+
}),
|
|
216
|
+
).toBe("#a09ba6");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -3,6 +3,7 @@ import { useTheme as useTamaguiTheme } from "tamagui";
|
|
|
3
3
|
import {
|
|
4
4
|
aaTextContrastRatio,
|
|
5
5
|
contrastRatio,
|
|
6
|
+
holdInkFloor,
|
|
6
7
|
normalizeToHex,
|
|
7
8
|
pickReadableForeground,
|
|
8
9
|
relativeLuminance,
|
|
@@ -22,11 +23,10 @@ import {
|
|
|
22
23
|
* (paper `$color1` / ink `$color12`) carries more contrast on the ACTUAL
|
|
23
24
|
* fill — never assumed.
|
|
24
25
|
*
|
|
25
|
-
* 2. Accent-hued text on the PAGE (`useAccentOnSurface`).
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* surfaces — a mid accent in light, a pale accent in dark.
|
|
26
|
+
* 2. Accent-hued text on the PAGE (`useAccentOnSurface`). The readable accent
|
|
27
|
+
* is the most chromatic step of the theme's own accent ramp that still
|
|
28
|
+
* clears AA (4.5:1) on both page surfaces — a mid accent in light, a pale
|
|
29
|
+
* accent in dark.
|
|
30
30
|
*
|
|
31
31
|
* Both read live theme values so they stay correct across schemes, tints and
|
|
32
32
|
* custom accent palettes; they mirror the runtime pattern already used by the
|
|
@@ -47,8 +47,9 @@ function resolveTokenValue(
|
|
|
47
47
|
* / "$color8", or an already-resolved color value — e.g. the step returned by
|
|
48
48
|
* `useAccentTintedSurface`): the scheme's paper (`$color1`) or ink (`$color12`)
|
|
49
49
|
* anchor, whichever contrasts more on the resolved fill, returned as a concrete
|
|
50
|
-
* value so no nested sub-theme can flip it.
|
|
51
|
-
*
|
|
50
|
+
* value so no nested sub-theme can flip it. When neither anchor clears AA on
|
|
51
|
+
* the fill, the pole that does (`holdInkFloor`). Returns `undefined` when any
|
|
52
|
+
* color fails to resolve/parse — callers keep their own default in that case.
|
|
52
53
|
*/
|
|
53
54
|
export function useReadableTextOn(fill_: string | undefined): string | undefined {
|
|
54
55
|
const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
|
|
@@ -58,10 +59,61 @@ export function useReadableTextOn(fill_: string | undefined): string | undefined
|
|
|
58
59
|
return useMemo(() => {
|
|
59
60
|
if (!fill || !paper || !ink) return undefined;
|
|
60
61
|
if (!normalizeToHex(fill) || !normalizeToHex(paper) || !normalizeToHex(ink)) return undefined;
|
|
61
|
-
return pickReadableForeground(fill, paper, ink);
|
|
62
|
+
return holdInkFloor(fill, pickReadableForeground(fill, paper, ink));
|
|
62
63
|
}, [fill, paper, ink]);
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Pure resolver behind {@link useLegibleInkOn}: `preferred` when it clears AA
|
|
68
|
+
* on `fill`, otherwise the readable anchor (paper or ink) held to the floor
|
|
69
|
+
* by `holdInkFloor`. Any unparseable input returns `preferred`.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveLegibleInkOn({
|
|
72
|
+
fill,
|
|
73
|
+
preferred,
|
|
74
|
+
paper,
|
|
75
|
+
ink,
|
|
76
|
+
}: {
|
|
77
|
+
fill?: string;
|
|
78
|
+
preferred?: string;
|
|
79
|
+
paper?: string;
|
|
80
|
+
ink?: string;
|
|
81
|
+
}): string | undefined {
|
|
82
|
+
const fillHex = fill ? normalizeToHex(fill) : null;
|
|
83
|
+
const preferredHex = preferred ? normalizeToHex(preferred) : null;
|
|
84
|
+
if (!fill || !fillHex || !preferred || !preferredHex) return preferred;
|
|
85
|
+
const fillLum = relativeLuminance(fillHex);
|
|
86
|
+
if (contrastRatio(relativeLuminance(preferredHex), fillLum) >= aaTextContrastRatio) {
|
|
87
|
+
return preferred;
|
|
88
|
+
}
|
|
89
|
+
const anchor =
|
|
90
|
+
paper && ink && normalizeToHex(paper) && normalizeToHex(ink)
|
|
91
|
+
? pickReadableForeground(fill, paper, ink)
|
|
92
|
+
: preferred;
|
|
93
|
+
return holdInkFloor(fill, anchor);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Keep a component's own ink (`preferred`, e.g. a chip's `$blue12` or a
|
|
98
|
+
* muted `$color11` title) on `fill` while it clears AA, and hand over to the
|
|
99
|
+
* readable anchor, then a pole, where it does not. The token comes back
|
|
100
|
+
* untouched when it passes, so a scheme where the pair already reads keeps
|
|
101
|
+
* its exact paint; a replacement comes back as a concrete color.
|
|
102
|
+
*/
|
|
103
|
+
export function useLegibleInkOn(fill_: string | undefined, preferred: string): string {
|
|
104
|
+
const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
|
|
105
|
+
const fill = fill_?.startsWith("$") ? resolveTokenValue(theme, fill_) : fill_;
|
|
106
|
+
const preferredValue = preferred.startsWith("$")
|
|
107
|
+
? resolveTokenValue(theme, preferred)
|
|
108
|
+
: preferred;
|
|
109
|
+
const paper = resolveTokenValue(theme, "color1");
|
|
110
|
+
const ink = resolveTokenValue(theme, "color12");
|
|
111
|
+
return useMemo(() => {
|
|
112
|
+
const resolved = resolveLegibleInkOn({ fill, preferred: preferredValue, paper, ink });
|
|
113
|
+
return !resolved || resolved === preferredValue ? preferred : resolved;
|
|
114
|
+
}, [fill, preferred, preferredValue, paper, ink]);
|
|
115
|
+
}
|
|
116
|
+
|
|
65
117
|
/**
|
|
66
118
|
* Accent-hued text/link color readable on the page surfaces (color1/color2).
|
|
67
119
|
* Scans the theme's own accent ramp for the most chromatic step that clears
|
|
@@ -195,11 +247,11 @@ function bestAnchorContrast(fill: string, paperLum: number, inkLum: number): num
|
|
|
195
247
|
/**
|
|
196
248
|
* Pick an accent solid that can carry AA text (paper or ink ≥ 4.5).
|
|
197
249
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* the mark on the accent channel (LC-05) instead
|
|
202
|
-
* tinted surface.
|
|
250
|
+
* A mid-chroma solid can clear the graphic 3:1 tier and still miss the text
|
|
251
|
+
* floor with both scheme anchors (the stock dark `#7766cc` measured 4.02:1
|
|
252
|
+
* before the build held `$accentBackground` to it, MPO-448). Walking to the
|
|
253
|
+
* nearest passing solid keeps the mark on the accent channel (LC-05) instead
|
|
254
|
+
* of dropping to ink or a tinted surface.
|
|
203
255
|
*
|
|
204
256
|
* Returns the preferred token when it already clears the floor, otherwise
|
|
205
257
|
* the solid token whose resolved luminance is closest to the preferred fill.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
3
|
+
import { createThemesBuilder } from "./createThemes";
|
|
4
|
+
import { defaultAccentTheme } from "./defaults/accent";
|
|
5
|
+
import { defaultBaseTheme } from "./defaults/base";
|
|
6
|
+
import { defaultBuilderOptions } from "./defaults/builderOptions";
|
|
7
|
+
|
|
8
|
+
const themes = createThemesBuilder(
|
|
9
|
+
defaultBaseTheme,
|
|
10
|
+
defaultAccentTheme,
|
|
11
|
+
defaultBuilderOptions,
|
|
12
|
+
).themes() as Record<string, Record<string, string>>;
|
|
13
|
+
|
|
14
|
+
const hex = (raw: string) => {
|
|
15
|
+
const value = normalizeToHex(raw);
|
|
16
|
+
if (!value) throw new Error(`not an opaque colour: ${raw}`);
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
const ratio = (a: string, b: string) =>
|
|
20
|
+
contrastRatio(relativeLuminance(hex(a)), relativeLuminance(hex(b)));
|
|
21
|
+
|
|
22
|
+
const hues = ["purple", "green", "blue", "orange", "red"] as const;
|
|
23
|
+
const token = (hue: (typeof hues)[number]) => `syntax${hue[0].toUpperCase()}${hue.slice(1)}`;
|
|
24
|
+
|
|
25
|
+
describe("MPO-468: code syntax ink holds Axiom 12 on the code surface", () => {
|
|
26
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
27
|
+
const theme = themes[scheme];
|
|
28
|
+
for (const hue of hues) {
|
|
29
|
+
it(`${scheme} $${token(hue)} reads at 4.5:1 on $color3 and on the $${hue}3 wash`, () => {
|
|
30
|
+
expect(ratio(theme[token(hue)], theme.color3)).toBeGreaterThanOrEqual(4.5);
|
|
31
|
+
expect(ratio(theme[token(hue)], theme[`${hue}3`])).toBeGreaterThanOrEqual(4.5);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it("dark keeps step 11; light walks green, blue and orange toward step 12", () => {
|
|
37
|
+
for (const hue of hues) {
|
|
38
|
+
expect(themes.dark[token(hue)]).toBe(hex(themes.dark[`${hue}11`]));
|
|
39
|
+
}
|
|
40
|
+
expect(themes.light.syntaxPurple).toBe(hex(themes.light.purple11));
|
|
41
|
+
expect(themes.light.syntaxRed).toBe(hex(themes.light.red11));
|
|
42
|
+
expect(themes.light.syntaxGreen).toBe("#207c54");
|
|
43
|
+
expect(themes.light.syntaxBlue).toBe("#0d6dc3");
|
|
44
|
+
expect(themes.light.syntaxOrange).toBe("#bb4904");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("the tokens ride exactly the themes that carry the hue ramps", () => {
|
|
48
|
+
for (const theme of Object.values(themes)) {
|
|
49
|
+
expect(Boolean(theme.syntaxGreen)).toBe(Boolean(theme.green11 && theme.color3));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -51,6 +51,14 @@ export interface ResolveChartPaletteInput {
|
|
|
51
51
|
* resolve palettes for any scheme/identity combination.
|
|
52
52
|
*/
|
|
53
53
|
export declare function resolveChartPalette(input: ResolveChartPaletteInput): ChartPalette;
|
|
54
|
+
/**
|
|
55
|
+
* The accent solids ordered nearest-first in luminance to `identity`, so a
|
|
56
|
+
* chart identity that misses the 3:1 mark floor on the reference cards falls
|
|
57
|
+
* to the next accent step before a neutral one. `$accentBackground` is held
|
|
58
|
+
* to the text floor (MPO-448), which in dark lands it one step deeper than
|
|
59
|
+
* the marks need; the chart keeps the accent channel on the step beside it.
|
|
60
|
+
*/
|
|
61
|
+
export declare function accentIdentityCandidates(identity: string | undefined, solids: readonly (string | undefined)[]): string[];
|
|
54
62
|
/**
|
|
55
63
|
* Canonical chart palette for the active Tamagui theme.
|
|
56
64
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chartPalette.d.ts","sourceRoot":"","sources":["../../src/theme/chartPalette.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,sEAAsE;IACtE,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/D;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,kBAAkB,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;CACtD;AAkJD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,YAAY,CAyBjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,IAAI,YAAY,
|
|
1
|
+
{"version":3,"file":"chartPalette.d.ts","sourceRoot":"","sources":["../../src/theme/chartPalette.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,sEAAsE;IACtE,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/D;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,kBAAkB,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;CACtD;AAkJD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,YAAY,CAyBjF;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GACtC,MAAM,EAAE,CAaV;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,IAAI,YAAY,CA2B9C"}
|
|
@@ -90,6 +90,15 @@ export declare function resolveThemeContrast(themes: Record<string, Record<strin
|
|
|
90
90
|
* `candidateA` unchanged when any color fails to parse (safe default).
|
|
91
91
|
*/
|
|
92
92
|
export declare function pickReadableForeground(fill: string, candidateA: string, candidateB: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* Axiom 12 backstop for ink on a fill: `ink` when it clears `floor` on
|
|
95
|
+
* `fill`, otherwise the pole (`#000000` / `#ffffff`) that reads better on it,
|
|
96
|
+
* when that pole beats `ink`. A mid-luminance fill, such as a literal
|
|
97
|
+
* `#cc4e00` bar or a Radix step-8 chip in dark, can sit where neither scheme
|
|
98
|
+
* anchor clears AA while a pole does. Returns `ink` unchanged when a color
|
|
99
|
+
* fails to parse.
|
|
100
|
+
*/
|
|
101
|
+
export declare function holdInkFloor(fill: string, ink: string, floor?: number): string;
|
|
93
102
|
/**
|
|
94
103
|
* Given a 12-color scale (hex strings), a background step, and a starting
|
|
95
104
|
* text step, scan towards `preferredDir` for the first step whose contrast
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"colorRules.d.ts","sourceRoot":"","sources":["../../src/theme/colorRules.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"colorRules.d.ts","sourceRoot":"","sources":["../../src/theme/colorRules.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createThemes.d.ts","sourceRoot":"","sources":["../../src/theme/createThemes.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"createThemes.d.ts","sourceRoot":"","sources":["../../src/theme/createThemes.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,wBAAwB,CAAC;AA4EhC,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB,CACrC,WAAW,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACvC,YAAY,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE;IAExC,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B,CACzC,WAAW,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACvC,YAAY,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACxC,WAAW,SAAS,OAAO,GAAG,OAAO,EACrC,YAAY,SAAS,OAAO,GAAG,OAAO,CACtC,SAAQ,sBAAsB,CAAC,WAAW,EAAE,YAAY,CAAC;IACzD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,mBAAmB,CAAC,EAAE,iBAAiB,CAAC,qBAAqB,CAAC,CAAC;IAC/D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACzF;AA8BD,eAAO,MAAM,YAAY,aA6BvB,CAAC;AA0OH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,0BAA0B,EACrC,WAAW,EAAE,sBAAsB,EACnC,OAAO,CAAC,EAAE,0BAA0B;;aAyLzB,CAAC,SAAS,MAAM,QAAQ,CAAC,SAAS,sBAAsB,GAuB1D,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,SAAS,CAAC,EAAE;;+BAAkC;EAG7E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeDevtoolsPanel.d.ts","sourceRoot":"","sources":["../../../src/theme/devtools/ThemeDevtoolsPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;
|
|
1
|
+
{"version":3,"file":"ThemeDevtoolsPanel.d.ts","sourceRoot":"","sources":["../../../src/theme/devtools/ThemeDevtoolsPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AA+LH,KAAK,UAAU,GAAG;IAChB,OAAO,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IACrC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;CAC/C,CAAC;AAsiBF,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,UAAU,CAAC;IAEjC;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAEjC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAEvB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAE7C;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAmDD,wBAAgB,kBAAkB,CAAC,EACjC,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,GACd,EAAE,uBAAuB,2CA87BzB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The animation names a Tamagui config registers, for the panel's Animation
|
|
3
|
+
* select. `createTamagui()` stores the resolved driver at `config.animations`,
|
|
4
|
+
* and every driver `createAnimations()` builds keeps its named animations one
|
|
5
|
+
* level down, on `driver.animations`. The driver's own members (`usePresence`,
|
|
6
|
+
* `useAnimations`, `inputStyle`, ...) are API, not animations (MPO-447).
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractAnimationNames(tamaguiConfig: unknown): string[];
|
|
9
|
+
//# sourceMappingURL=animationOptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animationOptions.d.ts","sourceRoot":"","sources":["../../../src/theme/devtools/animationOptions.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,OAAO,GAAG,MAAM,EAAE,CAQtE"}
|
|
@@ -3,10 +3,30 @@
|
|
|
3
3
|
* / "$color8", or an already-resolved color value — e.g. the step returned by
|
|
4
4
|
* `useAccentTintedSurface`): the scheme's paper (`$color1`) or ink (`$color12`)
|
|
5
5
|
* anchor, whichever contrasts more on the resolved fill, returned as a concrete
|
|
6
|
-
* value so no nested sub-theme can flip it.
|
|
7
|
-
*
|
|
6
|
+
* value so no nested sub-theme can flip it. When neither anchor clears AA on
|
|
7
|
+
* the fill, the pole that does (`holdInkFloor`). Returns `undefined` when any
|
|
8
|
+
* color fails to resolve/parse — callers keep their own default in that case.
|
|
8
9
|
*/
|
|
9
10
|
export declare function useReadableTextOn(fill_: string | undefined): string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Pure resolver behind {@link useLegibleInkOn}: `preferred` when it clears AA
|
|
13
|
+
* on `fill`, otherwise the readable anchor (paper or ink) held to the floor
|
|
14
|
+
* by `holdInkFloor`. Any unparseable input returns `preferred`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveLegibleInkOn({ fill, preferred, paper, ink, }: {
|
|
17
|
+
fill?: string;
|
|
18
|
+
preferred?: string;
|
|
19
|
+
paper?: string;
|
|
20
|
+
ink?: string;
|
|
21
|
+
}): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Keep a component's own ink (`preferred`, e.g. a chip's `$blue12` or a
|
|
24
|
+
* muted `$color11` title) on `fill` while it clears AA, and hand over to the
|
|
25
|
+
* readable anchor, then a pole, where it does not. The token comes back
|
|
26
|
+
* untouched when it passes, so a scheme where the pair already reads keeps
|
|
27
|
+
* its exact paint; a replacement comes back as a concrete color.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useLegibleInkOn(fill_: string | undefined, preferred: string): string;
|
|
10
30
|
/**
|
|
11
31
|
* Accent-hued text/link color readable on the page surfaces (color1/color2).
|
|
12
32
|
* Scans the theme's own accent ramp for the most chromatic step that clears
|
|
@@ -50,11 +70,11 @@ export interface ResolveAaSolidFillInput {
|
|
|
50
70
|
/**
|
|
51
71
|
* Pick an accent solid that can carry AA text (paper or ink ≥ 4.5).
|
|
52
72
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* the mark on the accent channel (LC-05) instead
|
|
57
|
-
* tinted surface.
|
|
73
|
+
* A mid-chroma solid can clear the graphic 3:1 tier and still miss the text
|
|
74
|
+
* floor with both scheme anchors (the stock dark `#7766cc` measured 4.02:1
|
|
75
|
+
* before the build held `$accentBackground` to it, MPO-448). Walking to the
|
|
76
|
+
* nearest passing solid keeps the mark on the accent channel (LC-05) instead
|
|
77
|
+
* of dropping to ink or a tinted surface.
|
|
58
78
|
*
|
|
59
79
|
* Returns the preferred token when it already clears the floor, otherwise
|
|
60
80
|
* the solid token whose resolved luminance is closest to the preferred fill.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readableColor.d.ts","sourceRoot":"","sources":["../../src/theme/readableColor.ts"],"names":[],"mappings":"AA4CA
|
|
1
|
+
{"version":3,"file":"readableColor.d.ts","sourceRoot":"","sources":["../../src/theme/readableColor.ts"],"names":[],"mappings":"AA4CA;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAU/E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,SAAS,EACT,KAAK,EACL,GAAG,GACJ,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,GAAG,SAAS,CAarB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAYpF;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CA8B3C;AA0BD,MAAM,WAAW,+BAA+B;IAC9C,mEAAmE;IACnE,KAAK,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAC9B,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,0EAA0E;AAC1E,wBAAgB,0BAA0B,CAAC,EACzC,KAAK,EACL,KAAK,EACL,GAAG,GACJ,EAAE,+BAA+B,GAAG,MAAM,GAAG,SAAS,CAQtD;AAED,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,SAAS,CAW3D;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,6DAA6D;IAC7D,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;OAGG;IACH,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,MAAM,EACN,KAAK,EACL,GAAG,EACH,KAA2B,GAC5B,EAAE,uBAAuB,GAAG,MAAM,CA0BlC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAuB7D"}
|