@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.
Files changed (106) hide show
  1. package/README.md +2 -1
  2. package/package.json +5 -5
  3. package/src/figma/figmaTokens.spec.ts +25 -21
  4. package/src/figma/figmaTokens.ts +2 -2
  5. package/src/font.ts +6 -19
  6. package/src/keyboardFocusRing.ts +12 -6
  7. package/src/theme/FontKnobStyles.tsx +5 -3
  8. package/src/theme/Tint.tsx +3 -2
  9. package/src/theme/animations/css.ts +14 -1
  10. package/src/theme/colorRules.spec.ts +233 -2
  11. package/src/theme/colorRules.ts +118 -0
  12. package/src/theme/cornerSmoothing.native.ts +39 -2
  13. package/src/theme/cornerSmoothing.spec.ts +213 -3
  14. package/src/theme/cornerSmoothing.ts +132 -10
  15. package/src/theme/createDefaultThemeConfig.spec.ts +107 -1
  16. package/src/theme/createDefaultThemeConfig.ts +27 -8
  17. package/src/theme/defaults/builderOptions.ts +23 -10
  18. package/src/theme/defaults/categoryFonts.ts +21 -0
  19. package/src/theme/defaults/fonts.spec.ts +93 -10
  20. package/src/theme/defaults/fonts.theme.spec.tsx +128 -0
  21. package/src/theme/defaults/fonts.ts +155 -27
  22. package/src/theme/defaults/index.ts +1 -0
  23. package/src/theme/devtools/ThemeDevtoolsPanel.tsx +0 -11
  24. package/src/theme/focusState.spec.ts +110 -1
  25. package/src/theme/focusState.ts +32 -7
  26. package/src/theme/fontCategoryStacks.ts +24 -0
  27. package/src/theme/fontKnobStyles.spec.ts +15 -2
  28. package/src/theme/glyphPaint.spec.ts +26 -0
  29. package/src/theme/glyphPaint.ts +46 -0
  30. package/src/theme/index.ts +3 -0
  31. package/src/theme/knobs.ts +18 -2
  32. package/src/theme/layoutTokens.spec.ts +3 -1
  33. package/src/theme/layoutTokens.ts +47 -4
  34. package/src/theme/mpo13Proof.spec.ts +3 -3
  35. package/src/theme/mpo19Proof.spec.ts +285 -0
  36. package/src/theme/presets.ts +1 -1
  37. package/src/theme/radiusClass.spec.ts +394 -0
  38. package/src/theme/radiusClass.ts +203 -4
  39. package/src/theme/radiusClassTable.generated.ts +121 -0
  40. package/src/theme/recipeInputs.ts +223 -36
  41. package/src/theme/recipes.ts +68 -4
  42. package/src/theme/resolveKnobs.spec.ts +139 -11
  43. package/src/theme/resolveKnobs.ts +190 -25
  44. package/src/theme/sizeLadder.spec.ts +84 -0
  45. package/src/theme/sizeRecipes.spec.ts +19 -6
  46. package/src/theme/sizeRecipes.ts +43 -13
  47. package/src/theme/theme.native.tsx +2 -0
  48. package/src/theme/theme.provider.spec.ts +9 -0
  49. package/src/theme/theme.tsx +6 -3
  50. package/src/theme/tint.spec.tsx +36 -35
  51. package/src/theme/useResolvedKnobs.ts +6 -3
  52. package/types/figma/figmaTokens.d.ts +1 -1
  53. package/types/figma/figmaTokens.d.ts.map +1 -1
  54. package/types/font.d.ts.map +1 -1
  55. package/types/keyboardFocusRing.d.ts +11 -5
  56. package/types/keyboardFocusRing.d.ts.map +1 -1
  57. package/types/theme/FontKnobStyles.d.ts +2 -1
  58. package/types/theme/FontKnobStyles.d.ts.map +1 -1
  59. package/types/theme/Tint.d.ts +2 -1
  60. package/types/theme/Tint.d.ts.map +1 -1
  61. package/types/theme/animations/css.d.ts +15 -2
  62. package/types/theme/animations/css.d.ts.map +1 -1
  63. package/types/theme/colorRules.d.ts +59 -1
  64. package/types/theme/colorRules.d.ts.map +1 -1
  65. package/types/theme/cornerSmoothing.d.ts +42 -20
  66. package/types/theme/cornerSmoothing.d.ts.map +1 -1
  67. package/types/theme/cornerSmoothing.native.d.ts +22 -2
  68. package/types/theme/cornerSmoothing.native.d.ts.map +1 -1
  69. package/types/theme/createDefaultThemeConfig.d.ts +21 -3
  70. package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
  71. package/types/theme/defaults/builderOptions.d.ts.map +1 -1
  72. package/types/theme/defaults/categoryFonts.d.ts +19 -0
  73. package/types/theme/defaults/categoryFonts.d.ts.map +1 -0
  74. package/types/theme/defaults/fonts.d.ts +135 -2
  75. package/types/theme/defaults/fonts.d.ts.map +1 -1
  76. package/types/theme/defaults/index.d.ts +1 -0
  77. package/types/theme/defaults/index.d.ts.map +1 -1
  78. package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
  79. package/types/theme/focusState.d.ts +32 -6
  80. package/types/theme/focusState.d.ts.map +1 -1
  81. package/types/theme/fontCategoryStacks.d.ts +11 -0
  82. package/types/theme/fontCategoryStacks.d.ts.map +1 -0
  83. package/types/theme/glyphPaint.d.ts +16 -0
  84. package/types/theme/glyphPaint.d.ts.map +1 -0
  85. package/types/theme/index.d.ts +3 -0
  86. package/types/theme/index.d.ts.map +1 -1
  87. package/types/theme/knobs.d.ts +10 -2
  88. package/types/theme/knobs.d.ts.map +1 -1
  89. package/types/theme/layoutTokens.d.ts +46 -4
  90. package/types/theme/layoutTokens.d.ts.map +1 -1
  91. package/types/theme/radiusClass.d.ts +96 -4
  92. package/types/theme/radiusClass.d.ts.map +1 -1
  93. package/types/theme/radiusClassTable.generated.d.ts +146 -0
  94. package/types/theme/radiusClassTable.generated.d.ts.map +1 -0
  95. package/types/theme/recipeInputs.d.ts +85 -14
  96. package/types/theme/recipeInputs.d.ts.map +1 -1
  97. package/types/theme/recipes.d.ts +78 -4
  98. package/types/theme/recipes.d.ts.map +1 -1
  99. package/types/theme/resolveKnobs.d.ts +12 -1
  100. package/types/theme/resolveKnobs.d.ts.map +1 -1
  101. package/types/theme/sizeRecipes.d.ts +27 -14
  102. package/types/theme/sizeRecipes.d.ts.map +1 -1
  103. package/types/theme/theme.d.ts.map +1 -1
  104. package/types/theme/theme.native.d.ts.map +1 -1
  105. package/types/theme/useResolvedKnobs.d.ts +2 -2
  106. package/types/theme/useResolvedKnobs.d.ts.map +1 -1
@@ -40,20 +40,46 @@ import { useResolvedKnobs } from "./useResolvedKnobs";
40
40
 
41
41
  export type LayoutSizeClass = "compact" | "medium" | "expanded" | "large" | "xl";
42
42
 
43
- /** Named min-widths. Prefer these over literals in layout code. */
43
+ /**
44
+ * Named min-widths. Prefer these over literals in layout code.
45
+ *
46
+ * MPO-19 X11: the four names below used to be the whole table, so a
47
+ * responsive design authored in Tamagui idiom named three breakpoints mpo
48
+ * layout code could not — `xs` 460, `md` 768 and `xxl` 1536 simply had no
49
+ * spelling here. The 860 pane threshold has an argument behind it and stays;
50
+ * the MISSING names had no argument at all, which is why they are added
51
+ * unconditionally and the pivot question is not settled here.
52
+ *
53
+ * The Tamagui-named entries carry Tamagui's numbers, exactly. A shared name
54
+ * meaning a different width is the failure this table is supposed to prevent.
55
+ */
44
56
  export const layoutBreakpoints = {
57
+ /** Tamagui `$xxs`. Smallest phones. */
58
+ xxs: 340,
59
+ /** Tamagui `$xs`. */
60
+ xs: 460,
45
61
  /**
46
62
  * End of compact / DeskShell narrow drawer threshold (Tamagui `$sm`).
47
63
  * Also the overlay sheet↔float pivot and the DataTable card↔grid pivot —
48
64
  * one idiom flip per viewport (MPO-13 C5).
49
65
  */
50
66
  medium: 640,
67
+ /**
68
+ * Tamagui `$md` — the width `<Adapt when="maxMd">` pivots on. Whether the
69
+ * overlay pivot should MOVE here from `medium`, and `expanded` be deleted,
70
+ * is the open half of X11; it decides together with GL-47 / MPO-21, so it
71
+ * is not decided here. Naming it costs nothing and is what lets that
72
+ * question be asked in code.
73
+ */
74
+ md: 768,
51
75
  /** Two-pane budget threshold (~M3 expanded 840). Not an overlay pivot. */
52
76
  expanded: 860,
53
77
  /** Tamagui `$lg`. */
54
78
  large: 1024,
55
79
  /** Tamagui `$xl`. */
56
80
  xl: 1280,
81
+ /** Tamagui `$xxl`. */
82
+ xxl: 1536,
57
83
  } as const;
58
84
 
59
85
  export type LayoutBreakpointName = keyof typeof layoutBreakpoints;
@@ -225,9 +251,9 @@ export function pressTargetHitSlop(
225
251
  * stay start-aligned (flush with the trigger's leading edge).
226
252
  * - Pointer-positioned CONTEXT MENUS and free POPOVERS/TOOLTIPS size to
227
253
  * content, start-aligned to their anchor.
228
- * - All of them keep `OVERLAY_ANCHOR_GAP` px between trigger edge and
229
- * overlay (floating-ui `offset` / tamagui Popover `offset`), and
230
- * shift/flip at viewport edges instead of overflowing.
254
+ * - FREE overlays (tooltips, pointer-positioned context menus, popovers with
255
+ * no owning control) keep `OVERLAY_ANCHOR_GAP` px between anchor edge and
256
+ * overlay, and shift/flip at viewport edges instead of overflowing.
231
257
  * - ONE SURFACE OWNER per overlay: the overlay frame draws the single
232
258
  * border + radius. When a panel intentionally covers its trigger
233
259
  * (Select's macOS-style aligned listbox), the covered trigger must not
@@ -236,6 +262,23 @@ export function pressTargetHitSlop(
236
262
  */
237
263
  export const OVERLAY_ANCHOR_GAP = 4;
238
264
 
265
+ /**
266
+ * Clay's FloatingPanel ruling, 2026-08-28 (MPO-38). COVER is the defining
267
+ * behaviour of the FloatingPanel family: a panel attaches to the trigger it
268
+ * was opened from and does not float clear of it. Floating 4px below is the
269
+ * EXCEPTION, not the rule, and no member takes it unless Clay rules it.
270
+ *
271
+ * So the family anchors at zero: the panel's top edge coincides with the
272
+ * trigger's bottom edge (or, for the covering members — Select and
273
+ * single-value Combobox — with the trigger's own top edge). `OVERLAY_ANCHOR_GAP`
274
+ * stays 4 for FREE overlays, which have no trigger to attach to.
275
+ *
276
+ * Measured before this landed (playwright, 1440x900 DPR 2, real components):
277
+ * DropdownMenu 3.96, DatePicker 4.00, TimePicker 4.00, ColorPicker 4.00 px of
278
+ * daylight between trigger bottom and panel top.
279
+ */
280
+ export const OVERLAY_ATTACH_GAP = 0;
281
+
239
282
  // ── Semantic gaps (DG-LAY-03) ─────────────────────────────────────────────
240
283
 
241
284
  /**
@@ -67,7 +67,7 @@ describe("MPO-13 C3 — radius is $radius.true = 9", () => {
67
67
  expect(sizeRecipeForToken("$4").gap).toBe(DEFAULT_SIZE_RECIPE_INPUTS.radiusPx);
68
68
  });
69
69
 
70
- it("radius never rides the size axis — it stays on the borderRadius knob", () => {
70
+ it("radius steps with the size token (R6); the recipe object still holds no radius field", () => {
71
71
  for (const token of ["$2", "$3", "$4", "$5", "$6"] as const) {
72
72
  expect(sizeRecipeForToken(token)).not.toHaveProperty("radius");
73
73
  expect(sizeRecipeForToken(token)).not.toHaveProperty("borderRadius");
@@ -89,8 +89,8 @@ describe("MPO-13 — the ratios are relationships, not literals", () => {
89
89
  const compact = sizeRecipeForToken("$4", { family: "controlCompact" });
90
90
  expect(compact.height).toBe(control.height);
91
91
  expect(compact.paddingHorizontal).toBeLessThan(control.paddingHorizontal);
92
- // $space.3 = 13 vs $space.true = 18 — the measured Input↔Button offset.
93
- expect(compact.paddingHorizontal).toBe(13);
92
+ // $space.3.5 = 16 vs $space.true = 18 — Input is one space half-step below Button.
93
+ expect(compact.paddingHorizontal).toBe(16);
94
94
  });
95
95
 
96
96
  it("re-pointing baseHeight re-derives every channel — no hardcoded 44", () => {
@@ -0,0 +1,285 @@
1
+ /**
2
+ * MPO-19 PROOF — the layer-3 correction ledger, asserted.
3
+ *
4
+ * Same job as `mpo13Proof.spec.ts` and the same shape: pin the RELATIONSHIP
5
+ * that produces a value, not only the value, so a correction cannot quietly
6
+ * regress into the thing it corrected. Where MPO-13 pinned geometry against
7
+ * measured numbers, this pins mpo against STOCK TAMAGUI — the eleven rows of
8
+ * `02-mpo.md` §5 that MPO-13 left open, each a place mpo differed with no
9
+ * improvement argument written down (`00-laws.md` L7: corrections outrank
10
+ * additions, and you never design around a contradiction).
11
+ *
12
+ * The strongest assertions here are the ones that read upstream directly.
13
+ * X8/X9 do not compare the font tables to transcribed numbers, they compare
14
+ * them to `defaultConfig.fonts` — so the test fails if the tables drift from
15
+ * the era pin in EITHER direction, which a literal snapshot cannot do.
16
+ *
17
+ * Not covered here, deliberately: the `03-our-surfaces.md` DL-12 trio (scoped
18
+ * marketing text tint, state layers composing with elevation, a spring motion
19
+ * driver). MPO-19 holds those for a ruling from Clay, because MPO-13's
20
+ * REFUSED list and 00 L7 disagree about them and neither side should
21
+ * implement until that is settled.
22
+ */
23
+
24
+ import { defaultConfig } from "@tamagui/config/v5";
25
+ import { describe, expect, it } from "vitest";
26
+ import { animationConfig } from "./animations/css";
27
+ import { tamaguiEra } from "./createDefaultThemeConfig";
28
+ import { defaultBuilderOptions } from "./defaults/builderOptions";
29
+ import { defaultBodyFont, defaultHeadingFont } from "./defaults/fonts";
30
+ import {
31
+ FOCUS_RING_CLIPPED_OFFSET,
32
+ FOCUS_VISIBLE_RING,
33
+ ensureFocusVisibleRing,
34
+ } from "./focusState";
35
+ import { defaultKnobs } from "./knobs";
36
+ import { layoutBreakpoints, OVERLAY_BREAKPOINT } from "./layoutTokens";
37
+ import { resolveKnobs } from "./resolveKnobs";
38
+
39
+ /** A font table's numbers/strings, Variables unwrapped and `$` stripped. */
40
+ function table(raw: unknown): Record<string, unknown> {
41
+ const out: Record<string, unknown> = {};
42
+ for (const [key, value] of Object.entries((raw ?? {}) as Record<string, unknown>)) {
43
+ const v =
44
+ value && typeof value === "object" && "val" in value
45
+ ? (value as { val?: unknown }).val
46
+ : value;
47
+ if (v !== undefined) out[key.replace(/^\$/, "")] = v;
48
+ }
49
+ return out;
50
+ }
51
+
52
+ const knobProps = (over: Partial<typeof defaultKnobs> = {}) =>
53
+ resolveKnobs({ ...defaultKnobs, ...over }).knobProps;
54
+
55
+ describe("MPO-19 X13 — one Tamagui era, and it is load-bearing", () => {
56
+ it("the era is pinned, and pinned to v5", () => {
57
+ expect(tamaguiEra).toBe("v5");
58
+ });
59
+
60
+ it("the config's tokens come from the pinned era and from nothing else", async () => {
61
+ // The legacy `@tamagui/themes` tokens used to override `defaultConfig`'s.
62
+ // Measured identical for every group the config actually uses, which is
63
+ // why nothing broke and why nobody noticed for a year.
64
+ const legacy = await import("@tamagui/themes");
65
+ for (const group of ["size", "space", "radius", "zIndex"] as const) {
66
+ expect(table(defaultConfig.tokens[group]), `tokens.${group}`).toEqual(
67
+ table((legacy.tokens as Record<string, unknown>)[group]),
68
+ );
69
+ }
70
+ });
71
+ });
72
+
73
+ describe("MPO-19 X8/X9 — the type scale IS the era's, not a copy of it", () => {
74
+ it("body and heading tables are read from defaultConfig, not transcribed", () => {
75
+ // The relationship, not the literals: re-point the era and the whole
76
+ // scale follows. A transcribed table would pass a literal snapshot and
77
+ // fail this.
78
+ for (const [name, ours, theirs] of [
79
+ ["body", defaultBodyFont, defaultConfig.fonts.body],
80
+ ["heading", defaultHeadingFont, defaultConfig.fonts.heading],
81
+ ] as const) {
82
+ expect(table(ours.size), `${name}.size`).toEqual(table(theirs.size));
83
+ expect(table(ours.lineHeight), `${name}.lineHeight`).toEqual(table(theirs.lineHeight));
84
+ expect(table(ours.weight), `${name}.weight`).toEqual(table(theirs.weight));
85
+ }
86
+ });
87
+
88
+ it("body $true is v5's 15, not v4's 14", () => {
89
+ expect(table(defaultBodyFont.size).true).toBe(15);
90
+ });
91
+
92
+ it("body leading is the taper, so the ratio falls as the size grows", () => {
93
+ const size = table(defaultBodyFont.size) as Record<string, number>;
94
+ const lineHeight = table(defaultBodyFont.lineHeight) as Record<string, number>;
95
+ // `size + 10` was never a ratio: 1.91 at 11px and 1.22 at 46px. The taper
96
+ // holds ~1.5 at text sizes and eases toward ~1.42 at display sizes.
97
+ const ratioAt = (key: string) => lineHeight[key] / size[key];
98
+ expect(ratioAt("4")).toBeGreaterThan(1.45);
99
+ expect(ratioAt("4")).toBeLessThan(1.56);
100
+ expect(ratioAt("10")).toBeLessThan(ratioAt("4"));
101
+ expect(ratioAt("10")).toBeGreaterThan(1.38);
102
+ });
103
+
104
+ it("heading rides the body size scale — there is no 1.4x fork", () => {
105
+ expect(table(defaultHeadingFont.size)).toEqual(table(defaultBodyFont.size));
106
+ });
107
+
108
+ it("heading weight is the sparse 600/700/800 ramp, so bold still means something", () => {
109
+ const weight = table(defaultHeadingFont.weight) as Record<string, string>;
110
+ expect(weight["1"]).toBe("600");
111
+ expect(weight["6"]).toBe("700");
112
+ expect(weight["9"]).toBe("800");
113
+ // H1 is $10: compact and heavy, where mpo used to draw 64px at 400.
114
+ expect(table(defaultHeadingFont.size)["10"]).toBe(40);
115
+ expect(weight["10"]).toBe("800");
116
+ });
117
+
118
+ it("every declared weight has a face, so nothing is synthesised", () => {
119
+ const face = defaultHeadingFont.face as Record<string, unknown> | undefined;
120
+ for (const w of new Set(Object.values(table(defaultHeadingFont.weight)))) {
121
+ expect(face?.[String(w)], `face[${w}]`).toBeDefined();
122
+ }
123
+ });
124
+ });
125
+
126
+ describe("MPO-19 X3 — a filled control's border is transparent, not $borderColor", () => {
127
+ it("filled paints no visible border; outlined does", () => {
128
+ const props = knobProps();
129
+ expect(props.controlBorderFilled.borderColor).toBe("transparent");
130
+ expect(props.controlBorderOutlined.borderColor).toBe("$borderColor");
131
+ });
132
+
133
+ it("both keep the same width, so switching fill style never moves a layout", () => {
134
+ const props = knobProps();
135
+ expect(props.controlBorderFilled.borderWidth).toBe(props.controlBorderOutlined.borderWidth);
136
+ });
137
+
138
+ it("the width resolves through the borderWidth knob, not a literal", () => {
139
+ expect(knobProps({ borderWidth: "none" }).controlBorderFilled.borderWidth).toBe(0);
140
+ expect(knobProps({ borderWidth: "small" }).controlBorderFilled.borderWidth).toBe(0.5);
141
+ expect(knobProps({ borderWidth: "medium" }).controlBorderFilled.borderWidth).toBe(1);
142
+ expect(knobProps({ borderWidth: "large" }).controlBorderFilled.borderWidth).toBe(2);
143
+ // Outlined is the one that clamps: the outline IS the affordance, so it
144
+ // never disappears entirely.
145
+ expect(knobProps({ borderWidth: "none" }).controlBorderOutlined.borderWidth).toBe(1);
146
+ });
147
+ });
148
+
149
+ describe("MPO-19 X4 — the interaction ramp is the template's again", () => {
150
+ const derived = defaultBuilderOptions.getTheme?.({
151
+ name: "light",
152
+ theme: Object.fromEntries(
153
+ Array.from({ length: 12 }, (_, i) => [`color${i + 1}`, `#${String(i + 1).padStart(6, "0")}`]),
154
+ ),
155
+ } as never) as Record<string, string>;
156
+
157
+ it("none of the seven pinned ramp steps is emitted any more", () => {
158
+ for (const key of [
159
+ "backgroundPress",
160
+ "backgroundFocus",
161
+ "backgroundActive",
162
+ "borderColor",
163
+ "borderColorHover",
164
+ "borderColorFocus",
165
+ "borderColorPress",
166
+ ]) {
167
+ expect(derived, `getTheme still pins ${key}`).not.toHaveProperty(key);
168
+ }
169
+ });
170
+
171
+ it("the additive keys stay — they name slots Tamagui has no token for", () => {
172
+ // M5's focus ring is an improvement (stock is rgba(ink, 0.2), measured
173
+ // 1.36:1), and the semantic aliases contradict nothing.
174
+ expect(derived).toHaveProperty("outlineColor");
175
+ expect(derived).toHaveProperty("placeholderColor");
176
+ for (const alias of ["textMuted", "textSubtle", "borderStrong", "surfaceStrong"]) {
177
+ expect(derived, `alias ${alias}`).toHaveProperty(alias);
178
+ }
179
+ });
180
+ });
181
+
182
+ describe("MPO-19 X5/X6 — surfaces address semantic tokens, so sub-themes work", () => {
183
+ it("every surface fragment paints $background, never a raw ramp step", () => {
184
+ const props = knobProps();
185
+ for (const key of ["surface", "cardSurface", "elevatedSurface", "featureSurface"] as const) {
186
+ expect(
187
+ (props[key] as { backgroundColor: string }).backgroundColor,
188
+ `${key}.backgroundColor`,
189
+ ).toBe("$background");
190
+ }
191
+ });
192
+
193
+ 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
+ const painted = Object.values(knobProps())
198
+ .filter((v): v is Record<string, unknown> => !!v && typeof v === "object")
199
+ .map((fragment) => fragment.backgroundColor)
200
+ .filter((v): v is string => typeof v === "string");
201
+ expect(painted).not.toContain("$color1");
202
+ expect(painted).not.toContain("$color2");
203
+ });
204
+ });
205
+
206
+ describe("MPO-19 X7 — the focus ring is inset only where a clip demands it", () => {
207
+ it("the default sits on the border-box edge, as Tamagui paints it", () => {
208
+ expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(0);
209
+ expect(ensureFocusVisibleRing().outlineOffset).toBe(0);
210
+ });
211
+
212
+ it("the ring shape itself is unchanged — 2px solid, non-removable", () => {
213
+ const ring = ensureFocusVisibleRing({ outlineWidth: 0, outlineColor: "transparent" });
214
+ expect(ring.outlineWidth).toBe(2);
215
+ expect(ring.outlineStyle).toBe("solid");
216
+ expect(ring.outlineColor).toBe("$outlineColor");
217
+ });
218
+
219
+ it("a clipped call site names the inset instead of typing -2", () => {
220
+ expect(FOCUS_RING_CLIPPED_OFFSET).toBe(-2);
221
+ expect(ensureFocusVisibleRing({ outlineOffset: FOCUS_RING_CLIPPED_OFFSET }).outlineOffset).toBe(
222
+ -2,
223
+ );
224
+ });
225
+ });
226
+
227
+ describe("MPO-19 X10 — a shared animation name means the shared thing", () => {
228
+ it("medium is the stock ease and duration", () => {
229
+ expect(animationConfig.medium).toBe("ease-in 300ms");
230
+ });
231
+
232
+ it("every other shared name already matched, and still does", () => {
233
+ expect(animationConfig.bouncy).toBe("ease-in 200ms");
234
+ expect(animationConfig.lazy).toBe("ease-in 600ms");
235
+ expect(animationConfig.slow).toBe("ease-in 500ms");
236
+ expect(animationConfig.quick).toBe("ease-in 100ms");
237
+ });
238
+ });
239
+
240
+ describe("MPO-19 X11 — the breakpoint vocabulary covers Tamagui's", () => {
241
+ it("names Tamagui uses carry Tamagui's numbers", () => {
242
+ expect(layoutBreakpoints.xxs).toBe(340);
243
+ expect(layoutBreakpoints.xs).toBe(460);
244
+ expect(layoutBreakpoints.medium).toBe(640);
245
+ expect(layoutBreakpoints.md).toBe(768);
246
+ expect(layoutBreakpoints.large).toBe(1024);
247
+ expect(layoutBreakpoints.xl).toBe(1280);
248
+ expect(layoutBreakpoints.xxl).toBe(1536);
249
+ });
250
+
251
+ it("expanded 860 survives as the additive pane class, still not a pivot", () => {
252
+ // The 860 threshold has an argument behind it (M3 expanded, two panes).
253
+ // Whether the overlay pivot should MOVE to md 768 is the open half of
254
+ // X11 and decides with GL-47 / MPO-21, so this pins the status quo
255
+ // rather than pre-empting it.
256
+ expect(layoutBreakpoints.expanded).toBe(860);
257
+ expect(OVERLAY_BREAKPOINT).toBe(layoutBreakpoints.medium);
258
+ expect(OVERLAY_BREAKPOINT).not.toBe(layoutBreakpoints.expanded);
259
+ });
260
+ });
261
+
262
+ describe("MPO-19 X12 — the nested radius is named for what it resolves to", () => {
263
+ it("it is never LARGER than the frame it sits inside", () => {
264
+ const radiusPx: Record<string, number> = {
265
+ $0: 0,
266
+ $2: 5,
267
+ $3: 7,
268
+ $4: 9,
269
+ $5: 10,
270
+ $6: 16,
271
+ $12: 50,
272
+ };
273
+ for (const knob of ["none", "small", "medium", "large", "full"] as const) {
274
+ const props = knobProps({ borderRadius: knob });
275
+ const outer = radiusPx[props.borderRadius.borderRadius as string];
276
+ const nested = radiusPx[props.borderRadiusNested.borderRadius as string];
277
+ expect(nested, `borderRadius: ${knob}`).toBeLessThanOrEqual(outer);
278
+ }
279
+ });
280
+
281
+ it("the old name is the SAME object, so the two cannot drift while both exist", () => {
282
+ const props = knobProps();
283
+ expect(props.borderRadiusOuter).toBe(props.borderRadiusNested);
284
+ });
285
+ });
@@ -33,7 +33,7 @@ export const defaultPreset: Preset = {
33
33
  focusVisible: {},
34
34
  },
35
35
  intents: defaultIntents,
36
- tints: ["orange", "yellow", "green", "blue", "purple", "pink", "red"] as ThemeName[],
36
+ tints: ["orange", "blue", "purple", "pink"] as ThemeName[],
37
37
  };
38
38
 
39
39
  export const boldPreset: Preset = {