@multiplatform.one/theme 8.2.2 → 8.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.gnome.cjs +4 -1
- package/dist/cjs/index.gnome.native.js +3 -0
- package/dist/cjs/index.gnome.native.js.map +1 -1
- package/dist/cjs/menuRow.cjs +1 -1
- package/dist/cjs/menuRow.native.js +1 -1
- package/dist/cjs/theme/accentInk.cjs +6 -2
- package/dist/cjs/theme/accentInk.native.js +6 -2
- package/dist/cjs/theme/accentInk.native.js.map +1 -1
- package/dist/cjs/theme/chartPalette.cjs +3 -3
- package/dist/cjs/theme/chartPalette.native.js +5 -4
- package/dist/cjs/theme/chartPalette.native.js.map +1 -1
- package/dist/cjs/theme/colorRules.cjs +5 -0
- package/dist/cjs/theme/colorRules.native.js +6 -0
- package/dist/cjs/theme/colorRules.native.js.map +1 -1
- package/dist/cjs/theme/controlTiers.cjs +9 -5
- package/dist/cjs/theme/controlTiers.native.js +10 -5
- package/dist/cjs/theme/controlTiers.native.js.map +1 -1
- package/dist/cjs/theme/createThemes.cjs +212 -8
- package/dist/cjs/theme/createThemes.native.js +397 -12
- package/dist/cjs/theme/createThemes.native.js.map +1 -1
- package/dist/cjs/theme/defaults/builderOptions.cjs +9 -4
- package/dist/cjs/theme/defaults/builderOptions.native.js +9 -4
- package/dist/cjs/theme/defaults/builderOptions.native.js.map +1 -1
- package/dist/cjs/theme/fillContrast.cjs +4 -1
- package/dist/cjs/theme/fillContrast.native.js +4 -1
- package/dist/cjs/theme/fillContrast.native.js.map +1 -1
- package/dist/cjs/theme/index.cjs +1 -0
- package/dist/cjs/theme/index.native.js +1 -0
- package/dist/cjs/theme/index.native.js.map +1 -1
- package/dist/cjs/theme/nativeSchemePreference.cjs +2 -1
- package/dist/cjs/theme/nativeSchemePreference.native.js +2 -1
- package/dist/cjs/theme/nativeSchemePreference.native.js.map +1 -1
- package/dist/cjs/theme/nativeTheme.native.js.map +1 -1
- package/dist/cjs/theme/overlaySurfaces.cjs +2 -2
- package/dist/cjs/theme/overlaySurfaces.native.js +2 -2
- package/dist/cjs/theme/overlaySurfaces.native.js.map +1 -1
- package/dist/cjs/theme/readableColor.cjs +20 -9
- package/dist/cjs/theme/readableColor.native.js +20 -9
- package/dist/cjs/theme/readableColor.native.js.map +1 -1
- package/dist/cjs/theme/reducedMotion.cjs +49 -0
- package/dist/cjs/theme/reducedMotion.native.js +81 -0
- package/dist/cjs/theme/reducedMotion.native.js.map +1 -0
- package/dist/cjs/theme/resolveKnobs.cjs +4 -1
- package/dist/cjs/theme/resolveKnobs.native.js +4 -1
- package/dist/cjs/theme/resolveKnobs.native.js.map +1 -1
- package/dist/cjs/theme/tokenNumber.cjs +34 -0
- package/dist/cjs/theme/tokenNumber.native.js +39 -0
- package/dist/cjs/theme/tokenNumber.native.js.map +1 -0
- package/dist/cjs/theme/useResolvedKnobs.cjs +2 -22
- package/dist/cjs/theme/useResolvedKnobs.native.js +2 -30
- package/dist/cjs/theme/useResolvedKnobs.native.js.map +1 -1
- package/dist/esm/index.gnome.mjs +3 -2
- package/dist/esm/index.gnome.mjs.map +1 -1
- package/dist/esm/index.gnome.native.js +3 -2
- package/dist/esm/index.gnome.native.js.map +1 -1
- package/dist/esm/menuRow.mjs +1 -1
- package/dist/esm/menuRow.native.js +1 -1
- package/dist/esm/theme/accentInk.mjs +6 -3
- package/dist/esm/theme/accentInk.mjs.map +1 -1
- package/dist/esm/theme/accentInk.native.js +6 -3
- package/dist/esm/theme/accentInk.native.js.map +1 -1
- package/dist/esm/theme/chartPalette.mjs +3 -3
- package/dist/esm/theme/chartPalette.mjs.map +1 -1
- package/dist/esm/theme/chartPalette.native.js +5 -4
- package/dist/esm/theme/chartPalette.native.js.map +1 -1
- package/dist/esm/theme/colorRules.mjs +5 -1
- package/dist/esm/theme/colorRules.mjs.map +1 -1
- package/dist/esm/theme/colorRules.native.js +6 -1
- package/dist/esm/theme/colorRules.native.js.map +1 -1
- package/dist/esm/theme/controlTiers.mjs +9 -6
- package/dist/esm/theme/controlTiers.mjs.map +1 -1
- package/dist/esm/theme/controlTiers.native.js +10 -6
- package/dist/esm/theme/controlTiers.native.js.map +1 -1
- package/dist/esm/theme/createThemes.mjs +215 -12
- package/dist/esm/theme/createThemes.mjs.map +1 -1
- package/dist/esm/theme/createThemes.native.js +400 -16
- package/dist/esm/theme/createThemes.native.js.map +1 -1
- package/dist/esm/theme/defaults/builderOptions.mjs +10 -6
- package/dist/esm/theme/defaults/builderOptions.mjs.map +1 -1
- package/dist/esm/theme/defaults/builderOptions.native.js +10 -6
- package/dist/esm/theme/defaults/builderOptions.native.js.map +1 -1
- package/dist/esm/theme/fillContrast.mjs +5 -2
- package/dist/esm/theme/fillContrast.mjs.map +1 -1
- package/dist/esm/theme/fillContrast.native.js +5 -2
- package/dist/esm/theme/fillContrast.native.js.map +1 -1
- package/dist/esm/theme/index.mjs +1 -0
- package/dist/esm/theme/index.mjs.map +1 -1
- package/dist/esm/theme/index.native.js +1 -0
- package/dist/esm/theme/index.native.js.map +1 -1
- package/dist/esm/theme/nativeSchemePreference.mjs +2 -1
- package/dist/esm/theme/nativeSchemePreference.mjs.map +1 -1
- package/dist/esm/theme/nativeSchemePreference.native.js +2 -1
- package/dist/esm/theme/nativeSchemePreference.native.js.map +1 -1
- package/dist/esm/theme/nativeTheme.mjs.map +1 -1
- package/dist/esm/theme/nativeTheme.native.js.map +1 -1
- package/dist/esm/theme/overlaySurfaces.mjs +2 -2
- package/dist/esm/theme/overlaySurfaces.native.js +2 -2
- package/dist/esm/theme/readableColor.mjs +20 -9
- package/dist/esm/theme/readableColor.mjs.map +1 -1
- package/dist/esm/theme/readableColor.native.js +20 -9
- package/dist/esm/theme/readableColor.native.js.map +1 -1
- package/dist/esm/theme/reducedMotion.mjs +24 -0
- package/dist/esm/theme/reducedMotion.mjs.map +1 -0
- package/dist/esm/theme/reducedMotion.native.js +53 -0
- package/dist/esm/theme/reducedMotion.native.js.map +1 -0
- package/dist/esm/theme/resolveKnobs.mjs +4 -1
- package/dist/esm/theme/resolveKnobs.mjs.map +1 -1
- package/dist/esm/theme/resolveKnobs.native.js +4 -1
- package/dist/esm/theme/resolveKnobs.native.js.map +1 -1
- package/dist/esm/theme/tokenNumber.mjs +9 -0
- package/dist/esm/theme/tokenNumber.mjs.map +1 -0
- package/dist/esm/theme/tokenNumber.native.js +11 -0
- package/dist/esm/theme/tokenNumber.native.js.map +1 -0
- package/dist/esm/theme/useResolvedKnobs.mjs +2 -22
- package/dist/esm/theme/useResolvedKnobs.mjs.map +1 -1
- package/dist/esm/theme/useResolvedKnobs.native.js +2 -30
- package/dist/esm/theme/useResolvedKnobs.native.js.map +1 -1
- package/package.json +5 -5
- package/src/figma/figmaTokens.spec.ts +2 -1
- package/src/index.gnome.ts +2 -0
- package/src/menuRow.palette.spec.ts +20 -7
- package/src/menuRow.spec.ts +2 -2
- package/src/menuRow.ts +3 -3
- package/src/theme/accentInk.ts +8 -2
- package/src/theme/accentOnSurface.spec.tsx +6 -1
- package/src/theme/chartPalette.spec.ts +2 -2
- package/src/theme/chartPalette.ts +9 -9
- package/src/theme/colorRules.ts +13 -0
- package/src/theme/controlEdges.spec.ts +114 -0
- package/src/theme/controlTiers.ts +18 -9
- package/src/theme/createDefaultThemeConfig.spec.ts +135 -12
- package/src/theme/createThemes.ts +363 -24
- package/src/theme/defaults/builderOptions.ts +25 -8
- package/src/theme/defaults/secondaryInk.spec.ts +10 -5
- package/src/theme/fillContrast.spec.ts +8 -2
- package/src/theme/fillContrast.ts +10 -2
- package/src/theme/houseGround.spec.ts +1 -1
- package/src/theme/index.ts +1 -0
- package/src/theme/nativeSchemePreference.ts +2 -1
- package/src/theme/nativeTheme.tsx +1 -1
- package/src/theme/overlaySurfaces.tsx +2 -2
- package/src/theme/readableColor.ts +23 -11
- package/src/theme/recipes.ts +1 -0
- package/src/theme/reducedMotion.native.spec.ts +28 -0
- package/src/theme/reducedMotion.native.ts +36 -0
- package/src/theme/reducedMotion.ts +27 -0
- package/src/theme/resolveKnobs.spec.ts +4 -4
- package/src/theme/resolveKnobs.ts +6 -4
- package/src/theme/selectionFill.spec.ts +58 -0
- package/src/theme/sheetGrabber.spec.ts +4 -0
- package/src/theme/switchThumb.spec.ts +13 -1
- package/src/theme/themeKeyRegistry.spec.ts +26 -0
- package/src/theme/tokenNumber.spec.tsx +47 -0
- package/src/theme/tokenNumber.ts +18 -0
- package/src/theme/useResolvedKnobs.ts +2 -27
- package/types/index.gnome.d.ts +2 -1
- package/types/index.gnome.d.ts.map +1 -1
- package/types/menuRow.d.ts +5 -5
- package/types/theme/accentInk.d.ts +1 -0
- package/types/theme/accentInk.d.ts.map +1 -1
- package/types/theme/chartPalette.d.ts +2 -2
- package/types/theme/colorRules.d.ts +9 -0
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/controlTiers.d.ts +11 -5
- package/types/theme/controlTiers.d.ts.map +1 -1
- package/types/theme/createThemes.d.ts +21 -12
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts +9 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/fillContrast.d.ts.map +1 -1
- package/types/theme/index.d.ts +1 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/nativeSchemePreference.d.ts.map +1 -1
- package/types/theme/nativeTheme.d.ts +1 -1
- package/types/theme/nativeTheme.d.ts.map +1 -1
- package/types/theme/overlaySurfaces.d.ts +2 -2
- package/types/theme/readableColor.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +3 -0
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/reducedMotion.d.ts +3 -0
- package/types/theme/reducedMotion.d.ts.map +1 -0
- package/types/theme/reducedMotion.native.d.ts +3 -0
- package/types/theme/reducedMotion.native.d.ts.map +1 -0
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/tokenNumber.d.ts +10 -0
- package/types/theme/tokenNumber.d.ts.map +1 -0
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
|
@@ -18,12 +18,14 @@ import {
|
|
|
18
18
|
plateContrastRatio,
|
|
19
19
|
relativeLuminance,
|
|
20
20
|
switchOffFillAlpha,
|
|
21
|
+
switchOffGround,
|
|
21
22
|
systemFillHex,
|
|
22
23
|
systemFillLuminance,
|
|
23
24
|
} from "./colorRules";
|
|
24
|
-
import { pickSecondaryInk } from "./defaults/builderOptions";
|
|
25
|
+
import { darkHairlineContrast, pickSecondaryInk } from "./defaults/builderOptions";
|
|
25
26
|
import { themeRampHues } from "./ramps";
|
|
26
|
-
import { withAccentOnSurface, withIdentityInk } from "./accentInk";
|
|
27
|
+
import { pageSurfaces, pickIdentityInk, withAccentOnSurface, withIdentityInk } from "./accentInk";
|
|
28
|
+
import { resolveThemeThroughParents } from "./dedupeInherited";
|
|
27
29
|
import { tintHueNames } from "./tintHues";
|
|
28
30
|
|
|
29
31
|
export { tintHueNames };
|
|
@@ -79,9 +81,63 @@ const lightSurfaceGrouped = {
|
|
|
79
81
|
backgroundActive: PALETTE_BACKGROUND_OFFSET,
|
|
80
82
|
};
|
|
81
83
|
|
|
84
|
+
/**
|
|
85
|
+
* LC-151 in dark (FZ-8400): the page drops to the ramp's darkest step
|
|
86
|
+
* (`$color1`) and the grouped surfaces climb one step (surface1 `$color4`,
|
|
87
|
+
* surface2 `$color5`), so a card, list group or field steps off the page by at
|
|
88
|
+
* least iOS's grouped step (`#1C1C1E` on black, 1.23:1). v5 stock put
|
|
89
|
+
* surface1 one step over a `$color2` page, `#191919` on `#151515` (1.07:1),
|
|
90
|
+
* and groups vanished. A Button's surface2 climbs with them so it still sits a
|
|
91
|
+
* step off the card. Light is untouched, and the ramp stays stock v5.
|
|
92
|
+
*/
|
|
93
|
+
const fillKeys = [
|
|
94
|
+
"background",
|
|
95
|
+
"backgroundHover",
|
|
96
|
+
"backgroundPress",
|
|
97
|
+
"backgroundFocus",
|
|
98
|
+
"backgroundActive",
|
|
99
|
+
] as const;
|
|
100
|
+
const edgeKeys = [
|
|
101
|
+
"borderColor",
|
|
102
|
+
"borderColorHover",
|
|
103
|
+
"borderColorFocus",
|
|
104
|
+
"borderColorPress",
|
|
105
|
+
] as const;
|
|
106
|
+
|
|
107
|
+
function liftOneStep<T extends Record<string, number>>(template: T) {
|
|
108
|
+
const lifted: Record<string, number> = { ...template };
|
|
109
|
+
for (const key of [...fillKeys, ...edgeKeys]) lifted[key] = template[key] + 1;
|
|
110
|
+
return lifted as T;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Cards, list groups and fields in dark: surface1 lifted, and the LC-05
|
|
115
|
+
* selection fills re-pointed one step above the card's own `$color4`, so a
|
|
116
|
+
* selected or pressed row inside an inset group never lands on the group's
|
|
117
|
+
* fill. On the page they stay `$color4` and `$color5`. Each theme key costs
|
|
118
|
+
* about 1.4 KB of theme CSS, so there are two.
|
|
119
|
+
*/
|
|
120
|
+
const darkSurfaceGrouped = {
|
|
121
|
+
...liftOneStep(v5Templates.dark_surface1),
|
|
122
|
+
selectFill: PALETTE_BACKGROUND_OFFSET + 4,
|
|
123
|
+
selectPress: PALETTE_BACKGROUND_OFFSET + 5,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const darkBasePage = {
|
|
127
|
+
...v5Templates.dark_base,
|
|
128
|
+
background: PALETTE_BACKGROUND_OFFSET,
|
|
129
|
+
backgroundActive: PALETTE_BACKGROUND_OFFSET,
|
|
130
|
+
backgroundFocus: PALETTE_BACKGROUND_OFFSET + 1,
|
|
131
|
+
backgroundHover: PALETTE_BACKGROUND_OFFSET + 1,
|
|
132
|
+
backgroundPress: PALETTE_BACKGROUND_OFFSET + 1,
|
|
133
|
+
};
|
|
134
|
+
|
|
82
135
|
export const houseTemplates = {
|
|
83
136
|
...v5Templates,
|
|
84
137
|
light_surface1: lightSurfaceGrouped,
|
|
138
|
+
dark_base: darkBasePage,
|
|
139
|
+
dark_surface1: darkSurfaceGrouped,
|
|
140
|
+
dark_surface2: liftOneStep(v5Templates.dark_surface2),
|
|
85
141
|
};
|
|
86
142
|
|
|
87
143
|
export const houseComponentThemes = {
|
|
@@ -296,7 +352,15 @@ function holdAccentFloor(
|
|
|
296
352
|
if (!base?.accentBackground || !base.accentColor) continue;
|
|
297
353
|
const paper = luminanceOf(base.color1);
|
|
298
354
|
const ink = luminanceOf(base.color12);
|
|
299
|
-
|
|
355
|
+
// A dark overlay paints `$color5` (LC-85, FZ-8400), and a Toast's accent
|
|
356
|
+
// action sits on it.
|
|
357
|
+
const surfaces = [
|
|
358
|
+
base.color1,
|
|
359
|
+
base.color2,
|
|
360
|
+
base.color3,
|
|
361
|
+
base.color4,
|
|
362
|
+
...(scheme === "dark" ? [base.color5] : []),
|
|
363
|
+
].map(luminanceOf);
|
|
300
364
|
if (paper === null || ink === null || surfaces.some((lum) => lum === null)) continue;
|
|
301
365
|
const steps = Array.from({ length: 12 }, (_, index) => base[`accent${index + 1}`]);
|
|
302
366
|
const fill = nearestHoldingStep(
|
|
@@ -367,12 +431,188 @@ function holdSelectionSurface(
|
|
|
367
431
|
...out[name],
|
|
368
432
|
color4: held,
|
|
369
433
|
...(theme.borderColor === scope.color4 ? { borderColor: held } : null),
|
|
434
|
+
...(theme.selectFill === scope.color4 ? { selectFill: held } : null),
|
|
370
435
|
};
|
|
371
436
|
}
|
|
372
437
|
}
|
|
373
438
|
return out;
|
|
374
439
|
}
|
|
375
440
|
|
|
441
|
+
/**
|
|
442
|
+
* LC-148 in dark (FZ-9281): a dark scope's hairline is the first ramp step
|
|
443
|
+
* that clears `darkHairlineContrast` against its page and its grouped surface
|
|
444
|
+
* (`$color4`), and its medium edge is the next step up. One step toward the
|
|
445
|
+
* page put the hairline under the card's own fill, and the medium edge on the
|
|
446
|
+
* card's fill, so both vanished. The picks are measured per scope, so a ramp
|
|
447
|
+
* change moves them with it. Edge states keep their offsets from the medium
|
|
448
|
+
* edge. Light keeps v5's edges.
|
|
449
|
+
*/
|
|
450
|
+
function holdDarkEdges(
|
|
451
|
+
themes: Record<string, Record<string, string>>,
|
|
452
|
+
): Record<string, Record<string, string>> {
|
|
453
|
+
const out: Record<string, Record<string, string>> = { ...themes };
|
|
454
|
+
const scopes = Object.keys(themes).filter((name) => /^dark(_[a-z]+)?$/.test(name));
|
|
455
|
+
const ownerOf = (name: string) =>
|
|
456
|
+
scopes
|
|
457
|
+
.filter((scope) => name === scope || name.startsWith(`${scope}_`))
|
|
458
|
+
.reduce((longest, scope) => (scope.length > longest.length ? scope : longest), "");
|
|
459
|
+
for (const scopeName of scopes) {
|
|
460
|
+
if (/_(accent|active)$/.test(scopeName)) continue;
|
|
461
|
+
const scope = themes[scopeName]!;
|
|
462
|
+
const ramp = Array.from({ length: 12 }, (_, index) => scope[`color${index + 1}`]);
|
|
463
|
+
const grounds = [scope.background, scope.color4].map((color) => luminanceOf(color));
|
|
464
|
+
if (grounds.some((ground) => ground === null)) continue;
|
|
465
|
+
const brightest = Math.max(...(grounds as number[]));
|
|
466
|
+
const clears = (color: string | undefined) => {
|
|
467
|
+
const luminance = luminanceOf(color);
|
|
468
|
+
return (
|
|
469
|
+
luminance !== null &&
|
|
470
|
+
luminance > brightest &&
|
|
471
|
+
(grounds as number[]).every(
|
|
472
|
+
(ground) => contrastRatio(luminance, ground) >= darkHairlineContrast,
|
|
473
|
+
)
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
const hairline = ramp.findIndex((color, index) => index < 11 && clears(color));
|
|
477
|
+
if (hairline < 0) continue;
|
|
478
|
+
const medium = hairline + 1;
|
|
479
|
+
for (const [name, theme] of Object.entries(themes)) {
|
|
480
|
+
if (ownerOf(name) !== scopeName) continue;
|
|
481
|
+
const surface = name === scopeName || theme.background === scope.color4;
|
|
482
|
+
const step = ramp.indexOf(theme.borderColor);
|
|
483
|
+
const shift = surface && step >= 0 ? medium - step : 0;
|
|
484
|
+
const moved: Record<string, string> = {};
|
|
485
|
+
if ("borderColorHairline" in theme || name === scopeName)
|
|
486
|
+
moved.borderColorHairline = ramp[hairline]!;
|
|
487
|
+
if (shift > 0) {
|
|
488
|
+
for (const key of [
|
|
489
|
+
"borderColor",
|
|
490
|
+
"borderColorHover",
|
|
491
|
+
"borderColorFocus",
|
|
492
|
+
"borderColorPress",
|
|
493
|
+
]) {
|
|
494
|
+
const at = ramp.indexOf(theme[key]);
|
|
495
|
+
if (at >= 0) moved[key] = ramp[Math.min(11, at + shift)]!;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
out[name] = { ...out[name], ...moved };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* LC-299 CONTROL-EDGE (FZ-9358, WCAG 1.4.11): a field's edge
|
|
506
|
+
* (`$borderColorField`) is the first neutral step at 3:1 on the page and on
|
|
507
|
+
* the field's own fill, and its hover never drops under it.
|
|
508
|
+
* `$borderColorControl` is the same pick on the page alone, for the segmented
|
|
509
|
+
* frame and the light off Switch track. Dark's off track stays edgeless: its
|
|
510
|
+
* white thumb already clears 3:1. A semantic intent's field edge is the first
|
|
511
|
+
* step of its own ramp at 3:1 on every page and on its fill, so error stays red.
|
|
512
|
+
* Picks sit on the scheme roots and a scope overrides only where its own
|
|
513
|
+
* differs: one copy per component sub-theme overruns the CSS budget.
|
|
514
|
+
*/
|
|
515
|
+
function holdControlEdges(
|
|
516
|
+
themes: Record<string, Record<string, string>>,
|
|
517
|
+
): Record<string, Record<string, string>> {
|
|
518
|
+
const out: Record<string, Record<string, string>> = { ...themes };
|
|
519
|
+
const firstClearing = (neutral: string[], grounds: (string | undefined)[]) => {
|
|
520
|
+
const lums = grounds.map((ground) => luminanceOf(ground));
|
|
521
|
+
if (lums.some((lum) => lum === null)) return -1;
|
|
522
|
+
const darkest = Math.min(...(lums as number[]));
|
|
523
|
+
const brightest = Math.max(...(lums as number[]));
|
|
524
|
+
const light = isLightGround(grounds[0]!);
|
|
525
|
+
return neutral.findIndex((color) => {
|
|
526
|
+
const lum = luminanceOf(color);
|
|
527
|
+
return (
|
|
528
|
+
lum !== null &&
|
|
529
|
+
(light ? lum < darkest : lum > brightest) &&
|
|
530
|
+
(lums as number[]).every((ground) => contrastRatio(lum, ground) >= minContrastRatio)
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
const resolved = (name: string, key: string): string | undefined => {
|
|
535
|
+
for (let at = name; at; at = at.slice(0, Math.max(0, at.lastIndexOf("_")))) {
|
|
536
|
+
const value = (out[at] ?? themes[at])?.[key];
|
|
537
|
+
if (value !== undefined) return value;
|
|
538
|
+
if (!at.includes("_")) break;
|
|
539
|
+
}
|
|
540
|
+
return undefined;
|
|
541
|
+
};
|
|
542
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
543
|
+
const root = themes[scheme];
|
|
544
|
+
if (!root) continue;
|
|
545
|
+
const neutral = Array.from({ length: 12 }, (_, index) => root[`color${index + 1}`]!);
|
|
546
|
+
const fieldEdges = (scopeName: string) => {
|
|
547
|
+
const scope = themes[scopeName]!;
|
|
548
|
+
const field = `${scopeName}_Input`;
|
|
549
|
+
const edge = firstClearing(neutral, [scope.background, resolved(field, "background")]);
|
|
550
|
+
if (edge < 0) return undefined;
|
|
551
|
+
const ramp = Array.from({ length: 12 }, (_, index) => scope[`color${index + 1}`]);
|
|
552
|
+
const stepOf = (color: string | undefined) => {
|
|
553
|
+
const lum = luminanceOf(color);
|
|
554
|
+
if (lum === null) return -1;
|
|
555
|
+
return ramp.reduce(
|
|
556
|
+
(best, step, index) =>
|
|
557
|
+
Math.abs((luminanceOf(step) ?? 2) - lum) <
|
|
558
|
+
Math.abs((luminanceOf(ramp[best]) ?? 2) - lum)
|
|
559
|
+
? index
|
|
560
|
+
: best,
|
|
561
|
+
0,
|
|
562
|
+
);
|
|
563
|
+
};
|
|
564
|
+
const rest = stepOf(resolved(field, "borderColor"));
|
|
565
|
+
const hover = stepOf(resolved(field, "borderColorHover"));
|
|
566
|
+
return {
|
|
567
|
+
borderColorField: neutral[edge]!,
|
|
568
|
+
borderColorFieldHover: neutral[Math.min(11, Math.max(edge, edge + hover - rest))]!,
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
const rootFields = fieldEdges(scheme);
|
|
572
|
+
const rootControl = firstClearing(neutral, [root.background]);
|
|
573
|
+
if (!rootFields || rootControl < 0) continue;
|
|
574
|
+
out[scheme] = {
|
|
575
|
+
...out[scheme],
|
|
576
|
+
...rootFields,
|
|
577
|
+
borderColorControl: neutral[rootControl]!,
|
|
578
|
+
switchTrackOffBorder: scheme === "light" ? neutral[rootControl]! : root.switchTrackOff!,
|
|
579
|
+
};
|
|
580
|
+
const scopes = Object.keys(themes).filter(
|
|
581
|
+
(name) => new RegExp(`^${scheme}_[a-z]+$`).test(name) && !/_(accent|active)$/.test(name),
|
|
582
|
+
);
|
|
583
|
+
const pages = [
|
|
584
|
+
root.background,
|
|
585
|
+
...scopes
|
|
586
|
+
.filter((name) => !semanticIntentNames.has(name.slice(scheme.length + 1)))
|
|
587
|
+
.map((name) => themes[name]!.background),
|
|
588
|
+
];
|
|
589
|
+
for (const scopeName of scopes) {
|
|
590
|
+
const scope = themes[scopeName]!;
|
|
591
|
+
const patch: Record<string, string> = {};
|
|
592
|
+
if (scheme === "dark" && "switchTrackOff" in scope)
|
|
593
|
+
patch.switchTrackOffBorder = scope.switchTrackOff!;
|
|
594
|
+
if (semanticIntentNames.has(scopeName.slice(scheme.length + 1))) {
|
|
595
|
+
const field = `${scopeName}_Input`;
|
|
596
|
+
const ramp = Array.from({ length: 12 }, (_, index) => scope[`color${index + 1}`]!);
|
|
597
|
+
const edge = firstClearing(ramp, [...pages, resolved(field, "background")]);
|
|
598
|
+
patch.borderColorField = edge < 0 ? resolved(field, "borderColor")! : ramp[edge]!;
|
|
599
|
+
patch.borderColorFieldHover =
|
|
600
|
+
edge < 0 ? resolved(field, "borderColorHover")! : ramp[Math.min(11, edge + 1)]!;
|
|
601
|
+
} else {
|
|
602
|
+
const fields = fieldEdges(scopeName);
|
|
603
|
+
if (fields) {
|
|
604
|
+
for (const [key, value] of Object.entries(fields))
|
|
605
|
+
if (value !== rootFields[key as keyof typeof rootFields]) patch[key] = value;
|
|
606
|
+
}
|
|
607
|
+
const control = firstClearing(neutral, [scope.background]);
|
|
608
|
+
if (control >= 0 && control !== rootControl) patch.borderColorControl = neutral[control]!;
|
|
609
|
+
}
|
|
610
|
+
if (Object.keys(patch).length > 0) out[scopeName] = { ...out[scopeName], ...patch };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
|
|
376
616
|
/**
|
|
377
617
|
* LC-131 SELECTED-STATE. A selected choice part paints a soft accent tint with
|
|
378
618
|
* accent ink. The tint is the accent step nearest the scheme's neutral
|
|
@@ -475,7 +715,10 @@ const switchStateContrastRatio = 1.5;
|
|
|
475
715
|
const switchRaisedAlphas = [0.16, 0.2, 0.24, 0.28, 0.32, 0.36, 0.4];
|
|
476
716
|
const switchRaisedAlphaStep = 0.04;
|
|
477
717
|
|
|
478
|
-
/**
|
|
718
|
+
/**
|
|
719
|
+
* Dark's raised grounds under a Switch: the `$color4` card or field it sits in
|
|
720
|
+
* and the `$color5` overlay (LC-151 and LC-85, FZ-8400; FZ-7944's plate floor).
|
|
721
|
+
*/
|
|
479
722
|
function raisedGrounds(
|
|
480
723
|
themes: Record<string, Record<string, string>>,
|
|
481
724
|
scheme: "light" | "dark",
|
|
@@ -484,9 +727,9 @@ function raisedGrounds(
|
|
|
484
727
|
const hues = Object.entries(themes)
|
|
485
728
|
.filter(([name]) => new RegExp(`^${scheme}_[a-z]+$`).test(name))
|
|
486
729
|
.filter(([name]) => !/_(accent|active)$/.test(name));
|
|
487
|
-
return [themes[scheme]
|
|
488
|
-
(
|
|
489
|
-
|
|
730
|
+
return [themes[scheme], ...hues.map(([, theme]) => theme)]
|
|
731
|
+
.flatMap((theme) => [theme?.color4, theme?.color5])
|
|
732
|
+
.filter((color): color is string => typeof color === "string");
|
|
490
733
|
}
|
|
491
734
|
|
|
492
735
|
function holdSwitchTrack(
|
|
@@ -496,13 +739,36 @@ function holdSwitchTrack(
|
|
|
496
739
|
for (const scheme of ["light", "dark"] as const) {
|
|
497
740
|
const base = themes[scheme];
|
|
498
741
|
if (!base) continue;
|
|
742
|
+
// A dark switch sits on the `$color4` card or field (LC-151, FZ-8400), as
|
|
743
|
+
// UISwitch does on a grouped cell, so its band is measured there.
|
|
499
744
|
const paint = switchTrackPaint(
|
|
500
745
|
base,
|
|
501
746
|
base.accentBackground,
|
|
502
|
-
base
|
|
747
|
+
switchOffGround(base),
|
|
503
748
|
raisedGrounds(themes, scheme),
|
|
504
749
|
);
|
|
505
750
|
if (paint) out[scheme] = { ...base, ...paint };
|
|
751
|
+
if (scheme !== "dark" || !paint) continue;
|
|
752
|
+
// A semantic intent's card is its own hue's `$color4`: repaint where the
|
|
753
|
+
// root's off track leaves the band or the plate floor there.
|
|
754
|
+
for (const intent of semanticIntentNames) {
|
|
755
|
+
const scope = themes[`${scheme}_${intent}`];
|
|
756
|
+
const off = luminanceOf(paint.switchTrackOff);
|
|
757
|
+
if (!scope?.color4 || !scope.color5 || off === null) continue;
|
|
758
|
+
const holds =
|
|
759
|
+
inSwitchOffBand(paint.switchTrackOff, scope.color4) &&
|
|
760
|
+
[scope.color4, scope.color5].every(
|
|
761
|
+
(ground) => contrastRatio(off, luminanceOf(ground) ?? off) >= plateContrastRatio,
|
|
762
|
+
);
|
|
763
|
+
if (holds) continue;
|
|
764
|
+
const track = switchTrackPaint(
|
|
765
|
+
base,
|
|
766
|
+
scope.accentBackground ?? base.accentBackground,
|
|
767
|
+
scope.color4,
|
|
768
|
+
[base.color4, scope.color4, scope.color5],
|
|
769
|
+
);
|
|
770
|
+
if (track) out[`${scheme}_${intent}`] = { ...scope, ...track };
|
|
771
|
+
}
|
|
506
772
|
}
|
|
507
773
|
return out;
|
|
508
774
|
}
|
|
@@ -766,7 +1032,13 @@ function holdColorThemeAccent(
|
|
|
766
1032
|
if (!scope || ramp.some((step) => !step)) continue;
|
|
767
1033
|
const paper = luminanceOf(scope.color1);
|
|
768
1034
|
const ink = luminanceOf(scope.color12);
|
|
769
|
-
const surfaces = [
|
|
1035
|
+
const surfaces = [
|
|
1036
|
+
scope.color1,
|
|
1037
|
+
scope.color2,
|
|
1038
|
+
scope.color3,
|
|
1039
|
+
scope.color4,
|
|
1040
|
+
...(scheme === "dark" ? [scope.color5] : []),
|
|
1041
|
+
].map(luminanceOf);
|
|
770
1042
|
if (paper === null || ink === null || surfaces.some((lum) => lum === null)) continue;
|
|
771
1043
|
const labels = [scope.color1, scope.color12].sort(
|
|
772
1044
|
(a, b) => (luminanceOf(b) as number) - (luminanceOf(a) as number),
|
|
@@ -798,17 +1070,22 @@ function holdColorThemeAccent(
|
|
|
798
1070
|
// A hue fill can sit on the root's off grey: repaint inside the band.
|
|
799
1071
|
const rootOn = luminanceOf(base.switchTrackOff);
|
|
800
1072
|
const fillLum = luminanceOf(fill);
|
|
1073
|
+
const switchGround = switchOffGround(scope) ?? scope.background;
|
|
801
1074
|
const rootHolds =
|
|
802
1075
|
rootOn === null ||
|
|
803
1076
|
fillLum === null ||
|
|
804
|
-
contrastRatio(rootOn, fillLum) >= switchStateContrastRatio
|
|
1077
|
+
(contrastRatio(rootOn, fillLum) >= switchStateContrastRatio &&
|
|
1078
|
+
inSwitchOffBand(base.switchTrackOff, switchGround) &&
|
|
1079
|
+
(isLightGround(scope.background) ? [] : [scope.color4, scope.color5]).every(
|
|
1080
|
+
(ground) => contrastRatio(rootOn, luminanceOf(ground) ?? rootOn) >= plateContrastRatio,
|
|
1081
|
+
));
|
|
805
1082
|
const track = rootHolds
|
|
806
1083
|
? undefined
|
|
807
1084
|
: switchTrackPaint(
|
|
808
1085
|
base,
|
|
809
1086
|
fill,
|
|
810
|
-
|
|
811
|
-
isLightGround(scope.background) ? [] : [scope.color4],
|
|
1087
|
+
switchGround,
|
|
1088
|
+
isLightGround(scope.background) ? [] : [base.color4, scope.color4, scope.color5],
|
|
812
1089
|
);
|
|
813
1090
|
for (const [name, theme] of Object.entries(themes)) {
|
|
814
1091
|
if (name === scopeName) {
|
|
@@ -943,6 +1220,41 @@ function holdHouseAccent(
|
|
|
943
1220
|
return out;
|
|
944
1221
|
}
|
|
945
1222
|
|
|
1223
|
+
/**
|
|
1224
|
+
* FZ-10073 with FZ-8400's dark overlay. The house root's identity ink is the
|
|
1225
|
+
* blue colour theme's. Judged on the neutral page alone, dark house picked
|
|
1226
|
+
* blue10 #3b9eff, which fails AA on blue's own `$color5` overlay, where blue
|
|
1227
|
+
* picks blue11 #70b8ff. A house theme (the scheme root, its components and
|
|
1228
|
+
* the intents that followed it) takes blue's ink when that ink also holds AA
|
|
1229
|
+
* on its own page surfaces, so the bar glyph is one blue in either scope.
|
|
1230
|
+
*/
|
|
1231
|
+
function holdHouseIdentityInk(
|
|
1232
|
+
themes: Record<string, Record<string, string>>,
|
|
1233
|
+
): Record<string, Record<string, string>> {
|
|
1234
|
+
const out: Record<string, Record<string, string>> = { ...themes };
|
|
1235
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
1236
|
+
const base = themes[scheme];
|
|
1237
|
+
const blueInk = themes[`${scheme}_blue`]?.identityInk;
|
|
1238
|
+
if (!base?.identityInk || !blueInk || base.identityInk === blueInk) continue;
|
|
1239
|
+
const held = Array.from({ length: 12 }, (_, index) => index + 1).every(
|
|
1240
|
+
(step) =>
|
|
1241
|
+
base[`accent${step}`] !== undefined && base[`accent${step}`] === base[`blue${step}`],
|
|
1242
|
+
);
|
|
1243
|
+
if (!held) continue;
|
|
1244
|
+
for (const [name, theme] of Object.entries(themes)) {
|
|
1245
|
+
if (name !== scheme && !name.startsWith(`${scheme}_`)) continue;
|
|
1246
|
+
const scope = name.split("_")[1];
|
|
1247
|
+
if (scope === "accent" || (childrenHues as readonly string[]).includes(scope)) continue;
|
|
1248
|
+
if (theme.identityInk !== base.identityInk) continue;
|
|
1249
|
+
const values = resolveThemeThroughParents(themes, name);
|
|
1250
|
+
if (pickIdentityInk([blueInk], pageSurfaces(name, values)) === blueInk) {
|
|
1251
|
+
out[name] = { ...theme, identityInk: blueInk };
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return out;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
946
1258
|
const defaultLightShadows: Shadows = {
|
|
947
1259
|
shadow1: "rgba(0,0,0,0.04)",
|
|
948
1260
|
shadow2: "rgba(0,0,0,0.08)",
|
|
@@ -1021,6 +1333,27 @@ function isChromatic(color: string | undefined): boolean {
|
|
|
1021
1333
|
return Math.max(...channels) - Math.min(...channels) >= 24;
|
|
1022
1334
|
}
|
|
1023
1335
|
|
|
1336
|
+
/**
|
|
1337
|
+
* Theme keys that only the post-passes in {@link createThemesBuilder} write
|
|
1338
|
+
* (the hold* passes, withAccentOnSurface, withIdentityInk). The `$token` lint
|
|
1339
|
+
* in public/config/lint.mjs reads this list, since it cannot run the passes,
|
|
1340
|
+
* and themeKeyRegistry.spec fails when a pass adds a key missing here.
|
|
1341
|
+
*/
|
|
1342
|
+
export const postPassThemeKeys = [
|
|
1343
|
+
"accentOnSurface",
|
|
1344
|
+
"borderColorControl",
|
|
1345
|
+
"borderColorField",
|
|
1346
|
+
"borderColorFieldHover",
|
|
1347
|
+
"borderColorHairline",
|
|
1348
|
+
"identityInk",
|
|
1349
|
+
"selectedBackground",
|
|
1350
|
+
"selectedColor",
|
|
1351
|
+
"switchTrackOff",
|
|
1352
|
+
"switchTrackOffBorder",
|
|
1353
|
+
"switchTrackOffHover",
|
|
1354
|
+
"switchTrackOffPress",
|
|
1355
|
+
] as const;
|
|
1356
|
+
|
|
1024
1357
|
export function createThemesBuilder(
|
|
1025
1358
|
baseTheme: BaseThemeBuilderDefinition,
|
|
1026
1359
|
accentTheme: ThemeBuilderDefinition,
|
|
@@ -1090,18 +1423,24 @@ export function createThemesBuilder(
|
|
|
1090
1423
|
const initialTheme = tamaguiCreateThemes(
|
|
1091
1424
|
createThemesProps as Parameters<typeof tamaguiCreateThemes>[0],
|
|
1092
1425
|
);
|
|
1093
|
-
const themes =
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1426
|
+
const themes = holdHouseIdentityInk(
|
|
1427
|
+
withIdentityInk(
|
|
1428
|
+
withAccentOnSurface(
|
|
1429
|
+
holdFocusRingAccent(
|
|
1430
|
+
holdControlEdges(
|
|
1431
|
+
holdHouseAccent(
|
|
1432
|
+
holdColorThemeAccent(
|
|
1433
|
+
holdSwitchTrack(
|
|
1434
|
+
holdSyntaxFloor(
|
|
1435
|
+
holdNamedInkFloor(
|
|
1436
|
+
holdSelectedState(
|
|
1437
|
+
holdAccentFloor(
|
|
1438
|
+
holdDarkEdges(
|
|
1439
|
+
holdSelectionSurface(
|
|
1440
|
+
neutralizeSemanticEntryText(neutralizeTintText(initialTheme)),
|
|
1441
|
+
),
|
|
1442
|
+
),
|
|
1443
|
+
),
|
|
1105
1444
|
),
|
|
1106
1445
|
),
|
|
1107
1446
|
),
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { CreateThemesBuilderOptions, GetThemeProps } from "../createThemes";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
contrastRatio,
|
|
4
|
+
isLightGround,
|
|
5
|
+
minContrastRatio,
|
|
6
|
+
normalizeToHex,
|
|
7
|
+
relativeLuminance,
|
|
8
|
+
} from "../colorRules";
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* DG-A11Y-01 / DF-03. The external focus ring must clear 3:1 against both page
|
|
@@ -62,6 +68,14 @@ function pickBoundaryColor(theme: Record<string, string>): string {
|
|
|
62
68
|
return best;
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
/**
|
|
72
|
+
* LC-148 HAIRLINE-TOKEN in dark (FZ-9281): the least contrast a dark hairline
|
|
73
|
+
* holds against the page and against the grouped surface it divides. iOS's
|
|
74
|
+
* dark `opaqueSeparator` #38383A over `secondarySystemGroupedBackground`
|
|
75
|
+
* #1C1C1E measures 1.454:1. The medium edge sits one ramp step above it.
|
|
76
|
+
*/
|
|
77
|
+
export const darkHairlineContrast = 1.454;
|
|
78
|
+
|
|
65
79
|
/**
|
|
66
80
|
* LC-148 HAIRLINE-TOKEN: `$borderColor` one ramp step toward the page. Where
|
|
67
81
|
* the `small` border stop can't draw finer than 1px (web), the resolver paints
|
|
@@ -84,7 +98,8 @@ function pickHairlineBorderColor(
|
|
|
84
98
|
* and the pick is the most muted ramp step that clears it. The stock v5 ramp
|
|
85
99
|
* is not symmetric: light $color11 is 14% lightness, two points off the
|
|
86
100
|
* primary ink, so one fixed step cannot read as secondary in both schemes.
|
|
87
|
-
* On the default ramp this lands on $color9 in light and $
|
|
101
|
+
* On the default ramp this lands on $color9 in light and $color11 in dark,
|
|
102
|
+
* where cards sit on $color4 (FZ-8400).
|
|
88
103
|
*/
|
|
89
104
|
export const secondaryInkContrast = 7;
|
|
90
105
|
|
|
@@ -97,9 +112,10 @@ export function pickSecondaryInk(
|
|
|
97
112
|
ramp: Record<string, string> = theme,
|
|
98
113
|
): string {
|
|
99
114
|
const fallback = ramp.color11 ?? ramp.color12 ?? "transparent";
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
);
|
|
115
|
+
const tiers = [theme.background, theme.color1, theme.color2, theme.color3];
|
|
116
|
+
// Dark cards and fields sit on `$color4` (FZ-8400).
|
|
117
|
+
if (theme.background && !isLightGround(theme.background)) tiers.push(theme.color4);
|
|
118
|
+
const surfaces = tiers.map((raw) => normalizeToHex(raw ?? ""));
|
|
103
119
|
if (surfaces.some((hex) => !hex)) return fallback;
|
|
104
120
|
const luminances = surfaces.map((hex) => relativeLuminance(hex!));
|
|
105
121
|
for (const key of [8, 9, 10, 11]) {
|
|
@@ -125,11 +141,12 @@ function getBaseTheme(theme: Record<string, string>) {
|
|
|
125
141
|
textMuted: pickSecondaryInk(theme),
|
|
126
142
|
textSubtle: theme.color10,
|
|
127
143
|
borderStrong: theme.color7,
|
|
128
|
-
borderStrongHover: theme.color8,
|
|
129
|
-
borderStrongPress: theme.color6,
|
|
130
144
|
surfaceStrong: theme.color7,
|
|
131
145
|
surfaceMuted: theme.color10,
|
|
132
|
-
|
|
146
|
+
// LC-05 selection fills (componentColors, formSelectionColors). A dark
|
|
147
|
+
// card re-points them one step above its own fill (createThemes).
|
|
148
|
+
selectFill: theme.color4,
|
|
149
|
+
selectPress: theme.color5,
|
|
133
150
|
};
|
|
134
151
|
}
|
|
135
152
|
|
|
@@ -14,17 +14,22 @@ const built = createThemesBuilder(
|
|
|
14
14
|
).themes() as Themes;
|
|
15
15
|
|
|
16
16
|
const luminance = (raw: string) => relativeLuminance(normalizeToHex(raw)!);
|
|
17
|
+
// A dark card is `$color4` (LC-151, FZ-8400), so dark checks that tier too.
|
|
17
18
|
const minContrast = (theme: Record<string, string>, ink: string) =>
|
|
18
19
|
Math.min(
|
|
19
|
-
...[
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
...[
|
|
21
|
+
theme.background!,
|
|
22
|
+
theme.color1!,
|
|
23
|
+
theme.color2!,
|
|
24
|
+
theme.color3!,
|
|
25
|
+
...(luminance(theme.background!) < 0.18 ? [theme.color4!] : []),
|
|
26
|
+
].map((surface) => contrastRatio(luminance(ink), luminance(surface))),
|
|
22
27
|
);
|
|
23
28
|
|
|
24
29
|
describe("LC-152 secondary ink", () => {
|
|
25
|
-
it("lands on $color9 in light and $
|
|
30
|
+
it("lands on $color9 in light and $color11 in dark on the default ramp", () => {
|
|
26
31
|
expect(built.light!.textMuted).toBe(built.light!.color9);
|
|
27
|
-
expect(built.dark!.textMuted).toBe(built.dark!.
|
|
32
|
+
expect(built.dark!.textMuted).toBe(built.dark!.color11);
|
|
28
33
|
});
|
|
29
34
|
|
|
30
35
|
it("clears 7:1 on the page and every card tier in both schemes", () => {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
contrastRatio,
|
|
23
23
|
fillLabelLc,
|
|
24
24
|
inSwitchOffBand,
|
|
25
|
+
switchOffGround,
|
|
25
26
|
normalizeToHex,
|
|
26
27
|
relativeLuminance,
|
|
27
28
|
} from "./colorRules";
|
|
@@ -133,9 +134,14 @@ describe("LC-172 the engine keeps every off toggle in the HIG band, a preset's o
|
|
|
133
134
|
});
|
|
134
135
|
|
|
135
136
|
for (const name of scopes) {
|
|
136
|
-
it(`${name}: the off track sits in the system-fill band over the
|
|
137
|
+
it(`${name}: the off track sits in the system-fill band over its ground (the dark card)`, () => {
|
|
137
138
|
const off = String(resolved[name].switchTrackOff);
|
|
138
|
-
const ground = String(
|
|
139
|
+
const ground = String(
|
|
140
|
+
switchOffGround({
|
|
141
|
+
background: String(resolved[name].background),
|
|
142
|
+
color4: String(resolved[name].color4),
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
139
145
|
expect(inSwitchOffBand(off, ground), `${off} on ${ground}`).toBe(true);
|
|
140
146
|
});
|
|
141
147
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { accentIdentityCandidates, resolveChartPalette } from "./chartPalette";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
apcaContrast,
|
|
4
|
+
fillLabelLc,
|
|
5
|
+
inSwitchOffBand,
|
|
6
|
+
normalizeToHex,
|
|
7
|
+
switchOffGround,
|
|
8
|
+
} from "./colorRules";
|
|
3
9
|
import { tintHueNames } from "./createThemes";
|
|
4
10
|
|
|
5
11
|
export { apcaContrast };
|
|
@@ -129,7 +135,9 @@ export function auditPresetFills(
|
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
const off = text(scope.switchTrackOff);
|
|
132
|
-
const ground = text(
|
|
138
|
+
const ground = text(
|
|
139
|
+
switchOffGround({ background: text(scope.background), color4: text(scope.color4) }),
|
|
140
|
+
);
|
|
133
141
|
if (!off || !ground || !inSwitchOffBand(off, ground)) {
|
|
134
142
|
findings.push({
|
|
135
143
|
preset,
|
package/src/theme/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from "./intents";
|
|
|
25
25
|
export * from "./knobs";
|
|
26
26
|
export * from "./layoutTokens";
|
|
27
27
|
export * from "./pageGutter";
|
|
28
|
+
export * from "./tokenNumber";
|
|
28
29
|
export * from "./Preset";
|
|
29
30
|
export type { Preset as PresetDefinition, IntentOverride } from "./preset.types";
|
|
30
31
|
export * from "./PresetContext";
|
|
@@ -50,9 +50,10 @@ export function useSchemePreference(): SchemePreference {
|
|
|
50
50
|
getNativeScheme();
|
|
51
51
|
Appearance.setColorScheme(setting === "system" ? "unspecified" : setting);
|
|
52
52
|
}, [setting]);
|
|
53
|
+
const resolved = setting === "system" ? os : setting;
|
|
53
54
|
return {
|
|
54
55
|
setting: hydrated ? setting : "system",
|
|
55
|
-
value:
|
|
56
|
+
value: hydrated ? resolved : "light",
|
|
56
57
|
set: setStoredScheme,
|
|
57
58
|
};
|
|
58
59
|
}
|
|
@@ -47,7 +47,7 @@ export function ThemeProvider<T extends CreateTamaguiProps>({
|
|
|
47
47
|
config,
|
|
48
48
|
systemTheme,
|
|
49
49
|
...tamaguiProps
|
|
50
|
-
}: ThemeProviderProps<T
|
|
50
|
+
}: Readonly<ThemeProviderProps<T>>) {
|
|
51
51
|
const scheme = useSchemePreference();
|
|
52
52
|
const activeTheme = systemTheme ?? scheme.value;
|
|
53
53
|
const ancestor = useContext(PresetContext);
|