@multiplatform.one/theme 7.34.3 → 7.35.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/package.json +20 -5
- package/src/audit/constraintAudit.spec.ts +274 -87
- package/src/audit/constraintAudit.ts +135 -120
- package/src/audit/constraintScope.spec.ts +1 -1
- package/src/audit/designTokens.ts +42 -0
- package/src/audit/themeMatrix.spec.ts +83 -2
- package/src/audit/themeMatrix.ts +33 -1
- package/src/figma/figmaTokens.spec.ts +26 -34
- package/src/figma/figmaTokens.ts +26 -50
- package/src/fonts/createFontLoader.ts +4 -1
- package/src/menuRow.ts +4 -1
- package/src/theme/FontKnobStyles.tsx +2 -1
- package/src/theme/Preset.stories.tsx +0 -1
- package/src/theme/Preset.tsx +6 -1
- package/src/theme/PresetContext.ts +4 -0
- package/src/theme/Surface.spec.tsx +114 -67
- package/src/theme/Surface.stories.tsx +8 -25
- package/src/theme/Surface.tsx +87 -43
- package/src/theme/accentFloor.spec.ts +37 -11
- package/src/theme/activateThemeConfig.ts +22 -6
- package/src/theme/animations/animations.spec.ts +62 -0
- package/src/theme/animations/css.ts +9 -35
- package/src/theme/animations/reactNative.ts +98 -25
- package/src/theme/boundaryFloor.spec.ts +57 -0
- package/src/theme/chartPalette.spec.ts +56 -27
- package/src/theme/chartPalette.ts +33 -28
- package/src/theme/colorRules.spec.ts +32 -72
- package/src/theme/cookies.spec.ts +49 -18
- package/src/theme/cookies.ts +36 -7
- package/src/theme/cookiesPersistence.spec.ts +5 -5
- package/src/theme/createDefaultThemeConfig.spec.ts +182 -20
- package/src/theme/createDefaultThemeConfig.ts +34 -28
- package/src/theme/createThemes.ts +169 -218
- package/src/theme/defaults/accent.ts +3 -35
- package/src/theme/defaults/base.ts +14 -53
- package/src/theme/defaults/builderOptions.ts +68 -85
- package/src/theme/defaults/categoryFonts.ts +10 -0
- package/src/theme/defaults/fonts.spec.ts +84 -4
- package/src/theme/defaults/fonts.ts +166 -124
- package/src/theme/defaults/house.ts +48 -0
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/defaults/neutralRamp.spec.ts +52 -47
- package/src/theme/devtools/ColorLineVisualizer.tsx +6 -6
- package/src/theme/devtools/ThemeDevtoolsPanel.setMarkers.spec.tsx +13 -6
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +14 -77
- package/src/theme/devtools/animationOptions.spec.ts +23 -37
- package/src/theme/devtools/animationOptions.ts +4 -6
- package/src/theme/devtools/buildSnapshot.ts +0 -3
- package/src/theme/devtools/knobIcons.tsx +4 -24
- package/src/theme/focusState.spec.ts +9 -30
- package/src/theme/index.ts +17 -0
- package/src/theme/intentFill.spec.ts +42 -0
- package/src/theme/knobs.ts +14 -42
- package/src/theme/layoutTokens.ts +15 -1
- package/src/theme/layoutTokensHooks.spec.tsx +8 -4
- package/src/theme/mergedTokens.ts +20 -0
- package/src/theme/motion.spec.ts +84 -0
- package/src/theme/motion.ts +53 -0
- package/src/theme/motionStops.ts +17 -0
- package/src/theme/motionTiming.ts +59 -0
- package/src/theme/mpo13Proof.spec.ts +43 -46
- package/src/theme/mpo19Proof.spec.ts +13 -14
- package/src/theme/namedInkFloor.spec.ts +60 -0
- package/src/theme/overlaySurfaces.tsx +330 -0
- package/src/theme/preset.types.ts +16 -0
- package/src/theme/presetPreview.ts +32 -0
- package/src/theme/presetRoot.spec.tsx +89 -0
- package/src/theme/presetStore.persistence.spec.ts +141 -0
- package/src/theme/presetStore.spec.ts +205 -0
- package/src/theme/presetStore.ts +286 -0
- package/src/theme/presetUserSync.spec.tsx +168 -0
- package/src/theme/presetUserSync.ts +47 -0
- package/src/theme/presets.spec.ts +22 -15
- package/src/theme/presets.ts +22 -10
- package/src/theme/radiusClass.spec.ts +60 -41
- package/src/theme/radiusClass.ts +17 -4
- package/src/theme/radiusClassTable.generated.ts +3 -3
- package/src/theme/readableColor.spec.ts +59 -152
- package/src/theme/readableColor.ts +34 -174
- package/src/theme/recipeInputs.ts +153 -295
- package/src/theme/recipes.ts +58 -18
- package/src/theme/resolveKnobs.spec.ts +528 -588
- package/src/theme/resolveKnobs.ts +250 -325
- package/src/theme/scale/ScaleStyles.native.tsx +19 -0
- package/src/theme/scale/ScaleStyles.tsx +48 -0
- package/src/theme/scale/activeScale.ts +30 -0
- package/src/theme/scale/deriveRecipeInputs.ts +110 -0
- package/src/theme/scale/generateScale.spec.ts +260 -0
- package/src/theme/scale/generateScale.ts +147 -0
- package/src/theme/scale/index.ts +54 -0
- package/src/theme/scale/installScaleConfig.native.spec.ts +77 -0
- package/src/theme/scale/installScaleConfig.native.ts +57 -0
- package/src/theme/scale/installScaleConfig.ts +7 -0
- package/src/theme/scale/scaleAttribute.ts +21 -0
- package/src/theme/scale/scaleCss.probe.spec.tsx +64 -0
- package/src/theme/scale/scaleCss.spec.ts +132 -0
- package/src/theme/scale/scaleCss.ts +95 -0
- package/src/theme/scale/scaleFonts.ts +106 -0
- package/src/theme/scale/scaleInputs.ts +322 -0
- package/src/theme/scale/themeScales.ts +181 -0
- package/src/theme/scale/useActiveScale.spec.tsx +129 -0
- package/src/theme/scale/useActiveScale.ts +95 -0
- package/src/theme/schemeStore.spec.ts +81 -0
- package/src/theme/schemeStore.ts +67 -0
- package/src/theme/shared.spec.ts +7 -6
- package/src/theme/shared.tsx +37 -3
- package/src/theme/sizeLadder.spec.ts +37 -36
- package/src/theme/sizeRecipes.spec.ts +179 -85
- package/src/theme/sizeRecipes.ts +48 -25
- package/src/theme/subsetThemes.spec.ts +9 -3
- package/src/theme/switchThumb.spec.ts +69 -0
- package/src/theme/syntaxFloor.spec.ts +11 -8
- package/src/theme/theme.hydration.native.spec.tsx +51 -0
- package/src/theme/theme.native.tsx +59 -21
- package/src/theme/theme.palette.spec.tsx +76 -0
- package/src/theme/theme.preset.native.spec.tsx +73 -0
- package/src/theme/theme.provider.spec.ts +143 -1
- package/src/theme/theme.tsx +61 -25
- package/src/theme/tileSizes.ts +6 -7
- package/src/theme/transitionProps.native.ts +25 -2
- package/src/theme/transitionProps.spec.ts +34 -0
- package/src/theme/transitionProps.ts +26 -2
- package/src/theme/usePresetSwitch.native.spec.tsx +68 -0
- package/src/theme/usePresetSwitch.native.ts +32 -0
- package/src/theme/usePresetSwitch.spec.tsx +103 -0
- package/src/theme/usePresetSwitch.ts +54 -0
- package/src/theme/useResolvedKnobs.hydration.spec.tsx +2 -2
- package/src/theme/useResolvedKnobs.ts +23 -90
- package/src/theme/useResolvedKnobsBehavior.spec.tsx +25 -30
- package/src/theme/useSchemePreference.native.ts +33 -0
- package/src/theme/useSchemePreference.spec.tsx +51 -0
- package/src/theme/useSchemePreference.ts +32 -0
- package/src/theme/useSchemePreference.web.spec.tsx +32 -0
- package/src/theme/useTheme.native.spec.tsx +68 -0
- package/src/theme/useTheme.native.ts +49 -24
- package/src/theme/useTheme.ts +7 -4
- package/types/audit/constraintAudit.d.ts +6 -5
- package/types/audit/constraintAudit.d.ts.map +1 -1
- package/types/audit/designTokens.d.ts +13 -0
- package/types/audit/designTokens.d.ts.map +1 -0
- package/types/audit/themeMatrix.d.ts +10 -0
- package/types/audit/themeMatrix.d.ts.map +1 -1
- package/types/figma/figmaTokens.d.ts +11 -28
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/fonts/createFontLoader.d.ts.map +1 -1
- package/types/menuRow.d.ts +4 -1
- package/types/menuRow.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Preset.d.ts.map +1 -1
- package/types/theme/PresetContext.d.ts +4 -0
- package/types/theme/PresetContext.d.ts.map +1 -1
- package/types/theme/Surface.d.ts +57 -26
- package/types/theme/Surface.d.ts.map +1 -1
- package/types/theme/activateThemeConfig.d.ts +5 -3
- package/types/theme/activateThemeConfig.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +27 -34
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/animations/reactNative.d.ts +196 -45
- package/types/theme/animations/reactNative.d.ts.map +1 -1
- package/types/theme/chartPalette.d.ts +11 -3
- package/types/theme/chartPalette.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +1 -1
- package/types/theme/cookies.d.ts +12 -1
- package/types/theme/cookies.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +13 -13
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/createThemes.d.ts +58 -2
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/defaults/accent.d.ts.map +1 -1
- package/types/theme/defaults/base.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +10 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -1
- package/types/theme/defaults/fonts.d.ts +330 -30
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- package/types/theme/defaults/house.d.ts +8 -0
- package/types/theme/defaults/house.d.ts.map +1 -0
- package/types/theme/defaults/index.d.ts +1 -0
- package/types/theme/defaults/index.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts +3 -3
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/devtools/animationOptions.d.ts.map +1 -1
- package/types/theme/devtools/buildSnapshot.d.ts.map +1 -1
- package/types/theme/devtools/knobIcons.d.ts +0 -3
- package/types/theme/devtools/knobIcons.d.ts.map +1 -1
- package/types/theme/index.d.ts +11 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +8 -36
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +13 -1
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/mergedTokens.d.ts +11 -0
- package/types/theme/mergedTokens.d.ts.map +1 -0
- package/types/theme/motion.d.ts +17 -0
- package/types/theme/motion.d.ts.map +1 -0
- package/types/theme/motionStops.d.ts +3 -0
- package/types/theme/motionStops.d.ts.map +1 -0
- package/types/theme/motionTiming.d.ts +36 -0
- package/types/theme/motionTiming.d.ts.map +1 -0
- package/types/theme/overlaySurfaces.d.ts +70 -0
- package/types/theme/overlaySurfaces.d.ts.map +1 -0
- package/types/theme/preset.types.d.ts +16 -0
- package/types/theme/preset.types.d.ts.map +1 -1
- package/types/theme/presetPreview.d.ts +14 -0
- package/types/theme/presetPreview.d.ts.map +1 -0
- package/types/theme/presetStore.d.ts +40 -0
- package/types/theme/presetStore.d.ts.map +1 -0
- package/types/theme/presetUserSync.d.ts +7 -0
- package/types/theme/presetUserSync.d.ts.map +1 -0
- package/types/theme/presets.d.ts +12 -4
- package/types/theme/presets.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +5 -2
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +3 -3
- package/types/theme/readableColor.d.ts +11 -49
- package/types/theme/readableColor.d.ts.map +1 -1
- package/types/theme/recipeInputs.d.ts +39 -70
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +79 -18
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +64 -32
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/scale/ScaleStyles.d.ts +18 -0
- package/types/theme/scale/ScaleStyles.d.ts.map +1 -0
- package/types/theme/scale/ScaleStyles.native.d.ts +10 -0
- package/types/theme/scale/ScaleStyles.native.d.ts.map +1 -0
- package/types/theme/scale/activeScale.d.ts +13 -0
- package/types/theme/scale/activeScale.d.ts.map +1 -0
- package/types/theme/scale/deriveRecipeInputs.d.ts +10 -0
- package/types/theme/scale/deriveRecipeInputs.d.ts.map +1 -0
- package/types/theme/scale/generateScale.d.ts +38 -0
- package/types/theme/scale/generateScale.d.ts.map +1 -0
- package/types/theme/scale/index.d.ts +17 -0
- package/types/theme/scale/index.d.ts.map +1 -0
- package/types/theme/scale/installScaleConfig.d.ts +4 -0
- package/types/theme/scale/installScaleConfig.d.ts.map +1 -0
- package/types/theme/scale/installScaleConfig.native.d.ts +13 -0
- package/types/theme/scale/installScaleConfig.native.d.ts.map +1 -0
- package/types/theme/scale/scaleAttribute.d.ts +14 -0
- package/types/theme/scale/scaleAttribute.d.ts.map +1 -0
- package/types/theme/scale/scaleCss.d.ts +25 -0
- package/types/theme/scale/scaleCss.d.ts.map +1 -0
- package/types/theme/scale/scaleFonts.d.ts +16 -0
- package/types/theme/scale/scaleFonts.d.ts.map +1 -0
- package/types/theme/scale/scaleInputs.d.ts +66 -0
- package/types/theme/scale/scaleInputs.d.ts.map +1 -0
- package/types/theme/scale/themeScales.d.ts +52 -0
- package/types/theme/scale/themeScales.d.ts.map +1 -0
- package/types/theme/scale/useActiveScale.d.ts +31 -0
- package/types/theme/scale/useActiveScale.d.ts.map +1 -0
- package/types/theme/schemeStore.d.ts +6 -0
- package/types/theme/schemeStore.d.ts.map +1 -0
- package/types/theme/shared.d.ts +18 -0
- package/types/theme/shared.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +20 -15
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts +2 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts +10 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/tileSizes.d.ts +6 -7
- package/types/theme/tileSizes.d.ts.map +1 -1
- package/types/theme/transitionProps.d.ts +3 -1
- package/types/theme/transitionProps.d.ts.map +1 -1
- package/types/theme/transitionProps.native.d.ts +3 -1
- package/types/theme/transitionProps.native.d.ts.map +1 -1
- package/types/theme/usePresetSwitch.d.ts +10 -0
- package/types/theme/usePresetSwitch.d.ts.map +1 -0
- package/types/theme/usePresetSwitch.native.d.ts +10 -0
- package/types/theme/usePresetSwitch.native.d.ts.map +1 -0
- package/types/theme/useResolvedKnobs.d.ts +13 -13
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
- package/types/theme/useSchemePreference.d.ts +8 -0
- package/types/theme/useSchemePreference.d.ts.map +1 -0
- package/types/theme/useSchemePreference.native.d.ts +8 -0
- package/types/theme/useSchemePreference.native.d.ts.map +1 -0
- package/types/theme/useTheme.d.ts +4 -2
- package/types/theme/useTheme.d.ts.map +1 -1
- package/types/theme/useTheme.native.d.ts +10 -1
- package/types/theme/useTheme.native.d.ts.map +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { animationConfig, animationDurations, motionStops } from "./animations/css";
|
|
3
|
+
import {
|
|
4
|
+
motionFor,
|
|
5
|
+
motionDurationMs,
|
|
6
|
+
motionSeconds,
|
|
7
|
+
motionSettleMs,
|
|
8
|
+
motionHideDelayMs,
|
|
9
|
+
motionRoleProperties,
|
|
10
|
+
} from "./motion";
|
|
11
|
+
|
|
12
|
+
describe("motion roles", () => {
|
|
13
|
+
it("assigns quick's control, overlay, scrim, reveal and slide stops", () => {
|
|
14
|
+
expect(
|
|
15
|
+
Object.fromEntries(
|
|
16
|
+
Object.entries(motionFor("quick")).map(([role, value]) => [role, value.transition]),
|
|
17
|
+
),
|
|
18
|
+
).toEqual({
|
|
19
|
+
control: "quick",
|
|
20
|
+
overlay: "quicker",
|
|
21
|
+
scrim: "slow",
|
|
22
|
+
reveal: "medium",
|
|
23
|
+
slide: "medium",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
it("silences every role at none while retaining property scopes", () => {
|
|
27
|
+
for (const [name, role] of Object.entries(motionFor("none"))) {
|
|
28
|
+
expect(role.transition).toBeUndefined();
|
|
29
|
+
expect(role.animateOnly).toEqual(
|
|
30
|
+
motionRoleProperties[name as keyof typeof motionRoleProperties],
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
it("selects registered non-bouncy role stops by nearest duration, ties faster", () => {
|
|
35
|
+
const candidates = motionStops.filter((stop) => stop !== "bouncy" && stop !== "superBouncy");
|
|
36
|
+
for (const stop of motionStops) {
|
|
37
|
+
const roles = motionFor(stop);
|
|
38
|
+
expect(roles.control.transition).toBe(stop);
|
|
39
|
+
for (const [name, factor] of [
|
|
40
|
+
["overlay", 0.8],
|
|
41
|
+
["scrim", 3],
|
|
42
|
+
["reveal", 2],
|
|
43
|
+
["slide", 2],
|
|
44
|
+
] as const) {
|
|
45
|
+
const closest = [...candidates].sort(
|
|
46
|
+
(a, b) =>
|
|
47
|
+
Math.abs(animationDurations[a] - animationDurations[stop] * factor) -
|
|
48
|
+
Math.abs(animationDurations[b] - animationDurations[stop] * factor) ||
|
|
49
|
+
animationDurations[a] - animationDurations[b],
|
|
50
|
+
)[0];
|
|
51
|
+
expect(roles[name].transition).toBe(closest);
|
|
52
|
+
expect(roles[name].transition).toBeOneOf(Object.keys(animationConfig));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
expect(motionFor("quickestLessBouncy").reveal.transition).toBe("quicker");
|
|
56
|
+
});
|
|
57
|
+
it("limits properties to each role's anatomy", () => {
|
|
58
|
+
expect(motionRoleProperties).toEqual({
|
|
59
|
+
control: ["backgroundColor", "borderColor", "color", "opacity", "transform"],
|
|
60
|
+
overlay: ["opacity", "transform"],
|
|
61
|
+
scrim: ["opacity"],
|
|
62
|
+
reveal: ["height", "opacity", "transform"],
|
|
63
|
+
slide: ["opacity", "transform"],
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("motion durations", () => {
|
|
69
|
+
it("derives duration, seconds, settle and hide delays from every table entry", () => {
|
|
70
|
+
for (const token of Object.keys(animationConfig) as (keyof typeof animationConfig)[]) {
|
|
71
|
+
expect(motionDurationMs(token)).toBe(animationDurations[token]);
|
|
72
|
+
expect(motionSeconds(token)).toBe(animationDurations[token] / 1000);
|
|
73
|
+
expect(motionSettleMs(token)).toBe(Math.max(300, 3 * animationDurations[token]) + 80);
|
|
74
|
+
expect(motionHideDelayMs(token)).toBe(animationDurations[token] + 50);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
it("uses zero at none and 300ms for structured transitions", () => {
|
|
78
|
+
expect(motionDurationMs(undefined)).toBe(0);
|
|
79
|
+
expect(motionSeconds(undefined)).toBe(0);
|
|
80
|
+
expect(motionSettleMs(undefined)).toBe(0);
|
|
81
|
+
expect(motionDurationMs(["quick", { opacity: "slow" }])).toBe(300);
|
|
82
|
+
expect(motionSettleMs(["quick", { opacity: "slow" }])).toBe(980);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { TransitionProp } from "tamagui";
|
|
2
|
+
import type { Animation } from "./knobs";
|
|
3
|
+
import { motionStops, type MotionStop } from "./motionStops";
|
|
4
|
+
import { animationDurations, motionDurationMs } from "./motionTiming";
|
|
5
|
+
|
|
6
|
+
export { motionDurationMs, motionHideDelayMs, motionSeconds, motionSettleMs } from "./motionTiming";
|
|
7
|
+
|
|
8
|
+
export type MotionRoleName = "control" | "overlay" | "scrim" | "reveal" | "slide";
|
|
9
|
+
|
|
10
|
+
export interface MotionRole {
|
|
11
|
+
transition: TransitionProp | undefined;
|
|
12
|
+
animateOnly: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const motionRoleProperties = {
|
|
16
|
+
control: ["backgroundColor", "borderColor", "color", "opacity", "transform"],
|
|
17
|
+
overlay: ["opacity", "transform"],
|
|
18
|
+
scrim: ["opacity"],
|
|
19
|
+
reveal: ["height", "opacity", "transform"],
|
|
20
|
+
slide: ["opacity", "transform"],
|
|
21
|
+
} as const satisfies Record<MotionRoleName, readonly string[]>;
|
|
22
|
+
|
|
23
|
+
const nonBouncyStops = motionStops.filter((stop) => stop !== "superBouncy" && stop !== "bouncy");
|
|
24
|
+
|
|
25
|
+
function nearestStop(duration: number): MotionStop {
|
|
26
|
+
return nonBouncyStops.reduce((nearest, candidate) =>
|
|
27
|
+
Math.abs(animationDurations[candidate] - duration) <
|
|
28
|
+
Math.abs(animationDurations[nearest] - duration)
|
|
29
|
+
? candidate
|
|
30
|
+
: nearest,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function motionFor(stop: Animation): Record<MotionRoleName, MotionRole> {
|
|
35
|
+
const transition = stop === "none" ? undefined : stop;
|
|
36
|
+
const duration = motionDurationMs(transition);
|
|
37
|
+
const role = (name: MotionRoleName, factor: number): MotionRole => ({
|
|
38
|
+
transition:
|
|
39
|
+
transition === undefined
|
|
40
|
+
? undefined
|
|
41
|
+
: name === "control"
|
|
42
|
+
? transition
|
|
43
|
+
: nearestStop(duration * factor),
|
|
44
|
+
animateOnly: motionRoleProperties[name],
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
control: role("control", 1),
|
|
48
|
+
overlay: role("overlay", 0.8),
|
|
49
|
+
scrim: role("scrim", 3),
|
|
50
|
+
reveal: role("reveal", 2),
|
|
51
|
+
slide: role("slide", 2),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const motionStops = [
|
|
2
|
+
"quickestLessBouncy",
|
|
3
|
+
"quickest",
|
|
4
|
+
"quickerLessBouncy",
|
|
5
|
+
"quicker",
|
|
6
|
+
"quick",
|
|
7
|
+
"quickLessBouncy",
|
|
8
|
+
"medium",
|
|
9
|
+
"superBouncy",
|
|
10
|
+
"bouncy",
|
|
11
|
+
"slow",
|
|
12
|
+
"lazy",
|
|
13
|
+
"superLazy",
|
|
14
|
+
"slowest",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export type MotionStop = (typeof motionStops)[number];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { TransitionProp } from "tamagui";
|
|
2
|
+
|
|
3
|
+
export const animationConfig = {
|
|
4
|
+
"0ms": "0ms linear",
|
|
5
|
+
"50ms": "50ms linear",
|
|
6
|
+
"75ms": "75ms linear",
|
|
7
|
+
"100ms": "100ms ease-out",
|
|
8
|
+
"200ms": "200ms ease-out",
|
|
9
|
+
"250ms": "250ms ease-out",
|
|
10
|
+
"300ms": "300ms ease-out",
|
|
11
|
+
"400ms": "400ms ease-out",
|
|
12
|
+
"500ms": "500ms ease-out",
|
|
13
|
+
superBouncy: "300ms cubic-bezier(0.175, 0.885, 0.32, 1.5)",
|
|
14
|
+
bouncy: "350ms cubic-bezier(0.175, 0.885, 0.32, 1.275)",
|
|
15
|
+
superLazy: "600ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
16
|
+
lazy: "500ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
17
|
+
medium: "300ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
18
|
+
slowest: "800ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
19
|
+
slow: "450ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
20
|
+
quick: "150ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
21
|
+
quickLessBouncy: "180ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
22
|
+
quicker: "120ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
23
|
+
quickerLessBouncy: "100ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
24
|
+
quickest: "80ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
25
|
+
quickestLessBouncy: "60ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
26
|
+
tooltip: "450ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
27
|
+
snappy: "80ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
28
|
+
gentle: "450ms cubic-bezier(0.25, 0.1, 0.25, 1)",
|
|
29
|
+
} as const;
|
|
30
|
+
export type AnimationName = keyof typeof animationConfig;
|
|
31
|
+
export const animationNames = Object.keys(animationConfig) as AnimationName[];
|
|
32
|
+
|
|
33
|
+
function parseDurationMs(timing: string): number {
|
|
34
|
+
const match = timing.match(/(\d+)\s*ms/);
|
|
35
|
+
return match ? Number.parseInt(match[1], 10) : 200;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const animationDurations: Record<AnimationName, number> = Object.fromEntries(
|
|
39
|
+
Object.entries(animationConfig).map(([k, v]) => [k, parseDurationMs(v)]),
|
|
40
|
+
) as Record<AnimationName, number>;
|
|
41
|
+
|
|
42
|
+
export function motionDurationMs(transition: TransitionProp | undefined): number {
|
|
43
|
+
if (transition === undefined) return 0;
|
|
44
|
+
return typeof transition === "string" && Object.hasOwn(animationDurations, transition)
|
|
45
|
+
? animationDurations[transition as AnimationName]
|
|
46
|
+
: 300;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function motionSeconds(transition: TransitionProp | undefined): number {
|
|
50
|
+
return motionDurationMs(transition) / 1000;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function motionSettleMs(transition: TransitionProp | undefined): number {
|
|
54
|
+
return transition === undefined ? 0 : Math.max(300, 3 * motionDurationMs(transition)) + 80;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function motionHideDelayMs(transition: TransitionProp | undefined): number {
|
|
58
|
+
return motionDurationMs(transition) + 50;
|
|
59
|
+
}
|
|
@@ -14,53 +14,48 @@
|
|
|
14
14
|
|
|
15
15
|
import { describe, expect, it } from "vitest";
|
|
16
16
|
import { layoutBreakpoints, OVERLAY_BREAKPOINT } from "./layoutTokens";
|
|
17
|
+
import { defaultScaleInputs, v5ScaleInputs } from "./scale/scaleInputs";
|
|
18
|
+
import { generateScale } from "./scale/generateScale";
|
|
17
19
|
import { DEFAULT_SIZE_RECIPE_INPUTS, generateSizeRecipes, sizeRecipeForToken } from "./sizeRecipes";
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"$
|
|
23
|
-
$2: 28,
|
|
24
|
-
"$2.5": 32,
|
|
25
|
-
$3: 36,
|
|
26
|
-
"$3.5": 40,
|
|
27
|
-
$4: 44,
|
|
28
|
-
$true: 44,
|
|
29
|
-
$5: 52,
|
|
30
|
-
$6: 64,
|
|
31
|
-
} as const;
|
|
32
|
-
|
|
33
|
-
const tamaguiRadiusTrue = 9;
|
|
34
|
-
const tamaguiSpaceTrue = 18;
|
|
35
|
-
|
|
36
|
-
describe("MPO-13 C1/C2 — $4 means ONE height, and it is 44", () => {
|
|
37
|
-
it("every recipe height equals the raw Tamagui $size token at the same key", () => {
|
|
21
|
+
const houseScale = generateScale(defaultScaleInputs);
|
|
22
|
+
|
|
23
|
+
describe("MPO-13 C1/C2 — $4 means ONE height, and it is 40", () => {
|
|
24
|
+
it("every recipe height equals the generated $size token at the same key", () => {
|
|
38
25
|
const { heights } = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
|
|
39
|
-
for (const
|
|
26
|
+
for (const token of Object.keys(heights)) {
|
|
27
|
+
const px = houseScale.tokens.size[token as keyof typeof houseScale.tokens.size];
|
|
40
28
|
expect(heights[token], `heights.${token}`).toBe(px);
|
|
41
29
|
}
|
|
42
30
|
});
|
|
43
31
|
|
|
44
|
-
it("
|
|
45
|
-
expect(DEFAULT_SIZE_RECIPE_INPUTS.
|
|
46
|
-
expect(sizeRecipeForToken("$4").height).toBe(
|
|
32
|
+
it("control is $size.true, so recipe and token cannot re-diverge", () => {
|
|
33
|
+
expect(DEFAULT_SIZE_RECIPE_INPUTS.control).toBe(houseScale.tokens.size.$true);
|
|
34
|
+
expect(sizeRecipeForToken("$4").height).toBe(houseScale.tokens.size.$4);
|
|
47
35
|
expect(sizeRecipeForToken("$true").height).toBe(sizeRecipeForToken("$4").height);
|
|
48
36
|
});
|
|
49
37
|
|
|
50
|
-
it("the $4 control is
|
|
38
|
+
it("the $4 control is 40 / 16 / 15 / 16 / 8", () => {
|
|
51
39
|
expect(sizeRecipeForToken("$4")).toEqual({
|
|
52
|
-
height:
|
|
53
|
-
paddingHorizontal:
|
|
54
|
-
fontSize:
|
|
40
|
+
height: 40,
|
|
41
|
+
paddingHorizontal: 16,
|
|
42
|
+
fontSize: 15,
|
|
55
43
|
iconSize: 16,
|
|
56
|
-
gap:
|
|
44
|
+
gap: 8,
|
|
57
45
|
});
|
|
58
46
|
});
|
|
59
47
|
});
|
|
60
48
|
|
|
61
|
-
|
|
49
|
+
it("v5 inputs retain the original size, space and radius anchors", () => {
|
|
50
|
+
const { tokens } = generateScale(v5ScaleInputs);
|
|
51
|
+
expect(tokens.size.$4).toBe(44);
|
|
52
|
+
expect(tokens.space.$4).toBe(18);
|
|
53
|
+
expect(tokens.radius.true).toBe(9);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("MPO-13 C3 — radius is $radius.true = 8", () => {
|
|
62
57
|
it("radiusPx is the stored input, at the measured value", () => {
|
|
63
|
-
expect(DEFAULT_SIZE_RECIPE_INPUTS.radiusPx).toBe(
|
|
58
|
+
expect(DEFAULT_SIZE_RECIPE_INPUTS.radiusPx).toBe(houseScale.tokens.radius.true);
|
|
64
59
|
});
|
|
65
60
|
|
|
66
61
|
it("gap tracks radius exactly at $4 (the reference's stated rule)", () => {
|
|
@@ -78,28 +73,30 @@ describe("MPO-13 C3 — radius is $radius.true = 9", () => {
|
|
|
78
73
|
describe("MPO-13 — the ratios are relationships, not literals", () => {
|
|
79
74
|
it("padding at $4 is $space.true, ~40% of the size step (reference §4)", () => {
|
|
80
75
|
const recipe = sizeRecipeForToken("$4");
|
|
81
|
-
expect(recipe.paddingHorizontal).toBe(
|
|
76
|
+
expect(recipe.paddingHorizontal).toBe(houseScale.tokens.space.$true);
|
|
82
77
|
const spaceToSize = recipe.paddingHorizontal / recipe.height;
|
|
83
78
|
expect(spaceToSize).toBeGreaterThan(0.38);
|
|
84
79
|
expect(spaceToSize).toBeLessThan(0.42);
|
|
85
80
|
});
|
|
86
81
|
|
|
87
|
-
it("
|
|
82
|
+
it("the chip family is one space step below control at the SAME height (LC-79)", () => {
|
|
88
83
|
const control = sizeRecipeForToken("$4");
|
|
89
|
-
const
|
|
90
|
-
expect(
|
|
91
|
-
expect(
|
|
92
|
-
|
|
93
|
-
expect(compact.paddingHorizontal).toBe(16);
|
|
84
|
+
const chip = sizeRecipeForToken("$4", { family: "chip" });
|
|
85
|
+
expect(chip.height).toBe(control.height);
|
|
86
|
+
expect(chip.paddingHorizontal).toBeLessThan(control.paddingHorizontal);
|
|
87
|
+
expect(chip.paddingHorizontal).toBe(houseScale.tokens.space["$3.5"]);
|
|
94
88
|
});
|
|
95
89
|
|
|
96
|
-
it("re-pointing
|
|
97
|
-
const
|
|
90
|
+
it("re-pointing control re-derives every channel from the scale — no hardcoded 44", () => {
|
|
91
|
+
const scale = generateScale({ control: 80, type: 30 });
|
|
92
|
+
const scaled = generateSizeRecipes({ control: 80, type: 30 });
|
|
98
93
|
const base = sizeRecipeForToken("$4");
|
|
99
94
|
expect(scaled.recipes.$4.height).toBe(base.height * 2);
|
|
100
|
-
expect(scaled.recipes.$4.
|
|
95
|
+
expect(scaled.recipes.$4.height).toBe(scale.tokens.size.$4);
|
|
96
|
+
// $space follows Tamagui's own sizeToSpace, which is not linear in size.
|
|
97
|
+
expect(scaled.recipes.$4.paddingHorizontal).toBe(scale.tokens.space.$4);
|
|
101
98
|
expect(scaled.recipes.$4.fontSize).toBe(base.fontSize * 2);
|
|
102
|
-
expect(scaled.recipes.$4.gap).toBe(
|
|
99
|
+
expect(scaled.recipes.$4.gap).toBe(scale.tokens.radius[4]);
|
|
103
100
|
});
|
|
104
101
|
});
|
|
105
102
|
|
|
@@ -131,18 +128,18 @@ describe("MPO-13 C4/C5 — one breakpoint system", () => {
|
|
|
131
128
|
});
|
|
132
129
|
|
|
133
130
|
describe("MPO-13 — the touch floor stops fighting the desktop ramp", () => {
|
|
134
|
-
it("$4
|
|
131
|
+
it("$4 is below the touch floor; touch lifts it, never shrinks it", () => {
|
|
135
132
|
const desktop = sizeRecipeForToken("$4");
|
|
136
133
|
const touch = sizeRecipeForToken("$4", { touch: true });
|
|
137
|
-
expect(desktop.height).
|
|
138
|
-
expect(touch.height
|
|
134
|
+
expect(desktop.height).toBeLessThan(DEFAULT_SIZE_RECIPE_INPUTS.touchFloor);
|
|
135
|
+
expect(touch).toEqual({ ...desktop, height: Math.max(desktop.height, 44) });
|
|
139
136
|
});
|
|
140
137
|
|
|
141
138
|
it("every touch height clears the floor", () => {
|
|
142
139
|
const { heightsTouch } = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
|
|
143
140
|
for (const [token, px] of Object.entries(heightsTouch)) {
|
|
144
|
-
expect(px, `heightsTouch.${token}`).
|
|
145
|
-
DEFAULT_SIZE_RECIPE_INPUTS.touchFloor,
|
|
141
|
+
expect(px, `heightsTouch.${token}`).toBe(
|
|
142
|
+
Math.max(sizeRecipeForToken(token).height, DEFAULT_SIZE_RECIPE_INPUTS.touchFloor),
|
|
146
143
|
);
|
|
147
144
|
}
|
|
148
145
|
});
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* implement until that is settled.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { animationsCSS } from "@tamagui/config/v5-css";
|
|
24
25
|
import { defaultConfig } from "@tamagui/config/v5";
|
|
25
26
|
import { describe, expect, it } from "vitest";
|
|
26
27
|
import { animationConfig } from "./animations/css";
|
|
@@ -71,7 +72,7 @@ describe("MPO-19 X13 — one Tamagui era, and it is load-bearing", () => {
|
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
describe("MPO-19 X8/X9 — the type scale IS the era's, not a copy of it", () => {
|
|
74
|
-
it("body and heading tables
|
|
75
|
+
it("body and heading tables follow v5 with heading true matched to size four", () => {
|
|
75
76
|
// The relationship, not the literals: re-point the era and the whole
|
|
76
77
|
// scale follows. A transcribed table would pass a literal snapshot and
|
|
77
78
|
// fail this.
|
|
@@ -81,7 +82,10 @@ describe("MPO-19 X8/X9 — the type scale IS the era's, not a copy of it", () =>
|
|
|
81
82
|
] as const) {
|
|
82
83
|
expect(table(ours.size), `${name}.size`).toEqual(table(theirs.size));
|
|
83
84
|
expect(table(ours.lineHeight), `${name}.lineHeight`).toEqual(table(theirs.lineHeight));
|
|
84
|
-
|
|
85
|
+
const weight = table(theirs.weight);
|
|
86
|
+
expect(table(ours.weight), `${name}.weight`).toEqual(
|
|
87
|
+
name === "heading" ? { ...weight, true: weight["4"] } : weight,
|
|
88
|
+
);
|
|
85
89
|
}
|
|
86
90
|
});
|
|
87
91
|
|
|
@@ -110,7 +114,7 @@ describe("MPO-19 X8/X9 — the type scale IS the era's, not a copy of it", () =>
|
|
|
110
114
|
expect(weight["1"]).toBe("600");
|
|
111
115
|
expect(weight["6"]).toBe("700");
|
|
112
116
|
expect(weight["9"]).toBe("800");
|
|
113
|
-
// H1 is $10
|
|
117
|
+
// H1 is $10 at 40px/800 on the default scale.
|
|
114
118
|
expect(table(defaultHeadingFont.size)["10"]).toBe(40);
|
|
115
119
|
expect(weight["10"]).toBe("800");
|
|
116
120
|
});
|
|
@@ -172,7 +176,7 @@ describe("MPO-19 X4 — the interaction ramp is the template's again", () => {
|
|
|
172
176
|
// M5's focus ring is an improvement (stock is rgba(ink, 0.2), measured
|
|
173
177
|
// 1.36:1), and the semantic aliases contradict nothing.
|
|
174
178
|
expect(derived).toHaveProperty("outlineColor");
|
|
175
|
-
expect(derived).toHaveProperty("placeholderColor");
|
|
179
|
+
expect(derived).not.toHaveProperty("placeholderColor");
|
|
176
180
|
for (const alias of ["textMuted", "textSubtle", "borderStrong", "surfaceStrong"]) {
|
|
177
181
|
expect(derived, `alias ${alias}`).toHaveProperty(alias);
|
|
178
182
|
}
|
|
@@ -191,9 +195,6 @@ describe("MPO-19 X5/X6 — surfaces address semantic tokens, so sub-themes work"
|
|
|
191
195
|
});
|
|
192
196
|
|
|
193
197
|
it("a raw ramp step is inert inside surface1/surface2, which is the whole point", () => {
|
|
194
|
-
// Structural, not chromatic: `$background` resolves to `$color1` under the
|
|
195
|
-
// default template so the rendered colour did not change. What changed is
|
|
196
|
-
// that `<Theme name="surface1">` can now move it.
|
|
197
198
|
const painted = Object.values(knobProps())
|
|
198
199
|
.filter((v): v is Record<string, unknown> => !!v && typeof v === "object")
|
|
199
200
|
.map((fragment) => fragment.backgroundColor)
|
|
@@ -225,15 +226,13 @@ describe("MPO-19 X7 — the focus ring is inset only where a clip demands it", (
|
|
|
225
226
|
});
|
|
226
227
|
|
|
227
228
|
describe("MPO-19 X10 — a shared animation name means the shared thing", () => {
|
|
228
|
-
it("medium is the
|
|
229
|
-
expect(animationConfig.medium).toBe(
|
|
229
|
+
it("medium is the v5 ease and duration", () => {
|
|
230
|
+
expect(animationConfig.medium).toBe(animationsCSS.animations.medium);
|
|
230
231
|
});
|
|
231
232
|
|
|
232
|
-
it("every other shared name
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
expect(animationConfig.slow).toBe("ease-in 500ms");
|
|
236
|
-
expect(animationConfig.quick).toBe("ease-in 100ms");
|
|
233
|
+
it("every other shared name matches v5", () => {
|
|
234
|
+
for (const stop of ["bouncy", "lazy", "slow", "quick"] as const)
|
|
235
|
+
expect(animationConfig[stop]).toBe(animationsCSS.animations[stop]);
|
|
237
236
|
});
|
|
238
237
|
});
|
|
239
238
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defaultChildrenThemes } from "@tamagui/themes/v5";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
4
|
+
import { createThemesBuilder } from "./createThemes";
|
|
5
|
+
import { defaultAccentTheme } from "./defaults/accent";
|
|
6
|
+
import { defaultBaseTheme } from "./defaults/base";
|
|
7
|
+
import { defaultBuilderOptions } from "./defaults/builderOptions";
|
|
8
|
+
|
|
9
|
+
const themes = createThemesBuilder(
|
|
10
|
+
defaultBaseTheme,
|
|
11
|
+
defaultAccentTheme,
|
|
12
|
+
defaultBuilderOptions,
|
|
13
|
+
).themes() as Record<string, Record<string, string>>;
|
|
14
|
+
|
|
15
|
+
const hex = (raw: string) => {
|
|
16
|
+
const value = normalizeToHex(raw);
|
|
17
|
+
if (!value) throw new Error(`not an opaque colour: ${raw}`);
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
20
|
+
const ratio = (a: string, b: string) =>
|
|
21
|
+
contrastRatio(relativeLuminance(hex(a)), relativeLuminance(hex(b)));
|
|
22
|
+
|
|
23
|
+
const hues = Object.keys(defaultChildrenThemes).filter((hue) => hue !== "neutral");
|
|
24
|
+
const stock = (scheme: "light" | "dark", hue: string) =>
|
|
25
|
+
hex(
|
|
26
|
+
(defaultChildrenThemes as Record<string, Record<string, Record<string, string>>>)[hue][scheme][
|
|
27
|
+
`${hue}11`
|
|
28
|
+
],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
describe("named step-11 ink holds 4.5:1 on the v5 page and surface1 card", () => {
|
|
32
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
33
|
+
const theme = themes[scheme];
|
|
34
|
+
for (const hue of hues) {
|
|
35
|
+
it(`${scheme} $${hue}11 reads on $background and $color3`, () => {
|
|
36
|
+
expect(ratio(theme[`${hue}11`], theme.background)).toBeGreaterThanOrEqual(4.5);
|
|
37
|
+
expect(ratio(theme[`${hue}11`], theme.color3)).toBeGreaterThanOrEqual(4.5);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
it("keeps v5's step 11 where it holds and darkens only the failing light hues", () => {
|
|
43
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
44
|
+
const theme = themes[scheme];
|
|
45
|
+
for (const hue of hues) {
|
|
46
|
+
const shipped = stock(scheme, hue);
|
|
47
|
+
const holds = [theme.background, theme.color3].every(
|
|
48
|
+
(surface) => ratio(shipped, surface) >= 4.5,
|
|
49
|
+
);
|
|
50
|
+
if (holds) expect(hex(theme[`${hue}11`])).toBe(shipped);
|
|
51
|
+
else
|
|
52
|
+
expect(relativeLuminance(hex(theme[`${hue}11`]))).toBeLessThan(
|
|
53
|
+
relativeLuminance(shipped),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
expect(hex(themes.light.green11)).not.toBe(stock("light", "green"));
|
|
58
|
+
expect(hex(themes.dark.green11)).toBe(stock("dark", "green"));
|
|
59
|
+
});
|
|
60
|
+
});
|