@multiplatform.one/theme 7.4.1 → 7.5.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/README.md +2 -1
- package/package.json +5 -5
- package/src/figma/figmaTokens.spec.ts +25 -21
- package/src/figma/figmaTokens.ts +2 -2
- package/src/font.ts +6 -19
- package/src/keyboardFocusRing.ts +12 -6
- package/src/theme/FontKnobStyles.tsx +5 -3
- package/src/theme/Tint.tsx +3 -2
- package/src/theme/animations/css.ts +14 -1
- package/src/theme/colorRules.spec.ts +233 -2
- package/src/theme/colorRules.ts +118 -0
- package/src/theme/cornerSmoothing.native.ts +39 -2
- package/src/theme/cornerSmoothing.spec.ts +213 -3
- package/src/theme/cornerSmoothing.ts +132 -10
- package/src/theme/createDefaultThemeConfig.spec.ts +107 -1
- package/src/theme/createDefaultThemeConfig.ts +27 -8
- package/src/theme/defaults/builderOptions.ts +23 -10
- package/src/theme/defaults/categoryFonts.ts +21 -0
- package/src/theme/defaults/fonts.spec.ts +93 -10
- package/src/theme/defaults/fonts.theme.spec.tsx +128 -0
- package/src/theme/defaults/fonts.ts +155 -27
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +0 -11
- package/src/theme/focusState.spec.ts +110 -1
- package/src/theme/focusState.ts +32 -7
- package/src/theme/fontCategoryStacks.ts +24 -0
- package/src/theme/fontKnobStyles.spec.ts +15 -2
- package/src/theme/glyphPaint.spec.ts +26 -0
- package/src/theme/glyphPaint.ts +46 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/knobs.ts +18 -2
- package/src/theme/layoutTokens.spec.ts +3 -1
- package/src/theme/layoutTokens.ts +47 -4
- package/src/theme/mpo13Proof.spec.ts +3 -3
- package/src/theme/mpo19Proof.spec.ts +285 -0
- package/src/theme/presets.ts +1 -1
- package/src/theme/radiusClass.spec.ts +394 -0
- package/src/theme/radiusClass.ts +203 -4
- package/src/theme/radiusClassTable.generated.ts +121 -0
- package/src/theme/recipeInputs.ts +223 -36
- package/src/theme/recipes.ts +68 -4
- package/src/theme/resolveKnobs.spec.ts +139 -11
- package/src/theme/resolveKnobs.ts +190 -25
- package/src/theme/sizeLadder.spec.ts +84 -0
- package/src/theme/sizeRecipes.spec.ts +19 -6
- package/src/theme/sizeRecipes.ts +43 -13
- package/src/theme/theme.native.tsx +2 -0
- package/src/theme/theme.provider.spec.ts +9 -0
- package/src/theme/theme.tsx +6 -3
- package/src/theme/tint.spec.tsx +36 -35
- package/src/theme/useResolvedKnobs.ts +6 -3
- package/types/figma/figmaTokens.d.ts +1 -1
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/font.d.ts.map +1 -1
- package/types/keyboardFocusRing.d.ts +11 -5
- package/types/keyboardFocusRing.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts +2 -1
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Tint.d.ts +2 -1
- package/types/theme/Tint.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +15 -2
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +59 -1
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.d.ts +42 -20
- package/types/theme/cornerSmoothing.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.native.d.ts +22 -2
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +21 -3
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +19 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +135 -2
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- 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.map +1 -1
- package/types/theme/focusState.d.ts +32 -6
- package/types/theme/focusState.d.ts.map +1 -1
- package/types/theme/fontCategoryStacks.d.ts +11 -0
- package/types/theme/fontCategoryStacks.d.ts.map +1 -0
- package/types/theme/glyphPaint.d.ts +16 -0
- package/types/theme/glyphPaint.d.ts.map +1 -0
- package/types/theme/index.d.ts +3 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +10 -2
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +46 -4
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +96 -4
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +146 -0
- package/types/theme/radiusClassTable.generated.d.ts.map +1 -0
- package/types/theme/recipeInputs.d.ts +85 -14
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +78 -4
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +12 -1
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +27 -14
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/useResolvedKnobs.d.ts +2 -2
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
|
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
defaultKnobs,
|
|
4
4
|
emptyStateKnobs,
|
|
5
|
+
FillStyle,
|
|
5
6
|
Space,
|
|
6
7
|
Size,
|
|
7
8
|
Density,
|
|
@@ -39,9 +40,13 @@ function knobsWith(overrides: Partial<Knobs>): Knobs {
|
|
|
39
40
|
|
|
40
41
|
const expectedKnobPropKeys: (keyof KnobProps)[] = [
|
|
41
42
|
"borderRadius",
|
|
43
|
+
"borderRadiusNested",
|
|
42
44
|
"borderRadiusOuter",
|
|
43
45
|
"containerRadius",
|
|
44
46
|
"elevation",
|
|
47
|
+
"overlayElevation",
|
|
48
|
+
"elevationChrome",
|
|
49
|
+
"scheme",
|
|
45
50
|
"surface",
|
|
46
51
|
"cardSurface",
|
|
47
52
|
"elevatedSurface",
|
|
@@ -222,6 +227,13 @@ describe("resolveKnobs", () => {
|
|
|
222
227
|
expect(resolveKnobs(knobsWith({ elevation: "small" })).knobProps.elevation).toBe("$1");
|
|
223
228
|
});
|
|
224
229
|
|
|
230
|
+
it("overlay elevation at small is $2, not the control $1 token", () => {
|
|
231
|
+
const { knobProps } = resolveKnobs(knobsWith({ elevation: "small" }));
|
|
232
|
+
expect(knobProps.overlayElevation).toBe("$2");
|
|
233
|
+
expect(knobProps.elevatedSurface.elevation).toBe("$2");
|
|
234
|
+
expect(knobProps.elevatedSurface.elevation).not.toBe(knobProps.elevation);
|
|
235
|
+
});
|
|
236
|
+
|
|
225
237
|
it('maps "medium" → "$2"', () => {
|
|
226
238
|
expect(resolveKnobs(knobsWith({ elevation: "medium" })).knobProps.elevation).toBe("$2");
|
|
227
239
|
});
|
|
@@ -229,6 +241,41 @@ describe("resolveKnobs", () => {
|
|
|
229
241
|
it('maps "large" → "$4"', () => {
|
|
230
242
|
expect(resolveKnobs(knobsWith({ elevation: "large" })).knobProps.elevation).toBe("$4");
|
|
231
243
|
});
|
|
244
|
+
|
|
245
|
+
it("light scheme keeps overlay fill at $background and paints a black shadow", () => {
|
|
246
|
+
const { knobProps } = resolveKnobs(knobsWith({ elevation: "large" }), undefined, "light");
|
|
247
|
+
expect(knobProps.scheme).toBe("light");
|
|
248
|
+
expect(knobProps.elevatedSurface.backgroundColor).toBe("$background");
|
|
249
|
+
expect(knobProps.cardSurface.backgroundColor).toBe("$background");
|
|
250
|
+
expect(knobProps.inputBackground).toBe("$background");
|
|
251
|
+
expect(knobProps.elevatedSurface.shadowColor).toBe("#000");
|
|
252
|
+
expect(knobProps.elevatedSurface.shadowOpacity).toBe(0.15);
|
|
253
|
+
expect(knobProps.elevationChrome.elevation).toBe(8);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("dark scheme tints overlay/card/input up the existing ramp (LC-85)", () => {
|
|
257
|
+
const none = resolveKnobs(knobsWith({ elevation: "none" }), undefined, "dark");
|
|
258
|
+
const small = resolveKnobs(knobsWith({ elevation: "small" }), undefined, "dark");
|
|
259
|
+
const large = resolveKnobs(knobsWith({ elevation: "large" }), undefined, "dark");
|
|
260
|
+
expect(none.knobProps.elevatedSurface.backgroundColor).toBe("$background");
|
|
261
|
+
expect(small.knobProps.elevatedSurface.backgroundColor).toBe("$color2");
|
|
262
|
+
expect(large.knobProps.elevatedSurface.backgroundColor).toBe("$color4");
|
|
263
|
+
expect(large.knobProps.cardSurface.backgroundColor).toBe("$color4");
|
|
264
|
+
expect(large.knobProps.inputBackground).toBe("$color5");
|
|
265
|
+
expect(large.knobProps.elevatedSurface.shadowColor).toBe("$color12");
|
|
266
|
+
expect(large.knobProps.elevatedSurface.shadowOpacity).toBe(0.12);
|
|
267
|
+
expect(none.knobProps.elevationChrome).toEqual({});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("dark outlined inputs stay transparent — tint is for filled surfaces", () => {
|
|
271
|
+
const { knobProps } = resolveKnobs(
|
|
272
|
+
knobsWith({ elevation: "large", fillStyle: "outlined" }),
|
|
273
|
+
undefined,
|
|
274
|
+
"dark",
|
|
275
|
+
);
|
|
276
|
+
expect(knobProps.inputBackground).toBe("transparent");
|
|
277
|
+
expect(knobProps.elevatedSurface.backgroundColor).toBe("$color4");
|
|
278
|
+
});
|
|
232
279
|
});
|
|
233
280
|
|
|
234
281
|
// ── elevation on NATIVE (SB-N-04) ──────────────────────────
|
|
@@ -251,10 +298,15 @@ describe("resolveKnobs", () => {
|
|
|
251
298
|
knobsWith({ elevation: "small", hover: { elevation: "medium" } }),
|
|
252
299
|
);
|
|
253
300
|
expect(small.knobProps.elevation).toBe(20);
|
|
254
|
-
expect(small.knobProps.
|
|
301
|
+
expect(small.knobProps.overlayElevation).toBe(28);
|
|
302
|
+
expect(small.knobProps.elevatedSurface.elevation).toBe(28);
|
|
303
|
+
expect(small.knobProps.elevatedSurface.elevation).not.toBe(small.knobProps.elevation);
|
|
255
304
|
expect(small.elevation.hoverKnobProps?.elevation).toBe(28);
|
|
256
305
|
expect(resolveNative(knobsWith({ elevation: "medium" })).knobProps.elevation).toBe(28);
|
|
257
306
|
expect(resolveNative(knobsWith({ elevation: "large" })).knobProps.elevation).toBe(44);
|
|
307
|
+
expect(
|
|
308
|
+
resolveNative(knobsWith({ elevation: "large" })).knobProps.elevationChrome.elevation,
|
|
309
|
+
).toBe(8);
|
|
258
310
|
} finally {
|
|
259
311
|
vi.doUnmock("@multiplatform.one/platform");
|
|
260
312
|
vi.resetModules();
|
|
@@ -424,7 +476,7 @@ describe("resolveKnobs", () => {
|
|
|
424
476
|
it("cardSurface = bg + radius + panelPadding, no border/shadow", () => {
|
|
425
477
|
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
426
478
|
expect(knobProps.cardSurface).toEqual({
|
|
427
|
-
backgroundColor: "$
|
|
479
|
+
backgroundColor: "$background",
|
|
428
480
|
borderRadius: "$4",
|
|
429
481
|
padding: "$4",
|
|
430
482
|
});
|
|
@@ -443,13 +495,27 @@ describe("resolveKnobs", () => {
|
|
|
443
495
|
expect(knobProps.cardSurface.borderRadius).toBe("$6");
|
|
444
496
|
});
|
|
445
497
|
|
|
446
|
-
it("elevatedSurface = surface + radius + elevation", () => {
|
|
498
|
+
it("elevatedSurface = surface + radius + overlay elevation + paint", () => {
|
|
447
499
|
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
448
|
-
expect(knobProps.elevatedSurface).
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
500
|
+
expect(knobProps.elevatedSurface.elevation).toBe(knobProps.overlayElevation);
|
|
501
|
+
expect(knobProps.elevatedSurface.elevation).not.toBe(knobProps.elevation);
|
|
502
|
+
expect(knobProps.elevatedSurface.backgroundColor).toBe(knobProps.surface.backgroundColor);
|
|
503
|
+
expect(knobProps.elevatedSurface.borderColor).toBe(knobProps.surface.borderColor);
|
|
504
|
+
expect(knobProps.elevatedSurface.borderWidth).toBe(knobProps.surface.borderWidth);
|
|
505
|
+
expect(knobProps.elevatedSurface.borderRadius).toBe(knobProps.borderRadius.borderRadius);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("overlay elevation token differs from the control elevation token", () => {
|
|
509
|
+
for (const elevation of ["small", "medium", "large"] as const) {
|
|
510
|
+
const { knobProps } = resolveKnobs(knobsWith({ elevation }));
|
|
511
|
+
expect(knobProps.overlayElevation, elevation).toBeDefined();
|
|
512
|
+
expect(knobProps.overlayElevation, elevation).not.toBe(knobProps.elevation);
|
|
513
|
+
expect(knobProps.elevatedSurface.elevation, elevation).toBe(knobProps.overlayElevation);
|
|
514
|
+
}
|
|
515
|
+
const none = resolveKnobs(knobsWith({ elevation: "none" }));
|
|
516
|
+
expect(none.knobProps.elevation).toBeUndefined();
|
|
517
|
+
expect(none.knobProps.overlayElevation).toBeUndefined();
|
|
518
|
+
expect(none.knobProps.elevatedSurface.elevation).toBeUndefined();
|
|
453
519
|
});
|
|
454
520
|
|
|
455
521
|
it("featureSurface uses big radius, $6 padding, and an ultra-soft shadow", () => {
|
|
@@ -543,7 +609,7 @@ describe("resolveKnobs", () => {
|
|
|
543
609
|
describe("cornerSmoothing knob", () => {
|
|
544
610
|
const radiusFragments = [
|
|
545
611
|
"borderRadius",
|
|
546
|
-
"
|
|
612
|
+
"borderRadiusNested",
|
|
547
613
|
"containerRadius",
|
|
548
614
|
"cardSurface",
|
|
549
615
|
"elevatedSurface",
|
|
@@ -585,7 +651,7 @@ describe("resolveKnobs", () => {
|
|
|
585
651
|
);
|
|
586
652
|
// $0 corners have no arc — square world stays square (Axiom 3 NULL).
|
|
587
653
|
expect(knobProps.borderRadius).not.toHaveProperty("className");
|
|
588
|
-
expect(knobProps.
|
|
654
|
+
expect(knobProps.borderRadiusNested).not.toHaveProperty("className");
|
|
589
655
|
expect(knobProps.containerRadius).not.toHaveProperty("className");
|
|
590
656
|
expect(knobProps.cardSurface).not.toHaveProperty("className");
|
|
591
657
|
expect(knobProps.elevatedSurface).not.toHaveProperty("className");
|
|
@@ -604,7 +670,7 @@ describe("resolveKnobs", () => {
|
|
|
604
670
|
expect(knobProps.elevatedSurface).not.toHaveProperty("className");
|
|
605
671
|
// Capped/outer fragments stay plain arcs (cap = padding px, outer ≤ $5)
|
|
606
672
|
// and keep smoothing.
|
|
607
|
-
expect(knobProps.
|
|
673
|
+
expect(knobProps.borderRadiusNested.className).toBe(cornerSmoothClassName);
|
|
608
674
|
expect(knobProps.containerRadius.className).toBe(cornerSmoothClassName);
|
|
609
675
|
expect(knobProps.cardSurface.className).toBe(cornerSmoothClassName);
|
|
610
676
|
expect(knobProps.featureSurface.className).toBe(cornerSmoothClassName);
|
|
@@ -681,6 +747,54 @@ describe("resolveKnobs", () => {
|
|
|
681
747
|
const { knobProps } = resolveKnobs(knobsWith({ fillStyle: "outlined" }));
|
|
682
748
|
expect(knobProps.outlined).toBe(true);
|
|
683
749
|
});
|
|
750
|
+
|
|
751
|
+
it("union is filled | outlined only (R9 G9)", () => {
|
|
752
|
+
expect(Object.values(FillStyle)).toEqual(["filled", "outlined"]);
|
|
753
|
+
expect(Object.values(FillStyle)).toHaveLength(2);
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
describe("separator weight is not a knob (R9 G10)", () => {
|
|
758
|
+
it("Knobs has no separatorWeight channel", () => {
|
|
759
|
+
expect("separatorWeight" in defaultKnobs).toBe(false);
|
|
760
|
+
expect(Object.keys(defaultKnobs)).not.toContain("separatorWeight");
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
describe("fillStyle × borderWidth (LC-86 OUTLINED-HAIRLINE)", () => {
|
|
765
|
+
it("filled + none is chromeless (width 0)", () => {
|
|
766
|
+
const { knobProps } = resolveKnobs(knobsWith({ fillStyle: "filled", borderWidth: "none" }));
|
|
767
|
+
expect(knobProps.borderRadius.borderWidth).toBe(0);
|
|
768
|
+
expect(knobProps.surface.borderWidth).toBe(0);
|
|
769
|
+
expect(knobProps.elevatedSurface.borderWidth).toBe(0);
|
|
770
|
+
expect(knobProps.inputSurface.borderWidth).toBe(0);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("outlined + none still draws the hairline (small stop, 0.5)", () => {
|
|
774
|
+
const { knobProps } = resolveKnobs(knobsWith({ fillStyle: "outlined", borderWidth: "none" }));
|
|
775
|
+
expect(knobProps.borderRadius.borderWidth).toBe(0.5);
|
|
776
|
+
expect(knobProps.surface.borderWidth).toBe(0.5);
|
|
777
|
+
expect(knobProps.elevatedSurface.borderWidth).toBe(0.5);
|
|
778
|
+
expect(knobProps.inputSurface.borderWidth).toBe(0.5);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("outlined + small stays at the hairline, not a hidden 1px floor", () => {
|
|
782
|
+
const { knobProps } = resolveKnobs(
|
|
783
|
+
knobsWith({ fillStyle: "outlined", borderWidth: "small" }),
|
|
784
|
+
);
|
|
785
|
+
expect(knobProps.borderRadius.borderWidth).toBe(0.5);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("outlined + medium/large keep the requested width", () => {
|
|
789
|
+
expect(
|
|
790
|
+
resolveKnobs(knobsWith({ fillStyle: "outlined", borderWidth: "medium" })).knobProps
|
|
791
|
+
.borderRadius.borderWidth,
|
|
792
|
+
).toBe(1);
|
|
793
|
+
expect(
|
|
794
|
+
resolveKnobs(knobsWith({ fillStyle: "outlined", borderWidth: "large" })).knobProps
|
|
795
|
+
.borderRadius.borderWidth,
|
|
796
|
+
).toBe(2);
|
|
797
|
+
});
|
|
684
798
|
});
|
|
685
799
|
|
|
686
800
|
// ── pointy ────────────────────────────────────────────────
|
|
@@ -1005,6 +1119,20 @@ describe("applyDensity", () => {
|
|
|
1005
1119
|
expect(resolveKnobs(knobsWith({ density: "compact" })).knobProps.density).toBe("compact");
|
|
1006
1120
|
});
|
|
1007
1121
|
|
|
1122
|
+
it("compact and comfortable differ in padX while height stays 44 (LC-76)", () => {
|
|
1123
|
+
const comfortable = resolveKnobs(knobsWith({ density: "comfortable" }));
|
|
1124
|
+
const compact = resolveKnobs(knobsWith({ density: "compact" }));
|
|
1125
|
+
expect(comfortable.knobProps.control.height).toBe(44);
|
|
1126
|
+
expect(compact.knobProps.control.height).toBe(44);
|
|
1127
|
+
expect(comfortable.knobProps.sizeToken).toBe("$4");
|
|
1128
|
+
expect(compact.knobProps.sizeToken).toBe("$4");
|
|
1129
|
+
expect(compact.knobProps.control.paddingHorizontal).not.toBe(
|
|
1130
|
+
comfortable.knobProps.control.paddingHorizontal,
|
|
1131
|
+
);
|
|
1132
|
+
expect(comfortable.knobProps.control.paddingHorizontal).toBe(18);
|
|
1133
|
+
expect(compact.knobProps.control.paddingHorizontal).toBe(16);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1008
1136
|
it("compact density produces the same sizeToken and a smaller gap than comfortable", () => {
|
|
1009
1137
|
const comfortable = resolveKnobs(applyDensity(knobsWith({ size: "medium", space: "medium" })));
|
|
1010
1138
|
const compact = resolveKnobs(
|
|
@@ -9,11 +9,13 @@ import {
|
|
|
9
9
|
STATE_LAYER_PRESS,
|
|
10
10
|
withDefaultStateLayer,
|
|
11
11
|
} from "./focusState";
|
|
12
|
-
import type { CornerSmoothing, Knobs, PageTitleScale, StateKnobs } from "./knobs";
|
|
12
|
+
import type { CornerSmoothing, Elevation, Knobs, PageTitleScale, StateKnobs } from "./knobs";
|
|
13
13
|
import { pressTargetHitSlop } from "./layoutTokens";
|
|
14
14
|
import type {
|
|
15
15
|
ControlStateProps,
|
|
16
16
|
DisabledRecipe,
|
|
17
|
+
ElevationChromeProps,
|
|
18
|
+
ElevationPaintProps,
|
|
17
19
|
ElevationStateProps,
|
|
18
20
|
KnobProps,
|
|
19
21
|
KnobPropsOverride,
|
|
@@ -64,7 +66,7 @@ export function applyDensity(knobs: Knobs, compactOverride?: boolean): Knobs {
|
|
|
64
66
|
|
|
65
67
|
// ── Knob-to-token mapping tables ──────────────────────────────
|
|
66
68
|
|
|
67
|
-
const borderRadiusMap = {
|
|
69
|
+
export const borderRadiusMap = {
|
|
68
70
|
none: "$0",
|
|
69
71
|
small: "$2",
|
|
70
72
|
medium: "$4",
|
|
@@ -72,7 +74,12 @@ const borderRadiusMap = {
|
|
|
72
74
|
full: "$12",
|
|
73
75
|
} as const;
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
// Radius for chrome nested INSIDE a radius-bearing frame. Smaller than
|
|
78
|
+
// `borderRadiusMap` at every stop that has room to be — a child corner equal
|
|
79
|
+
// to its parent's reads as a second frame rather than as part of one.
|
|
80
|
+
// MPO-19 X12: was `outerRadiusMap`, feeding a fragment called
|
|
81
|
+
// `borderRadiusOuter` that every consumer used for inner chrome.
|
|
82
|
+
const nestedRadiusMap = {
|
|
76
83
|
none: "$0",
|
|
77
84
|
small: "$2",
|
|
78
85
|
medium: "$3",
|
|
@@ -243,10 +250,121 @@ const fontWeightMap = {
|
|
|
243
250
|
// be cast to java.lang.Double" (SB-N-04). The numbers are the exact
|
|
244
251
|
// tokens.size values Tamagui resolves those tokens to ($1=20, $2=28, $4=44),
|
|
245
252
|
// so Tamagui consumers render identical shadows either way.
|
|
246
|
-
|
|
253
|
+
//
|
|
254
|
+
// Control elevation ($1 at the default small stop) is the in-page Button
|
|
255
|
+
// token. Overlay elevation is a DISTINCT token one step above that stop so
|
|
256
|
+
// a dialog over a scrim and a Button on the page do not read as the same
|
|
257
|
+
// distance off the surface (DG-OVL-01 / E-OVERLAY). elevatedSurface binds
|
|
258
|
+
// the overlay token — not the control one.
|
|
259
|
+
export const elevationMap: Record<string, string | number | undefined> = isWeb
|
|
247
260
|
? { none: undefined, small: "$1", medium: "$2", large: "$4" }
|
|
248
261
|
: { none: undefined, small: 20, medium: 28, large: 44 };
|
|
249
262
|
|
|
263
|
+
export const overlayElevationMap: Record<string, string | number | undefined> = isWeb
|
|
264
|
+
? { none: undefined, small: "$2", medium: "$3", large: "$5" }
|
|
265
|
+
: { none: undefined, small: 28, medium: 36, large: 52 };
|
|
266
|
+
|
|
267
|
+
/** Scheme the resolver was invoked with. Defaults light so unit tests stay stable. */
|
|
268
|
+
export type ColorScheme = "light" | "dark";
|
|
269
|
+
|
|
270
|
+
// Existing 12-step ramp (colorRules groups). Do not invent overlay tokens:
|
|
271
|
+
// dark elevation is a lighter $color step. Overlay/card rest on $color1;
|
|
272
|
+
// filled inputs rest on $color2 and step from there.
|
|
273
|
+
const elevationTintOverlay: Record<Elevation, string> = {
|
|
274
|
+
none: "$color1",
|
|
275
|
+
small: "$color2",
|
|
276
|
+
medium: "$color3",
|
|
277
|
+
large: "$color4",
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const elevationTintInput: Record<Elevation, string> = {
|
|
281
|
+
none: "$color2",
|
|
282
|
+
small: "$color3",
|
|
283
|
+
medium: "$color4",
|
|
284
|
+
large: "$color5",
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Axiom 14 light source: y-offset:blur = 0.5. Metrics match the maps
|
|
288
|
+
// InputParts / getElevationWrapperProps already shipped.
|
|
289
|
+
const elevationShadowMetrics: Record<
|
|
290
|
+
"$1" | "$2" | "$4",
|
|
291
|
+
{ offsetY: number; radius: number; android: number }
|
|
292
|
+
> = {
|
|
293
|
+
$1: { offsetY: 1, radius: 2, android: 2 },
|
|
294
|
+
$2: { offsetY: 3, radius: 6, android: 4 },
|
|
295
|
+
$4: { offsetY: 10, radius: 20, android: 8 },
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const elevationKeyAlias: Record<string, "$1" | "$2" | "$4"> = {
|
|
299
|
+
$1: "$1",
|
|
300
|
+
$2: "$2",
|
|
301
|
+
$3: "$2",
|
|
302
|
+
$4: "$4",
|
|
303
|
+
$5: "$4",
|
|
304
|
+
"20": "$1",
|
|
305
|
+
"28": "$2",
|
|
306
|
+
"36": "$2",
|
|
307
|
+
"44": "$4",
|
|
308
|
+
"52": "$4",
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const webShadowLight: Record<"$1" | "$2" | "$4", string> = {
|
|
312
|
+
$1: "0 1px 2px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.08)",
|
|
313
|
+
$2: "0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.10)",
|
|
314
|
+
$4: "0 10px 20px rgba(0,0,0,0.15), 0 3px 6px rgba(0,0,0,0.10)",
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const webShadowDark: Record<"$1" | "$2" | "$4", string> = {
|
|
318
|
+
$1: "0 1px 2px rgba(255,255,255,0.08), 0 0 1px rgba(255,255,255,0.06)",
|
|
319
|
+
$2: "0 3px 6px rgba(255,255,255,0.10), 0 2px 4px rgba(255,255,255,0.06)",
|
|
320
|
+
$4: "0 10px 20px rgba(255,255,255,0.12), 0 3px 6px rgba(255,255,255,0.08)",
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
function elevationTokenKey(token: string | number | undefined): "$1" | "$2" | "$4" | undefined {
|
|
324
|
+
if (token == null) return undefined;
|
|
325
|
+
return elevationKeyAlias[String(token)];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function elevationPaint(
|
|
329
|
+
token: string | number | undefined,
|
|
330
|
+
scheme: ColorScheme,
|
|
331
|
+
): ElevationPaintProps {
|
|
332
|
+
const key = elevationTokenKey(token);
|
|
333
|
+
if (!key) return {};
|
|
334
|
+
const metrics = elevationShadowMetrics[key];
|
|
335
|
+
const dark = scheme === "dark";
|
|
336
|
+
const paint: ElevationPaintProps = {
|
|
337
|
+
shadowColor: dark ? "$color12" : "#000",
|
|
338
|
+
shadowOffset: { width: 0, height: metrics.offsetY },
|
|
339
|
+
shadowOpacity: dark
|
|
340
|
+
? key === "$4"
|
|
341
|
+
? 0.12
|
|
342
|
+
: key === "$2"
|
|
343
|
+
? 0.1
|
|
344
|
+
: 0.08
|
|
345
|
+
: key === "$1"
|
|
346
|
+
? 0.12
|
|
347
|
+
: 0.15,
|
|
348
|
+
shadowRadius: metrics.radius,
|
|
349
|
+
};
|
|
350
|
+
if (isWeb) {
|
|
351
|
+
paint.style = { boxShadow: dark ? webShadowDark[key] : webShadowLight[key] };
|
|
352
|
+
}
|
|
353
|
+
return paint;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function elevationChromeFor(
|
|
357
|
+
token: string | number | undefined,
|
|
358
|
+
scheme: ColorScheme,
|
|
359
|
+
): ElevationChromeProps {
|
|
360
|
+
const key = elevationTokenKey(token);
|
|
361
|
+
if (!key) return {};
|
|
362
|
+
return {
|
|
363
|
+
...elevationPaint(token, scheme),
|
|
364
|
+
elevation: elevationShadowMetrics[key].android,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
250
368
|
// Legibility floor: body/label text must never land below ~4.5:1 against its
|
|
251
369
|
// surface. $color11 is the lowest ramp step that clears AA on color1/2
|
|
252
370
|
// surfaces, so "low" clamps there ($color9 measured 1.29-3.43:1 — SB-R-01).
|
|
@@ -317,7 +435,10 @@ function resolveDisabled(disabledStyle: Knobs["disabledStyle"]): DisabledRecipe
|
|
|
317
435
|
|
|
318
436
|
// ── State style resolver ──────────────────────────────────────
|
|
319
437
|
|
|
320
|
-
function resolveStateKnobs(
|
|
438
|
+
function resolveStateKnobs(
|
|
439
|
+
state: StateKnobs,
|
|
440
|
+
scheme: ColorScheme,
|
|
441
|
+
): {
|
|
321
442
|
control: ControlStateProps;
|
|
322
443
|
text: TextStateProps;
|
|
323
444
|
elevation: ElevationStateProps;
|
|
@@ -339,7 +460,12 @@ function resolveStateKnobs(state: StateKnobs): {
|
|
|
339
460
|
control.padding = gapMap[state.space];
|
|
340
461
|
}
|
|
341
462
|
if (state.elevation !== undefined) {
|
|
342
|
-
|
|
463
|
+
const token = elevationMap[state.elevation];
|
|
464
|
+
elevation.elevation = token;
|
|
465
|
+
Object.assign(elevation, elevationPaint(token, scheme));
|
|
466
|
+
if (scheme === "dark" && state.elevation !== "none") {
|
|
467
|
+
elevation.backgroundColor = elevationTintOverlay[state.elevation];
|
|
468
|
+
}
|
|
343
469
|
}
|
|
344
470
|
if (state.textAccent !== undefined) {
|
|
345
471
|
text.color = textAccentColorMap[state.textAccent];
|
|
@@ -358,15 +484,19 @@ function resolveStateKnobs(state: StateKnobs): {
|
|
|
358
484
|
|
|
359
485
|
// ── Resolver ──────────────────────────────────────────────────
|
|
360
486
|
|
|
361
|
-
export function resolveKnobs(
|
|
487
|
+
export function resolveKnobs(
|
|
488
|
+
knobs: Knobs,
|
|
489
|
+
override?: KnobPropsOverride,
|
|
490
|
+
scheme: ColorScheme = "light",
|
|
491
|
+
): ResolvedKnobs {
|
|
362
492
|
const radiusToken = borderRadiusMap[knobs.borderRadius];
|
|
363
|
-
const
|
|
364
|
-
const borderWidthValue = borderWidthMap[knobs.borderWidth];
|
|
493
|
+
const nestedRadiusToken = nestedRadiusMap[knobs.borderRadius];
|
|
365
494
|
const sizeToken = sizeTokenMap[knobs.size];
|
|
366
495
|
const nestedPx = nestedControlPxMap[knobs.size];
|
|
367
496
|
const nestedHitSlop = pressTargetHitSlop(nestedPx);
|
|
368
497
|
const sizeRecipe = sizeRecipeForToken(sizeToken, {
|
|
369
498
|
touch: isTouchable || isWebTouchable,
|
|
499
|
+
family: knobs.density === "compact" ? "controlCompact" : "control",
|
|
370
500
|
});
|
|
371
501
|
const paddingToken = panelPaddingMap[knobs.space];
|
|
372
502
|
const gapToken = gapMap[knobs.space];
|
|
@@ -393,18 +523,38 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
393
523
|
const containerSmoothing = knobs.borderRadius === "none" ? undefined : smoothing;
|
|
394
524
|
|
|
395
525
|
const outlined = knobs.fillStyle === "outlined";
|
|
526
|
+
// LC-86 OUTLINED-HAIRLINE: outlined implies a minimum border. none on an
|
|
527
|
+
// outlined control still draws the enumerated `small` stop (0.5). Chromeless
|
|
528
|
+
// is filled + none. Components must not add a second local floor.
|
|
529
|
+
const borderWidthValue =
|
|
530
|
+
outlined && knobs.borderWidth === "none"
|
|
531
|
+
? borderWidthMap.small
|
|
532
|
+
: borderWidthMap[knobs.borderWidth];
|
|
396
533
|
const elevationToken = elevationMap[knobs.elevation];
|
|
534
|
+
const overlayElevationToken = overlayElevationMap[knobs.elevation];
|
|
535
|
+
const overlayPaintProps = elevationPaint(overlayElevationToken, scheme);
|
|
536
|
+
const elevationChrome = elevationChromeFor(elevationToken, scheme);
|
|
537
|
+
const dark = scheme === "dark";
|
|
538
|
+
// MPO-19 X5: `$background` (not `$color1`) so surface1/2/3 sub-themes move
|
|
539
|
+
// the fill. Dark + elevation still steps the filled rest fill up the ramp
|
|
540
|
+
// (LC-85).
|
|
541
|
+
const overlayFill =
|
|
542
|
+
dark && knobs.elevation !== "none" ? elevationTintOverlay[knobs.elevation] : "$background";
|
|
397
543
|
const textAccentColor = textAccentColorMap[knobs.textAccent] ?? "$color";
|
|
398
|
-
// One input surface (DF-19 / Axiom 5 ONE SOURCE):
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
const inputBackground = outlined
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
544
|
+
// One input surface (DF-19 / Axiom 5 ONE SOURCE): Timeline's composer,
|
|
545
|
+
// RichEditor and form fields agree on what an input surface is.
|
|
546
|
+
// MPO-19 X6: `$background` not `$color2` so a surface sub-theme can move it.
|
|
547
|
+
// Dark + elevation steps the filled rest fill up the existing ramp (LC-85).
|
|
548
|
+
const inputBackground = outlined
|
|
549
|
+
? "transparent"
|
|
550
|
+
: dark
|
|
551
|
+
? elevationTintInput[knobs.elevation]
|
|
552
|
+
: "$background";
|
|
553
|
+
|
|
554
|
+
const hover = resolveStateKnobs(knobs.hover, scheme);
|
|
555
|
+
const press = resolveStateKnobs(knobs.press, scheme);
|
|
556
|
+
const focus = resolveStateKnobs(knobs.focus, scheme);
|
|
557
|
+
const focusVisible = resolveStateKnobs(knobs.focusVisible, scheme);
|
|
408
558
|
|
|
409
559
|
const nonEmpty = <T extends object>(obj: T): T | undefined =>
|
|
410
560
|
Object.keys(obj).length > 0 ? obj : undefined;
|
|
@@ -430,6 +580,8 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
430
580
|
focusVisibleKnobProps: nonEmpty(focusVisible.elevation),
|
|
431
581
|
};
|
|
432
582
|
|
|
583
|
+
const nestedRadius = { borderRadius: nestedRadiusToken, ...containerSmoothing };
|
|
584
|
+
|
|
433
585
|
const knobProps: KnobProps = {
|
|
434
586
|
borderRadius: {
|
|
435
587
|
borderRadius: radiusToken,
|
|
@@ -437,33 +589,46 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
437
589
|
borderWidth: borderWidthValue,
|
|
438
590
|
...scaleSmoothing,
|
|
439
591
|
},
|
|
440
|
-
|
|
592
|
+
borderRadiusNested: nestedRadius,
|
|
593
|
+
// MPO-19 X12: the old name for the same fragment, kept for one release so
|
|
594
|
+
// an out-of-tree consumer does not break on the rename. Same object, so
|
|
595
|
+
// the two cannot drift while both exist.
|
|
596
|
+
borderRadiusOuter: nestedRadius,
|
|
597
|
+
controlBorderFilled: { borderColor: "transparent", borderWidth: borderWidthValue },
|
|
598
|
+
controlBorderOutlined: {
|
|
599
|
+
borderColor: "$borderColor",
|
|
600
|
+
borderWidth: Math.max(borderWidthValue, 1),
|
|
601
|
+
},
|
|
441
602
|
// Axiom 1 child-clip cap: padded containers (Card/Panel/DialogContent)
|
|
442
603
|
// spread this after their radius-bearing fragment. Non-container parts
|
|
443
604
|
// keep the uncapped `borderRadius` fragment.
|
|
444
605
|
containerRadius: { borderRadius: containerRadiusValue, ...containerSmoothing },
|
|
445
606
|
elevation: elevationToken,
|
|
607
|
+
overlayElevation: overlayElevationToken,
|
|
608
|
+
elevationChrome,
|
|
609
|
+
scheme,
|
|
446
610
|
surface: {
|
|
447
|
-
backgroundColor: "$
|
|
611
|
+
backgroundColor: "$background",
|
|
448
612
|
borderColor: "$borderColor",
|
|
449
613
|
borderWidth: borderWidthValue,
|
|
450
614
|
},
|
|
451
615
|
cardSurface: {
|
|
452
|
-
backgroundColor:
|
|
616
|
+
backgroundColor: overlayFill,
|
|
453
617
|
borderRadius: containerRadiusValue,
|
|
454
618
|
padding: paddingToken,
|
|
455
619
|
...containerSmoothing,
|
|
456
620
|
},
|
|
457
621
|
elevatedSurface: {
|
|
458
|
-
backgroundColor:
|
|
622
|
+
backgroundColor: overlayFill,
|
|
459
623
|
borderColor: "$borderColor",
|
|
460
624
|
borderWidth: borderWidthValue,
|
|
461
625
|
borderRadius: radiusToken,
|
|
462
|
-
elevation:
|
|
626
|
+
elevation: overlayElevationToken,
|
|
627
|
+
...overlayPaintProps,
|
|
463
628
|
...scaleSmoothing,
|
|
464
629
|
},
|
|
465
630
|
featureSurface: {
|
|
466
|
-
backgroundColor: "$
|
|
631
|
+
backgroundColor: "$background",
|
|
467
632
|
borderRadius: "$10",
|
|
468
633
|
padding: "$6",
|
|
469
634
|
// Bento marketing/auth card shadow: ~0px 9px 12px rgba(0,0,0,0.02) —
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPO-46 / MPO-19 X1/X2 — the size ladder is the measured Tamagui v5 table
|
|
3
|
+
* (R6: real TABLE, no escape). padX / label / icon / radius at $2/$3/$4/$6
|
|
4
|
+
* come from tamagui-rendered-reference.md §6.1; Input padX is §6.2
|
|
5
|
+
* (one space half-step below the button). gap == radius at every size
|
|
6
|
+
* and every borderRadius knob stop.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
SIZE_RECIPE_RADII,
|
|
12
|
+
SIZE_RECIPE_RADIUS_STOPS,
|
|
13
|
+
TAMAGUI_CONTROL_SIZE_TABLE,
|
|
14
|
+
TAMAGUI_INPUT_PADX,
|
|
15
|
+
applyRadiusStop,
|
|
16
|
+
generateSizeRecipes,
|
|
17
|
+
recipeFamilies,
|
|
18
|
+
sizeRecipeForToken,
|
|
19
|
+
sizeRecipes,
|
|
20
|
+
type SizeRecipeRadiusStop,
|
|
21
|
+
} from "./sizeRecipes";
|
|
22
|
+
|
|
23
|
+
const MEASURED = {
|
|
24
|
+
$2: { paddingHorizontal: 7, fontSize: 12, iconSize: 12, radius: 5 },
|
|
25
|
+
$3: { paddingHorizontal: 13, fontSize: 13, iconSize: 14, radius: 7 },
|
|
26
|
+
$4: { paddingHorizontal: 18, fontSize: 14, iconSize: 16, radius: 9 },
|
|
27
|
+
$6: { paddingHorizontal: 32, fontSize: 18, iconSize: 20, radius: 16 },
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
describe("MPO-46 size ladder — measured table, not a ratio (R6)", () => {
|
|
31
|
+
it("padX, label, icon and radius at $2/$3/$4/$6 match the Tamagui v5 reference", () => {
|
|
32
|
+
for (const token of ["$2", "$3", "$4", "$6"] as const) {
|
|
33
|
+
const recipe = sizeRecipeForToken(token);
|
|
34
|
+
const row = MEASURED[token];
|
|
35
|
+
expect(recipe.paddingHorizontal, `${token} padX`).toBe(row.paddingHorizontal);
|
|
36
|
+
expect(recipe.fontSize, `${token} label`).toBe(row.fontSize);
|
|
37
|
+
expect(recipe.iconSize, `${token} icon`).toBe(row.iconSize);
|
|
38
|
+
expect(SIZE_RECIPE_RADII[token], `${token} radius`).toBe(row.radius);
|
|
39
|
+
expect(TAMAGUI_CONTROL_SIZE_TABLE[token]).toEqual(row);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not ship the linear-ratio $2 defects (padX 11, label 9, flat radius 9)", () => {
|
|
44
|
+
const two = sizeRecipeForToken("$2");
|
|
45
|
+
expect(two.paddingHorizontal).not.toBe(11);
|
|
46
|
+
expect(two.fontSize).not.toBe(9);
|
|
47
|
+
expect(SIZE_RECIPE_RADII.$2).not.toBe(9);
|
|
48
|
+
expect(SIZE_RECIPE_RADII.$2).not.toBe(SIZE_RECIPE_RADII.$6);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Button / Select trigger / ListItem $4 is 44 / padX 18 / radius 9; Input padX is 16", () => {
|
|
52
|
+
const button = sizeRecipes.$4;
|
|
53
|
+
expect(button.height).toBe(44);
|
|
54
|
+
expect(button.paddingHorizontal).toBe(18);
|
|
55
|
+
expect(SIZE_RECIPE_RADII.$4).toBe(9);
|
|
56
|
+
expect(TAMAGUI_INPUT_PADX.$4).toBe(16);
|
|
57
|
+
expect(recipeFamilies.controlCompact.recipes.$4.paddingHorizontal).toBe(16);
|
|
58
|
+
expect(recipeFamilies.controlCompact.recipes.$4.height).toBe(44);
|
|
59
|
+
expect(recipeFamilies.controlCompact.radii.$4).toBe(9);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("MPO-46 gap == radius", () => {
|
|
64
|
+
it("holds at every size step", () => {
|
|
65
|
+
const generated = generateSizeRecipes();
|
|
66
|
+
for (const token of Object.keys(generated.recipes)) {
|
|
67
|
+
expect(generated.recipes[token].gap, `control ${token}`).toBe(generated.radii[token]);
|
|
68
|
+
expect(generated.families.controlCompact.recipes[token].gap, `compact ${token}`).toBe(
|
|
69
|
+
generated.families.controlCompact.radii[token],
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("holds at every borderRadius knob stop", () => {
|
|
75
|
+
const stops = Object.keys(SIZE_RECIPE_RADIUS_STOPS) as SizeRecipeRadiusStop[];
|
|
76
|
+
for (const token of ["$2", "$3", "$4", "$5", "$6"] as const) {
|
|
77
|
+
const recipe = sizeRecipeForToken(token);
|
|
78
|
+
for (const stop of stops) {
|
|
79
|
+
const next = applyRadiusStop(recipe, stop);
|
|
80
|
+
expect(next.gap, `${token} @ ${stop}`).toBe(SIZE_RECIPE_RADIUS_STOPS[stop]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|