@multiplatform.one/theme 7.7.6 → 7.10.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.
Files changed (47) hide show
  1. package/package.json +5 -5
  2. package/src/audit/constraintAudit.spec.ts +157 -0
  3. package/src/audit/constraintAudit.ts +154 -35
  4. package/src/audit/constraintScope.spec.ts +48 -0
  5. package/src/audit/index.ts +16 -0
  6. package/src/audit/themeMatrix.spec.ts +292 -1
  7. package/src/audit/themeMatrix.ts +1015 -22
  8. package/src/theme/Intent.stories.tsx +88 -0
  9. package/src/theme/Preset.stories.tsx +80 -0
  10. package/src/theme/Surface.spec.tsx +6 -0
  11. package/src/theme/Surface.stories.tsx +91 -0
  12. package/src/theme/ThemeProvider.stories.tsx +50 -0
  13. package/src/theme/Tint.stories.tsx +98 -0
  14. package/src/theme/chartPalette.spec.ts +128 -5
  15. package/src/theme/chartPalette.ts +105 -22
  16. package/src/theme/colorRules.spec.ts +92 -39
  17. package/src/theme/colorRules.ts +4 -6
  18. package/src/theme/createDefaultThemeConfig.ts +1 -1
  19. package/src/theme/createThemes.ts +58 -3
  20. package/src/theme/devtools/ColorLineVisualizer.stories.tsx +36 -0
  21. package/src/theme/devtools/ThemeDevtoolsPanel.stories.tsx +16 -0
  22. package/src/theme/glyphPaint.spec.ts +14 -0
  23. package/src/theme/glyphPaint.ts +2 -1
  24. package/src/theme/intent.spec.tsx +1 -0
  25. package/src/theme/layoutTokensHooks.spec.tsx +1 -0
  26. package/src/theme/recipeInputs.ts +9 -9
  27. package/src/theme/resolveKnobs.spec.ts +2 -2
  28. package/src/theme/sizeLadder.spec.ts +19 -10
  29. package/src/theme/sizeRecipes.ts +2 -2
  30. package/src/theme/themeValue.spec.ts +2 -2
  31. package/src/theme/useResolvedKnobsBehavior.spec.tsx +6 -0
  32. package/types/audit/constraintAudit.d.ts +17 -1
  33. package/types/audit/constraintAudit.d.ts.map +1 -1
  34. package/types/audit/index.d.ts +2 -2
  35. package/types/audit/index.d.ts.map +1 -1
  36. package/types/audit/themeMatrix.d.ts +135 -2
  37. package/types/audit/themeMatrix.d.ts.map +1 -1
  38. package/types/theme/chartPalette.d.ts +6 -4
  39. package/types/theme/chartPalette.d.ts.map +1 -1
  40. package/types/theme/colorRules.d.ts +3 -3
  41. package/types/theme/colorRules.d.ts.map +1 -1
  42. package/types/theme/createThemes.d.ts +1 -1
  43. package/types/theme/createThemes.d.ts.map +1 -1
  44. package/types/theme/glyphPaint.d.ts.map +1 -1
  45. package/types/theme/recipeInputs.d.ts +8 -8
  46. package/types/theme/recipeInputs.d.ts.map +1 -1
  47. package/types/theme/sizeRecipes.d.ts +2 -2
@@ -0,0 +1,88 @@
1
+ import { SizableText, XStack, YStack } from "tamagui";
2
+ import { Intent, type IntentProps } from "./Intent";
3
+ import { useIntentContext, useResolvedKnobs } from "./useResolvedKnobs";
4
+
5
+ export default {
6
+ title: "Theme/Intent",
7
+ component: Intent,
8
+ parameters: {
9
+ status: { type: "beta" },
10
+ docs: {
11
+ description: {
12
+ component:
13
+ "Intent activates the named Tamagui sub-theme (layer 1) and publishes the intent name through IntentContext so useResolvedKnobs merges the preset's intent overrides (layer 2). The probe is a Tamagui stack painting $background / $color and spreading the resolved surface.",
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ const intents: IntentProps["name"][] = ["accent", "error", "warning", "success"];
20
+
21
+ function Probe({ label }: { label: string }) {
22
+ const intent = useIntentContext();
23
+ const { knobProps } = useResolvedKnobs();
24
+ const attrs = {
25
+ "data-intent": intent ?? "none",
26
+ "data-outlined": String(Boolean(knobProps.outlined)),
27
+ "data-fill-style": knobProps.outlined ? "outlined" : "filled",
28
+ } as Record<string, unknown>;
29
+ return (
30
+ <YStack
31
+ {...knobProps.surface}
32
+ {...knobProps.borderRadius}
33
+ backgroundColor={knobProps.outlined ? "transparent" : "$background"}
34
+ borderColor="$borderColor"
35
+ padding="$3"
36
+ minWidth={140}
37
+ testID={`intent-probe-${label}`}
38
+ {...attrs}
39
+ >
40
+ <SizableText color="$color" testID={`intent-text-${label}`}>
41
+ {label}
42
+ </SizableText>
43
+ <SizableText color="$color11" fontSize="$2">
44
+ context: {intent ?? "none"}
45
+ </SizableText>
46
+ </YStack>
47
+ );
48
+ }
49
+
50
+ /** The four intents beside an un-wrapped baseline. */
51
+ export const main = {
52
+ name: "Main",
53
+ render: () => (
54
+ <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
55
+ <Probe label="none" />
56
+ {intents.map((name) => (
57
+ <Intent key={name} name={name}>
58
+ <Probe label={name} />
59
+ </Intent>
60
+ ))}
61
+ </XStack>
62
+ ),
63
+ };
64
+
65
+ /** Nesting replaces, never merges: the innermost intent wins. */
66
+ export const nested = {
67
+ name: "Nested",
68
+ render: () => (
69
+ <Intent name="error">
70
+ <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
71
+ <Probe label="outer-error" />
72
+ <Intent name="success">
73
+ <Probe label="inner-success" />
74
+ </Intent>
75
+ </XStack>
76
+ </Intent>
77
+ ),
78
+ };
79
+
80
+ /** An explicit `theme` keeps the intent context but paints another Tamagui theme. */
81
+ export const explicitTheme = {
82
+ name: "Explicit theme",
83
+ render: () => (
84
+ <Intent name="accent" theme="blue">
85
+ <Probe label="accent-as-blue" />
86
+ </Intent>
87
+ ),
88
+ };
@@ -0,0 +1,80 @@
1
+ import { SizableText, XStack, YStack } from "tamagui";
2
+ import { Preset } from "./Preset";
3
+ import { useResolvedKnobs } from "./useResolvedKnobs";
4
+
5
+ export default {
6
+ title: "Theme/Preset",
7
+ component: Preset,
8
+ parameters: {
9
+ status: { type: "beta" },
10
+ docs: {
11
+ description: {
12
+ component:
13
+ "Preset layers knob overrides on the nearest preset (cascade, the default) or replaces it (cascade={false}). The probe is a Tamagui stack spreading the resolved surface and radius, so what is measured is the stack.",
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ function Probe({ label }: { label: string }) {
20
+ const { knobProps } = useResolvedKnobs();
21
+ const attrs = {
22
+ "data-border-radius": String(knobProps.borderRadius.borderRadius),
23
+ "data-border-width": String(knobProps.surface.borderWidth),
24
+ "data-fill-style": knobProps.outlined ? "outlined" : "filled",
25
+ "data-space": String(knobProps.space),
26
+ } as Record<string, unknown>;
27
+ return (
28
+ <YStack
29
+ {...knobProps.surface}
30
+ {...knobProps.borderRadius}
31
+ backgroundColor={knobProps.outlined ? "transparent" : "$background"}
32
+ borderColor="$borderColor"
33
+ elevation={knobProps.elevation}
34
+ padding="$3"
35
+ minWidth={160}
36
+ testID={`preset-probe-${label}`}
37
+ {...attrs}
38
+ >
39
+ <SizableText>{label}</SizableText>
40
+ <SizableText color="$color11" fontSize="$2">
41
+ radius {String(knobProps.borderRadius.borderRadius)}, border{" "}
42
+ {String(knobProps.surface.borderWidth)}, {knobProps.outlined ? "outlined" : "filled"}
43
+ </SizableText>
44
+ </YStack>
45
+ );
46
+ }
47
+
48
+ /** Overrides on top of whatever the page's preset resolved. */
49
+ export const main = {
50
+ name: "Main",
51
+ render: () => (
52
+ <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
53
+ <Probe label="inherited" />
54
+ <Preset overrides={{ borderRadius: "full", borderWidth: "large" }}>
55
+ <Probe label="full-radius" />
56
+ </Preset>
57
+ <Preset overrides={{ fillStyle: "outlined" }}>
58
+ <Probe label="outlined" />
59
+ </Preset>
60
+ </XStack>
61
+ ),
62
+ };
63
+
64
+ /** Cascade carries the parent's overrides forward; cascade={false} starts from defaults. */
65
+ export const cascade = {
66
+ name: "Cascade",
67
+ render: () => (
68
+ <Preset overrides={{ borderRadius: "full" }}>
69
+ <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
70
+ <Probe label="parent-full" />
71
+ <Preset overrides={{ borderWidth: "large" }}>
72
+ <Probe label="child-cascade" />
73
+ </Preset>
74
+ <Preset cascade={false}>
75
+ <Probe label="child-reset" />
76
+ </Preset>
77
+ </XStack>
78
+ </Preset>
79
+ ),
80
+ };
@@ -14,6 +14,12 @@ import {
14
14
  } from "./Surface";
15
15
  import { useResolvedKnobs } from "./useResolvedKnobs";
16
16
 
17
+ // These structural context tests hold the color scheme constant.
18
+ vi.mock(import("tamagui"), async (importOriginal) => ({
19
+ ...(await importOriginal()),
20
+ useThemeName: () => "light",
21
+ }));
22
+
17
23
  let originalMatchMedia: typeof window.matchMedia;
18
24
 
19
25
  beforeEach(() => {
@@ -0,0 +1,91 @@
1
+ import { SizableText, XStack, YStack } from "tamagui";
2
+ import { Surface } from "./Surface";
3
+ import { useResolvedKnobs } from "./useResolvedKnobs";
4
+
5
+ export default {
6
+ title: "Theme/Surface",
7
+ component: Surface,
8
+ parameters: {
9
+ status: { type: "beta" },
10
+ docs: {
11
+ description: {
12
+ component:
13
+ "Surface paints nothing. It declares size/density intent for a subtree and nested Surfaces clamp step-down only (LC-68). The probe below is a Tamagui stack that spreads what useResolvedKnobs resolved inside the wrapper, so the measured part is the stack, never the wrapper.",
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ /** Spreads the resolved recipe on a stack so the wrapper's effect is measurable. */
20
+ function Probe({ label }: { label: string }) {
21
+ const { knobProps } = useResolvedKnobs();
22
+ const attrs = {
23
+ "data-size": String(knobProps.size),
24
+ "data-space": String(knobProps.space),
25
+ "data-size-token": String(knobProps.sizeToken),
26
+ "data-gap": String(knobProps.gap.gap),
27
+ } as Record<string, unknown>;
28
+ return (
29
+ <YStack
30
+ {...knobProps.surface}
31
+ {...knobProps.borderRadius}
32
+ {...knobProps.gap}
33
+ padding={knobProps.gap.gap}
34
+ testID={`surface-probe-${label}`}
35
+ {...attrs}
36
+ >
37
+ <SizableText fontSize={knobProps.sizeToken as never} testID={`surface-text-${label}`}>
38
+ {label}: token {String(knobProps.sizeToken)}, gap {String(knobProps.gap.gap)}
39
+ </SizableText>
40
+ </YStack>
41
+ );
42
+ }
43
+
44
+ export const main = {
45
+ name: "Main",
46
+ render: () => (
47
+ <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
48
+ <Probe label="outside" />
49
+ <Surface size="small">
50
+ <Probe label="small" />
51
+ </Surface>
52
+ <Surface size="large">
53
+ <Probe label="large" />
54
+ </Surface>
55
+ </XStack>
56
+ ),
57
+ };
58
+
59
+ /** LC-68: a large request inside a small Surface resolves small; small inside large stays small. */
60
+ export const nestedClamp = {
61
+ name: "Nested clamp",
62
+ render: () => (
63
+ <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
64
+ <Surface size="small">
65
+ <Surface size="large">
66
+ <Probe label="large-in-small" />
67
+ </Surface>
68
+ </Surface>
69
+ <Surface size="large">
70
+ <Surface size="small">
71
+ <Probe label="small-in-large" />
72
+ </Surface>
73
+ </Surface>
74
+ </XStack>
75
+ ),
76
+ };
77
+
78
+ /** Density steps the gap, not the control height. */
79
+ export const density = {
80
+ name: "Density",
81
+ render: () => (
82
+ <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
83
+ <Surface density="comfortable">
84
+ <Probe label="comfortable" />
85
+ </Surface>
86
+ <Surface density="compact">
87
+ <Probe label="compact" />
88
+ </Surface>
89
+ </XStack>
90
+ ),
91
+ };
@@ -0,0 +1,50 @@
1
+ import { useEffect, useState } from "react";
2
+ import { SizableText, YStack } from "tamagui";
3
+
4
+ export default {
5
+ title: "Theme/ThemeProvider",
6
+ parameters: {
7
+ status: { type: "beta" },
8
+ docs: {
9
+ description: {
10
+ component:
11
+ "ThemeProvider is the Storybook preview wrapper (TamaguiProvider + KnobBridge). This story measures that live provider: the html theme class and the FontKnobStyles stylesheet KnobBridge mounts. Remounting ThemeProvider inside the preview would nest TamaguiProvider, so the probe reads the one already on the page.",
12
+ },
13
+ },
14
+ },
15
+ };
16
+
17
+ function Probe() {
18
+ const [themeClass, setThemeClass] = useState("pending");
19
+ const [fontStyles, setFontStyles] = useState("pending");
20
+ useEffect(() => {
21
+ const html = document.documentElement;
22
+ setThemeClass(
23
+ html.classList.contains("t_dark")
24
+ ? "t_dark"
25
+ : html.classList.contains("t_light")
26
+ ? "t_light"
27
+ : "none",
28
+ );
29
+ setFontStyles(document.getElementById("mp-font-knob-styles") ? "present" : "missing");
30
+ }, []);
31
+ return (
32
+ <YStack
33
+ testID="theme-provider-probe"
34
+ gap="$2"
35
+ padding="$4"
36
+ {...({
37
+ "data-theme-class": themeClass,
38
+ "data-font-styles": fontStyles,
39
+ } as Record<string, unknown>)}
40
+ >
41
+ <SizableText>html class: {themeClass}</SizableText>
42
+ <SizableText>FontKnobStyles: {fontStyles}</SizableText>
43
+ </YStack>
44
+ );
45
+ }
46
+
47
+ export const main = {
48
+ name: "Main",
49
+ render: () => <Probe />,
50
+ };
@@ -0,0 +1,98 @@
1
+ import { type ReactNode, useMemo } from "react";
2
+ import { SizableText, XStack, YStack } from "tamagui";
3
+ import { PresetContext, usePresetContext } from "./PresetContext";
4
+ import { defaultPreset } from "./presets";
5
+ import { Tint, useTintDepth } from "./Tint";
6
+
7
+ export default {
8
+ title: "Theme/Tint",
9
+ component: Tint,
10
+ parameters: {
11
+ status: { type: "beta" },
12
+ docs: {
13
+ description: {
14
+ component:
15
+ "Tint selects tints[(depth - 1) % tints.length] from the preset and wraps children in that Tamagui sub-theme. The swatch is a Tamagui stack painting $color5 / $color11, so the hue you see is the sub-theme, not the wrapper.",
16
+ },
17
+ },
18
+ },
19
+ };
20
+
21
+ /** Keeps the surrounding knobs, pins the tint family to defaultPreset's four names. */
22
+ function WithDefaultTints({ children }: { children: ReactNode }) {
23
+ const parent = usePresetContext();
24
+ const value = useMemo(
25
+ () => ({
26
+ preset: { ...(parent?.preset ?? defaultPreset), tints: defaultPreset.tints },
27
+ overrides: parent?.overrides,
28
+ }),
29
+ [parent],
30
+ );
31
+ return <PresetContext.Provider value={value}>{children}</PresetContext.Provider>;
32
+ }
33
+
34
+ function Swatch({ label }: { label?: string }) {
35
+ const depth = useTintDepth();
36
+ return (
37
+ <YStack
38
+ backgroundColor="$color5"
39
+ borderColor="$color8"
40
+ borderWidth={1}
41
+ borderRadius="$3"
42
+ padding="$3"
43
+ gap="$2"
44
+ testID={`tint-swatch-${label ?? depth}`}
45
+ >
46
+ <SizableText color="$color11">
47
+ {label ?? "depth"} {depth}
48
+ </SizableText>
49
+ </YStack>
50
+ );
51
+ }
52
+
53
+ /** Five nested Tints: orange, blue, purple, pink, then orange again (modulo). */
54
+ export const main = {
55
+ name: "Main",
56
+ render: () => (
57
+ <WithDefaultTints>
58
+ <Tint>
59
+ <Swatch />
60
+ <Tint>
61
+ <Swatch />
62
+ <Tint>
63
+ <Swatch />
64
+ <Tint>
65
+ <Swatch />
66
+ <Tint>
67
+ <Swatch />
68
+ </Tint>
69
+ </Tint>
70
+ </Tint>
71
+ </Tint>
72
+ </Tint>
73
+ </WithDefaultTints>
74
+ ),
75
+ };
76
+
77
+ /** `alt` offsets the depth; `disable` keeps the depth but drops the Theme wrapper. */
78
+ export const altAndDisable = {
79
+ name: "Alt and disable",
80
+ render: () => (
81
+ <WithDefaultTints>
82
+ <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
83
+ <Tint>
84
+ <Swatch label="plain" />
85
+ </Tint>
86
+ <Tint alt={1}>
87
+ <Swatch label="alt-1" />
88
+ </Tint>
89
+ <Tint alt={2}>
90
+ <Swatch label="alt-2" />
91
+ </Tint>
92
+ <Tint disable>
93
+ <Swatch label="disabled" />
94
+ </Tint>
95
+ </XStack>
96
+ </WithDefaultTints>
97
+ ),
98
+ };
@@ -1,7 +1,9 @@
1
1
  import * as Colors from "@tamagui/colors";
2
+ import { themes as stockThemes } from "@tamagui/themes";
2
3
  import { describe, expect, it } from "vitest";
3
4
  import { resolveChartPalette } from "./chartPalette";
4
5
  import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
6
+ import { tintHueNames } from "./createThemes";
5
7
 
6
8
  // The default accent solid (accentBackground) from defaults/accent.ts.
7
9
  const lightAccent = "hsla(250, 50%, 54%, 1)";
@@ -53,6 +55,126 @@ function adjacentDistinguishable(a: string, b: string): boolean {
53
55
  }
54
56
 
55
57
  describe("resolveChartPalette", () => {
58
+ it("floors every categorical mark across the registered tint identities", () => {
59
+ for (const scheme of ["light", "dark"] as const) {
60
+ const cardSurfaces = Object.entries(stockThemes)
61
+ .filter(
62
+ ([name]) =>
63
+ name.startsWith(`${scheme}_`) &&
64
+ name.endsWith("_Card") &&
65
+ tintHueNames.has(name.split("_")[1]) &&
66
+ name.split("_").length === 3,
67
+ )
68
+ .map(([, theme]) => theme.background);
69
+ expect(cardSurfaces.length).toBeGreaterThan(0);
70
+ const surfaces = [
71
+ ...cardSurfaces,
72
+ ...(scheme === "light"
73
+ ? ["#ffffff", Colors.gray.gray2, Colors.mauve.mauve2]
74
+ : [Colors.grayDark.gray2, Colors.mauveDark.mauve2]),
75
+ ];
76
+ for (const hue of tintHueNames) {
77
+ const ramp = (Colors as unknown as Record<string, Record<string, string>>)[
78
+ scheme === "dark" ? `${hue}Dark` : hue
79
+ ];
80
+ const palette = resolveChartPalette({
81
+ scheme,
82
+ identitySolid: ramp[`${hue}9`],
83
+ identityCandidates: [ramp[`${hue}10`], ramp[`${hue}11`]],
84
+ });
85
+ for (let i = 0; i < palette.categorical.length; i++) {
86
+ expect(
87
+ adjacentDistinguishable(
88
+ palette.categorical[i],
89
+ palette.categorical[(i + 1) % palette.categorical.length],
90
+ ),
91
+ `${scheme}/${hue}: adjacent series ${i}`,
92
+ ).toBe(true);
93
+ }
94
+ for (const paint of [...palette.categorical, ...Object.values(palette.semantic)]) {
95
+ for (const surface of surfaces) {
96
+ const ratio = contrastRatio(
97
+ relativeLuminance(normalizeToHex(paint)!),
98
+ relativeLuminance(normalizeToHex(surface)!),
99
+ );
100
+ expect(ratio, `${scheme}/${hue}: ${paint} on ${surface}`).toBeGreaterThanOrEqual(3);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ });
106
+
107
+ it("retains the dark cycle order while deepening its failing violet slot", () => {
108
+ const palette = resolveChartPalette({ scheme: "dark", identitySolid: "#aaaaaa" });
109
+ expect(palette.categorical.slice(1)).toEqual([
110
+ Colors.blueDark.blue9,
111
+ Colors.orangeDark.orange9,
112
+ Colors.greenDark.green9,
113
+ Colors.amberDark.amber9,
114
+ Colors.pinkDark.pink9,
115
+ Colors.tealDark.teal9,
116
+ Colors.violetDark.violet10,
117
+ Colors.redDark.red9,
118
+ ]);
119
+ });
120
+
121
+ it("keeps the valid default light order and every grass-tint series", () => {
122
+ expect(
123
+ resolveChartPalette({ scheme: "light", identitySolid: lightAccent }).categorical,
124
+ ).toEqual([
125
+ lightAccent,
126
+ Colors.blue.blue10,
127
+ Colors.orange.orange11,
128
+ Colors.green.green10,
129
+ Colors.amber.amber11,
130
+ Colors.pink.pink9,
131
+ Colors.teal.teal10,
132
+ Colors.red.red9,
133
+ ]);
134
+ const grass = resolveChartPalette({
135
+ scheme: "light",
136
+ identitySolid: Colors.grass.grass9,
137
+ identityCandidates: [Colors.grass.grass10, Colors.grass.grass11],
138
+ });
139
+ expect(grass.categorical[0]).toBe(Colors.grass.grass10);
140
+ expect([...grass.categorical].sort()).toEqual(
141
+ [
142
+ Colors.grass.grass10,
143
+ Colors.blue.blue10,
144
+ Colors.orange.orange11,
145
+ Colors.amber.amber11,
146
+ Colors.pink.pink9,
147
+ Colors.teal.teal10,
148
+ Colors.violet.violet9,
149
+ Colors.red.red9,
150
+ ].sort(),
151
+ );
152
+ });
153
+
154
+ it("uses the first readable identity candidate and retains a readable original", () => {
155
+ expect(
156
+ resolveChartPalette({
157
+ scheme: "light",
158
+ identitySolid: "#ffffff",
159
+ identityCandidates: ["var(--ink)", "#777777", "#333333"],
160
+ }).single,
161
+ ).toBe("#777777");
162
+ expect(
163
+ resolveChartPalette({
164
+ scheme: "light",
165
+ identitySolid: lightAccent,
166
+ identityCandidates: ["#333333"],
167
+ }).single,
168
+ ).toBe(lightAccent);
169
+ expect(
170
+ resolveChartPalette({
171
+ scheme: "light",
172
+ identitySolid: "#ffffff",
173
+ identityCandidates: ["#eeeeee"],
174
+ }).single,
175
+ ).toBe(Colors.violet.violet9);
176
+ });
177
+
56
178
  it("single-series takes the theme identity solid", () => {
57
179
  expect(resolveChartPalette({ scheme: "light", identitySolid: lightAccent }).single).toBe(
58
180
  lightAccent,
@@ -70,7 +192,7 @@ describe("resolveChartPalette", () => {
70
192
  it("drops the cycle hue confusable with the accent identity (violet)", () => {
71
193
  const palette = resolveChartPalette({ scheme: "light", identitySolid: lightAccent });
72
194
  expect(palette.categorical).not.toContain(Colors.violet.violet9);
73
- expect(palette.categorical).toContain(Colors.blue.blue9);
195
+ expect(palette.categorical).toContain(Colors.blue.blue10);
74
196
  expect(palette.categorical).toHaveLength(8);
75
197
  });
76
198
 
@@ -88,11 +210,12 @@ describe("resolveChartPalette", () => {
88
210
  expect(palette.categorical).toContain(Colors.red.red9);
89
211
  });
90
212
 
91
- it("drops every cycle hue within the confusable band (tomato drops red and orange)", () => {
213
+ it("drops every cycle hue within the confusable band (tomato drops red, orange and amber)", () => {
92
214
  const palette = resolveChartPalette({ scheme: "light", identitySolid: tomatoTintSolid });
93
215
  expect(palette.categorical).not.toContain(Colors.red.red9);
94
- expect(palette.categorical).not.toContain(Colors.orange.orange9);
95
- expect(palette.categorical).toHaveLength(7);
216
+ expect(palette.categorical).not.toContain(Colors.orange.orange11);
217
+ expect(palette.categorical).not.toContain(Colors.amber.amber11);
218
+ expect(palette.categorical).toHaveLength(6);
96
219
  });
97
220
 
98
221
  it("keeps the full cycle behind a neutral identity (gray tint)", () => {
@@ -117,7 +240,7 @@ describe("resolveChartPalette", () => {
117
240
  const light = resolveChartPalette({ scheme: "light", identitySolid: lightAccent });
118
241
  const dark = resolveChartPalette({ scheme: "dark", identitySolid: darkAccent });
119
242
  expect(light.semantic.error).toBe(Colors.red.red9);
120
- expect(light.semantic.success).toBe(Colors.green.green9);
243
+ expect(light.semantic.success).toBe(Colors.green.green10);
121
244
  expect(light.semantic.warning).toBe(Colors.yellow.yellow11);
122
245
  expect(dark.semantic.error).toBe(Colors.redDark.red9);
123
246
  expect(dark.semantic.success).toBe(Colors.greenDark.green9);