@multiplatform.one/theme 7.4.1 → 7.6.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 (119) hide show
  1. package/README.md +2 -1
  2. package/package.json +5 -5
  3. package/src/audit/index.ts +19 -0
  4. package/src/audit/themeMatrix.spec.ts +274 -0
  5. package/src/audit/themeMatrix.ts +630 -0
  6. package/src/figma/figmaTokens.spec.ts +25 -21
  7. package/src/figma/figmaTokens.ts +2 -2
  8. package/src/font.ts +37 -25
  9. package/src/index.ts +12 -0
  10. package/src/keyboardFocusRing.ts +12 -6
  11. package/src/theme/FontKnobStyles.tsx +14 -31
  12. package/src/theme/Tint.tsx +3 -2
  13. package/src/theme/animations/css.ts +14 -1
  14. package/src/theme/colorRules.spec.ts +306 -2
  15. package/src/theme/colorRules.ts +118 -0
  16. package/src/theme/cornerSmoothing.native.ts +39 -2
  17. package/src/theme/cornerSmoothing.spec.ts +270 -3
  18. package/src/theme/cornerSmoothing.ts +147 -10
  19. package/src/theme/createDefaultThemeConfig.spec.ts +107 -1
  20. package/src/theme/createDefaultThemeConfig.ts +27 -8
  21. package/src/theme/defaults/builderOptions.ts +23 -10
  22. package/src/theme/defaults/categoryFonts.ts +24 -0
  23. package/src/theme/defaults/fonts.spec.ts +163 -10
  24. package/src/theme/defaults/fonts.theme.spec.tsx +139 -0
  25. package/src/theme/defaults/fonts.ts +214 -50
  26. package/src/theme/defaults/index.ts +1 -0
  27. package/src/theme/devtools/ThemeDevtoolsPanel.tsx +0 -11
  28. package/src/theme/focusState.spec.ts +119 -1
  29. package/src/theme/focusState.ts +32 -7
  30. package/src/theme/fontCategoryMetrics.ts +57 -0
  31. package/src/theme/fontCategoryStacks.ts +37 -0
  32. package/src/theme/fontKnobStyles.spec.ts +15 -2
  33. package/src/theme/glyphPaint.spec.ts +26 -0
  34. package/src/theme/glyphPaint.ts +46 -0
  35. package/src/theme/index.ts +4 -0
  36. package/src/theme/knobs.ts +18 -2
  37. package/src/theme/layoutTokens.spec.ts +3 -1
  38. package/src/theme/layoutTokens.ts +47 -4
  39. package/src/theme/mpo13Proof.spec.ts +3 -3
  40. package/src/theme/mpo19Proof.spec.ts +285 -0
  41. package/src/theme/presets.ts +1 -1
  42. package/src/theme/radiusClass.spec.ts +394 -0
  43. package/src/theme/radiusClass.ts +203 -4
  44. package/src/theme/radiusClassTable.generated.ts +121 -0
  45. package/src/theme/recipeInputs.ts +223 -36
  46. package/src/theme/recipes.ts +68 -4
  47. package/src/theme/resolveKnobs.spec.ts +155 -13
  48. package/src/theme/resolveKnobs.ts +193 -27
  49. package/src/theme/sizeLadder.spec.ts +84 -0
  50. package/src/theme/sizeRecipes.spec.ts +19 -6
  51. package/src/theme/sizeRecipes.ts +43 -13
  52. package/src/theme/theme.native.tsx +2 -0
  53. package/src/theme/theme.provider.spec.ts +9 -0
  54. package/src/theme/theme.tsx +6 -3
  55. package/src/theme/tint.spec.tsx +36 -35
  56. package/src/theme/useResolvedKnobs.ts +6 -3
  57. package/types/audit/index.d.ts +2 -0
  58. package/types/audit/index.d.ts.map +1 -1
  59. package/types/audit/themeMatrix.d.ts +172 -0
  60. package/types/audit/themeMatrix.d.ts.map +1 -0
  61. package/types/figma/figmaTokens.d.ts +1 -1
  62. package/types/figma/figmaTokens.d.ts.map +1 -1
  63. package/types/font.d.ts.map +1 -1
  64. package/types/index.d.ts +2 -0
  65. package/types/index.d.ts.map +1 -1
  66. package/types/keyboardFocusRing.d.ts +11 -5
  67. package/types/keyboardFocusRing.d.ts.map +1 -1
  68. package/types/theme/FontKnobStyles.d.ts +8 -12
  69. package/types/theme/FontKnobStyles.d.ts.map +1 -1
  70. package/types/theme/Tint.d.ts +2 -1
  71. package/types/theme/Tint.d.ts.map +1 -1
  72. package/types/theme/animations/css.d.ts +15 -2
  73. package/types/theme/animations/css.d.ts.map +1 -1
  74. package/types/theme/colorRules.d.ts +59 -1
  75. package/types/theme/colorRules.d.ts.map +1 -1
  76. package/types/theme/cornerSmoothing.d.ts +42 -20
  77. package/types/theme/cornerSmoothing.d.ts.map +1 -1
  78. package/types/theme/cornerSmoothing.native.d.ts +22 -2
  79. package/types/theme/cornerSmoothing.native.d.ts.map +1 -1
  80. package/types/theme/createDefaultThemeConfig.d.ts +21 -3
  81. package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
  82. package/types/theme/defaults/builderOptions.d.ts.map +1 -1
  83. package/types/theme/defaults/categoryFonts.d.ts +23 -0
  84. package/types/theme/defaults/categoryFonts.d.ts.map +1 -0
  85. package/types/theme/defaults/fonts.d.ts +140 -2
  86. package/types/theme/defaults/fonts.d.ts.map +1 -1
  87. package/types/theme/defaults/index.d.ts +1 -0
  88. package/types/theme/defaults/index.d.ts.map +1 -1
  89. package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
  90. package/types/theme/focusState.d.ts +32 -6
  91. package/types/theme/focusState.d.ts.map +1 -1
  92. package/types/theme/fontCategoryMetrics.d.ts +36 -0
  93. package/types/theme/fontCategoryMetrics.d.ts.map +1 -0
  94. package/types/theme/fontCategoryStacks.d.ts +24 -0
  95. package/types/theme/fontCategoryStacks.d.ts.map +1 -0
  96. package/types/theme/glyphPaint.d.ts +16 -0
  97. package/types/theme/glyphPaint.d.ts.map +1 -0
  98. package/types/theme/index.d.ts +4 -0
  99. package/types/theme/index.d.ts.map +1 -1
  100. package/types/theme/knobs.d.ts +10 -2
  101. package/types/theme/knobs.d.ts.map +1 -1
  102. package/types/theme/layoutTokens.d.ts +46 -4
  103. package/types/theme/layoutTokens.d.ts.map +1 -1
  104. package/types/theme/radiusClass.d.ts +96 -4
  105. package/types/theme/radiusClass.d.ts.map +1 -1
  106. package/types/theme/radiusClassTable.generated.d.ts +146 -0
  107. package/types/theme/radiusClassTable.generated.d.ts.map +1 -0
  108. package/types/theme/recipeInputs.d.ts +85 -14
  109. package/types/theme/recipeInputs.d.ts.map +1 -1
  110. package/types/theme/recipes.d.ts +78 -4
  111. package/types/theme/recipes.d.ts.map +1 -1
  112. package/types/theme/resolveKnobs.d.ts +12 -1
  113. package/types/theme/resolveKnobs.d.ts.map +1 -1
  114. package/types/theme/sizeRecipes.d.ts +27 -14
  115. package/types/theme/sizeRecipes.d.ts.map +1 -1
  116. package/types/theme/theme.d.ts.map +1 -1
  117. package/types/theme/theme.native.d.ts.map +1 -1
  118. package/types/theme/useResolvedKnobs.d.ts +2 -2
  119. package/types/theme/useResolvedKnobs.d.ts.map +1 -1
@@ -1,5 +1,5 @@
1
1
  import * as Colors from "@tamagui/colors";
2
- import { tokens } from "@tamagui/themes";
2
+ import { defaultConfig } from "@tamagui/config/v5";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import { normalizeToHex } from "../theme/colorRules";
5
5
  import { defaultAccentTheme } from "../theme/defaults/accent";
@@ -89,13 +89,13 @@ describe("buildFigmaTokens", () => {
89
89
 
90
90
  describe("semantic", () => {
91
91
  it("derives the current interaction-ramp aliases (post-knobs rebuild)", () => {
92
- // These pin the drift the May 2026 spec missed: borderColor moved from
93
- // color7 to color5, textMuted from color10 to color11.
94
- expect(result.semantic.borderColor?.alias).toBe("color5");
95
- expect(result.semantic.borderColorHover?.alias).toBe("color6");
92
+ // MPO-19 X4: the seven pinned ramp steps were deleted from
93
+ // `defaultBuilderOptions.getTheme`, so these aliases are the theme
94
+ // BUILDER's derivation again rather than mpo's re-anchoring. That is
95
+ // the correction landing, and what this pins is that it landed —
96
+ // borderColor back off color5, the background states no longer forced.
97
+ expect(result.semantic.borderColor?.alias).toBe("color4");
96
98
  expect(result.semantic.textMuted?.alias).toBe("color11");
97
- expect(result.semantic.backgroundPress?.alias).toBe("color5");
98
- expect(result.semantic.backgroundActive?.alias).toBe("color4");
99
99
  expect(result.semantic.placeholderColor?.alias).toBe("color10");
100
100
  expect(result.semantic.surfaceMuted?.alias).toBe("color10");
101
101
  expect(result.semantic.borderStrong?.alias).toBe("color7");
@@ -135,13 +135,16 @@ describe("buildFigmaTokens", () => {
135
135
  });
136
136
 
137
137
  describe("sizing", () => {
138
- it("mirrors @tamagui/themes tokens with figma-safe names", () => {
139
- const sizeTrue = (tokens.size as Record<string, { val: number }>).true.val;
140
- expect(result.sizing["size-true"]).toBe(sizeTrue);
141
- const radius4 = (tokens.radius as Record<string, { val: number }>)["4"].val;
142
- expect(result.sizing["radius-4"]).toBe(radius4);
138
+ it("mirrors the v5 defaultConfig tokens with figma-safe names", () => {
139
+ // Read against the ERA the package pins (MPO-19 X13), not against a
140
+ // transcribed table: v5 keys carry the `$`, values are raw numbers.
141
+ const size = defaultConfig.tokens.size as Record<string, number>;
142
+ expect(result.sizing["size-true"]).toBe(size.$true);
143
+ expect(result.sizing["radius-4"]).toBe(
144
+ (defaultConfig.tokens.radius as Record<string, number>)["4"],
145
+ );
143
146
  expect(result.sizing["space-1-5"]).toBe(
144
- (tokens.space as Record<string, { val: number }>)["1.5"].val,
147
+ (defaultConfig.tokens.space as Record<string, number>)["$1.5"],
145
148
  );
146
149
  });
147
150
 
@@ -181,7 +184,7 @@ describe("buildFigmaTokens", () => {
181
184
  const { modes } = result.knobs.elevation;
182
185
  expect(modes.none).toEqual({ sizeToken: null, px: null });
183
186
  expect(modes.small.sizeToken).toBe("$1");
184
- expect(modes.small.px).toBe((tokens.size as Record<string, { val: number }>)["1"].val);
187
+ expect(modes.small.px).toBe((defaultConfig.tokens.size as Record<string, number>).$1);
185
188
  expect(modes.medium.sizeToken).toBe("$2");
186
189
  expect(modes.large.sizeToken).toBe("$4");
187
190
  });
@@ -216,17 +219,18 @@ describe("buildFigmaTokens", () => {
216
219
 
217
220
  describe("typography (closes deferred task 10.0)", () => {
218
221
  it("exports heading/body size + lineHeight tables from the built default fonts", () => {
219
- // Heading scale is 1.4x Inter base: H1 $10 = round(46 * 1.4) = 64.
220
- expect(result.typography.heading.size["10"]).toBe(64);
221
- expect(result.typography.body.size.true).toBe(14);
222
- // lineHeight = size + 10 (defaults/fonts.ts).
223
- expect(result.typography.body.lineHeight.true).toBe(24);
224
- expect(result.typography.heading.lineHeight["10"]).toBe(74);
222
+ // MPO-19 X8/X9: heading rides the body scale now, so H1 $10 is 40 at
223
+ // weight 800 rather than 64 at 400, and body $true is v5's 15 with the
224
+ // tapering leading instead of v4's 14 with `size + 10`.
225
+ expect(result.typography.heading.size["10"]).toBe(40);
226
+ expect(result.typography.body.size.true).toBe(15);
227
+ expect(result.typography.body.lineHeight.true).toBe(23);
228
+ expect(result.typography.heading.lineHeight["10"]).toBe(50);
225
229
  });
226
230
 
227
231
  it("exports Inter dynamic tracking per size", () => {
228
232
  const body = result.typography.body;
229
- expect(body.letterSpacing.true).toBe(interTrackingPx(14));
233
+ expect(body.letterSpacing.true).toBe(interTrackingPx(15));
230
234
  expect(Object.keys(body.letterSpacing).length).toBeGreaterThanOrEqual(16);
231
235
  });
232
236
 
@@ -93,7 +93,7 @@ export interface FigmaRadiusMode {
93
93
  /** `Sizing` collection variable to alias (e.g. `radius-4`). */
94
94
  figmaName: string;
95
95
  px: number;
96
- /** Outer/nested radius fragment (`knobProps.borderRadiusOuter`). */
96
+ /** Outer/nested radius fragment (`knobProps.borderRadiusNested`). */
97
97
  outerToken: string;
98
98
  outerFigmaName: string;
99
99
  outerPx: number;
@@ -344,7 +344,7 @@ function buildKnobs(): FigmaKnobsExport {
344
344
  for (const mode of RADIUS_KNOB_MODES) {
345
345
  const { knobProps } = resolveKnobs({ ...defaultKnobs, borderRadius: mode });
346
346
  const token = String(knobProps.borderRadius.borderRadius);
347
- const outerToken = String(knobProps.borderRadiusOuter.borderRadius);
347
+ const outerToken = String(knobProps.borderRadiusNested.borderRadius);
348
348
  borderRadius[mode] = {
349
349
  token,
350
350
  figmaName: tokenToFigma(token, "radius"),
package/src/font.ts CHANGED
@@ -1,33 +1,38 @@
1
1
  import type { GenericFont } from "@tamagui/web";
2
2
  import { createFont } from "@tamagui/web";
3
+ import {
4
+ bodyFontLineHeights,
5
+ bodyFontSizes,
6
+ bodyLineHeightPx,
7
+ categoryLineHeights,
8
+ } from "./theme/defaults/fonts";
3
9
 
4
10
  const defaults = {
5
- size: {
6
- 1: 11,
7
- 2: 12,
8
- 3: 13,
9
- 4: 14,
10
- 5: 15,
11
- 6: 16,
12
- 7: 18,
13
- 8: 21,
14
- 9: 28,
15
- 10: 42,
16
- 11: 52,
17
- 12: 62,
18
- 13: 72,
19
- 14: 92,
20
- 15: 114,
21
- 16: 124,
22
- } as const,
11
+ // The era's `$body` ladder, including `true`. A missing `true` key is what
12
+ // collapsed `$serif`/`$mono`/`$rounded` to -apple-system 14px/normal on the
13
+ // label text node (MPO-44); a DIFFERENT ladder is the follow-on defect —
14
+ // a family knob that also resizes the label is still moving type off the
15
+ // ramp. A consumer font registered through this helper lands exactly where
16
+ // the built-in category fonts do.
17
+ size: bodyFontSizes,
18
+ // Floored at the generic body ratio (LC-11). `createDefaultFont` does not
19
+ // know which category it is building, so it takes the sans-serif floor;
20
+ // `defaults/fonts.ts` applies the per-category ones.
21
+ lineHeight: categoryLineHeights("sans-serif"),
23
22
  // Neutral by default (LC-11): tracking is a per-family function of size,
24
23
  // so a shared constant table is always wrong for someone — the previous
25
24
  // Inter-ish table leaked +3px tracking into mono/script faces at $5/$6
26
- // (see p-results/fixes-polish2-codeblock-density.md). Families that want
27
- // optical tracking pass their own `letterSpacing` table.
28
- letterSpacing: {} as Record<string | number, number>,
25
+ // (see p-results/fixes-polish2-codeblock-density.md). Explicit zeros, not
26
+ // an empty table: an empty table emits no `--f-letterSpacing-*` and the
27
+ // value falls through to the `:root` Inter curve.
28
+ letterSpacing: Object.fromEntries(Object.keys(bodyFontSizes).map((k) => [k, 0])) as Record<
29
+ string | number,
30
+ number
31
+ >,
32
+ // The era's body weight. Was 300, which is off the reference's 400 label
33
+ // weight in the light direction and made a bodyFont flip change weight.
29
34
  weight: {
30
- 4: "300",
35
+ 4: "400",
31
36
  } as const,
32
37
  };
33
38
 
@@ -35,10 +40,17 @@ export function createDefaultFont<A extends GenericFont>(
35
40
  font: Partial<A> & { family: A["family"] },
36
41
  ): A {
37
42
  const size = font.size || defaults.size;
43
+ const lineHeight =
44
+ size === defaults.size
45
+ ? defaults.lineHeight
46
+ : (Object.fromEntries(
47
+ Object.entries(size).map(([k, v]) => [
48
+ k,
49
+ typeof v === "number" ? (bodyFontLineHeights[k] ?? bodyLineHeightPx(v)) : v,
50
+ ]),
51
+ ) as typeof size);
38
52
  return createFont({
39
- lineHeight: Object.fromEntries(
40
- Object.entries(size).map(([k, v]) => [k, typeof v === "number" ? v * 1.2 + 6 : v]),
41
- ) as typeof size,
53
+ lineHeight,
42
54
  letterSpacing: defaults.letterSpacing,
43
55
  weight: defaults.weight,
44
56
  ...font,
package/src/index.ts CHANGED
@@ -7,5 +7,17 @@ export * from "./menuRow";
7
7
  export * from "./font";
8
8
  export * from "./fonts/index";
9
9
 
10
+ // Contrast + matrix assertions downstream repos call (MPO-18). The full
11
+ // audit surface including the browser-side capturer stays on ./audit.
12
+ export { diffGeometry, evaluateNoBreakage, evaluateTextContrast } from "./audit";
13
+ export type {
14
+ GeometryDiff,
15
+ MatrixTextSample,
16
+ NoBreakageInput,
17
+ NoBreakageResult,
18
+ TextContrastResult,
19
+ TextContrastViolation,
20
+ } from "./audit";
21
+
10
22
  // Thin type re-exports so packages/apps never import @tamagui/web directly.
11
23
  export type { GenericFonts, MediaQueryKey, ThemeName } from "@tamagui/web";
@@ -36,15 +36,21 @@ export function wasKeyboardFocus(): boolean {
36
36
  return lastInputWasKeyboard;
37
37
  }
38
38
 
39
- import { FOCUS_VISIBLE_RING } from "./theme/focusState";
39
+ import { FOCUS_VISIBLE_RING_INSET } from "./theme/focusState";
40
40
 
41
41
  /**
42
- * Standard manual ring props — the SAME geometry as `FOCUS_VISIBLE_RING`
43
- * (2px solid `$outlineColor`, inset −2), spread here so manually-painted
44
- * rings and `focusVisibleStyle` rings can never drift apart.
42
+ * Standard manual ring props — the INSET ring geometry
43
+ * (`FOCUS_VISIBLE_RING_INSET`: 2px solid `$outlineColor`, offset −2), spread
44
+ * here so manually-painted rings can never drift from the theme contract.
45
+ * Manual painting exists for roving-focus / activedescendant composites
46
+ * (ToggleGroup segments inside a clipped Group, menu/listbox rows under a
47
+ * clipped overlay, table rows and cells) — items inside clipping ancestors,
48
+ * which is exactly the LC-71 §4 inset carve-out; an offset-0 band there
49
+ * would be severed by the clip. Standalone controls take the offset-0
50
+ * `FOCUS_VISIBLE_RING` via `focusVisibleStyle` / `ensureFocusVisibleRing()`.
45
51
  */
46
52
  export const keyboardFocusRingProps = {
47
- ...FOCUS_VISIBLE_RING,
53
+ ...FOCUS_VISIBLE_RING_INSET,
48
54
  } as const;
49
55
 
50
56
  /**
@@ -85,7 +91,7 @@ export const compositeFocusRingCss = `/* LC-71: ring on the composite outer box;
85
91
  }
86
92
  .mp-chip-dismiss:focus-visible .mp-chip-dismiss-ring {
87
93
  outline: 2px solid var(--outlineColor, var(--c-outlineColor, CanvasText));
88
- outline-offset: 2px;
94
+ outline-offset: 0;
89
95
  }`;
90
96
 
91
97
  const COMPOSITE_RING_STYLE_ID = "mp-composite-focus-ring";
@@ -27,6 +27,8 @@
27
27
  */
28
28
  import { getConfig } from "@tamagui/web";
29
29
  import { useEffect } from "react";
30
+ import { fontCategoryMetrics, flooredLineHeight } from "./fontCategoryMetrics";
31
+ import { fontCategoryStacks } from "./fontCategoryStacks";
30
32
  import type { FontCategory } from "./knobs";
31
33
  import { defaultKnobs } from "./knobs";
32
34
  import { usePresetContext } from "./PresetContext";
@@ -34,32 +36,12 @@ import { usePresetContext } from "./PresetContext";
34
36
  const STYLE_TAG_ID = "mp-font-knob-styles";
35
37
 
36
38
  /**
37
- * Per-category legibility floors, applied on top of the base (Inter-tuned)
38
- * lineHeight tables when the heading/body knob selects that category.
39
- * `heading`/`body` are minimum lineHeight:fontSize ratios; `letterSpacing:
40
- * "neutral"` zeroes Inter's tracking-by-size curve (wrong for fixed-pitch
41
- * and connected-script families). "sans-serif" is the default scope (never
42
- * overridden), listed for completeness.
39
+ * Re-exported so `@multiplatform.one/theme` consumers keep the historical
40
+ * import path. The table itself lives in `fontCategoryMetrics.ts` because
41
+ * `defaults/fonts.ts` bakes the same floors into the registered category
42
+ * fonts and the two must not drift (LC-11).
43
43
  */
44
- export const fontCategoryMetrics: Record<
45
- FontCategory,
46
- { heading: number; body: number; letterSpacing: "inherit" | "neutral" }
47
- > = {
48
- "sans-serif": { heading: 1.15, body: 1.4, letterSpacing: "inherit" },
49
- serif: { heading: 1.2, body: 1.4, letterSpacing: "inherit" },
50
- mono: { heading: 1.25, body: 1.45, letterSpacing: "neutral" },
51
- // Heading floors below are sized to each configured face's em box
52
- // (ascent+descent) so display headings never clip (LC-11): Roboto Slab
53
- // ≈1.32, Nunito ≈1.36, Silkscreen ≈1.3, Poppins/Futura ≈1.4.
54
- slab: { heading: 1.35, body: 1.4, letterSpacing: "inherit" },
55
- rounded: { heading: 1.4, body: 1.4, letterSpacing: "inherit" },
56
- condensed: { heading: 1.15, body: 1.4, letterSpacing: "inherit" },
57
- cursive: { heading: 1.3, body: 1.5, letterSpacing: "neutral" },
58
- handwriting: { heading: 1.25, body: 1.45, letterSpacing: "neutral" },
59
- pixel: { heading: 1.35, body: 1.4, letterSpacing: "neutral" },
60
- blackletter: { heading: 1.3, body: 1.45, letterSpacing: "neutral" },
61
- geometric: { heading: 1.45, body: 1.4, letterSpacing: "inherit" },
62
- };
44
+ export { fontCategoryMetrics };
63
45
 
64
46
  // Default leading for font-scoped text that carries no explicit lineHeight
65
47
  // (e.g. `Text fontFamily="$body" fontSize=…` renders `line-height: normal`
@@ -90,7 +72,8 @@ export interface FontScopeTables {
90
72
  /**
91
73
  * Resolve the concrete font-family stack registered in the Tamagui config for
92
74
  * a font category. Returns undefined when the category font is not registered
93
- * (the caller then leaves the default family in place).
75
+ * (the caller then uses `fontCategoryStacks` so the knob still writes
76
+ * `--f-family` — live SHC console had the style tag and skipped the family).
94
77
  */
95
78
  export function getConfiguredFontFamily(category: FontCategory): string | undefined {
96
79
  let config: ReturnType<typeof getConfig> | undefined;
@@ -164,9 +147,9 @@ function buildScopeRule(
164
147
  for (const [key, size] of Object.entries(tables.size)) {
165
148
  const baseLineHeight = tables.lineHeight[key];
166
149
  if (typeof baseLineHeight !== "number") continue;
167
- const floor = Math.round(size * minRatio);
168
- if (baseLineHeight < floor) {
169
- decls.push(`--f-lineHeight-${key}: ${floor}px !important`);
150
+ const floored = flooredLineHeight(size, baseLineHeight, minRatio);
151
+ if (floored > baseLineHeight) {
152
+ decls.push(`--f-lineHeight-${key}: ${floored}px !important`);
170
153
  }
171
154
  }
172
155
  if (metrics.letterSpacing === "neutral" && tables.letterSpacing) {
@@ -202,13 +185,13 @@ export function buildFontKnobCss(
202
185
  defaultLeadingRule("body", bodyFont),
203
186
  ];
204
187
  if (headingFont !== "sans-serif") {
205
- const family = resolveFamily(headingFont);
188
+ const family = resolveFamily(headingFont) ?? fontCategoryStacks[headingFont];
206
189
  if (family) {
207
190
  rules.push(buildScopeRule("heading", headingFont, family, resolveTables("heading")));
208
191
  }
209
192
  }
210
193
  if (bodyFont !== "sans-serif") {
211
- const family = resolveFamily(bodyFont);
194
+ const family = resolveFamily(bodyFont) ?? fontCategoryStacks[bodyFont];
212
195
  if (family) {
213
196
  rules.push(buildScopeRule("body", bodyFont, family, resolveTables("body")));
214
197
  }
@@ -13,7 +13,8 @@ export interface TintProps extends PropsWithChildren {
13
13
 
14
14
  /**
15
15
  * Depth-based tint component. Each nesting level increments the depth
16
- * and selects a tint from the preset's tints array via modulo.
16
+ * and selects a tint from the preset's tints array via
17
+ * `tints[(depth - 1) % length]` so the first Tint is `tints[0]`.
17
18
  *
18
19
  * Applies a tint sub-theme based on nesting depth.
19
20
  */
@@ -21,7 +22,7 @@ export function Tint({ children, alt = 0, disable, ...props }: TintProps) {
21
22
  const parentDepth = useContext(TintDepthContext);
22
23
  const depth = parentDepth + 1 + alt;
23
24
  const tints = usePresetTints();
24
- const tint = tints.length > 0 ? tints[depth % tints.length] : undefined;
25
+ const tint = tints.length > 0 ? tints[(depth - 1) % tints.length] : undefined;
25
26
 
26
27
  return (
27
28
  <TintDepthContext.Provider value={depth}>
@@ -1,10 +1,23 @@
1
1
  import { createAnimations } from "@tamagui/animations-css";
2
2
 
3
+ /**
4
+ * The CSS driver's named durations.
5
+ *
6
+ * `bouncy`, `lazy`, `slow`, `medium` and `quick` are Tamagui's own names and
7
+ * carry Tamagui's own values — a shared name that means something different
8
+ * here is worse than no name, because every consumer reading `animation="medium"`
9
+ * against the upstream docs gets a different answer than the one it renders.
10
+ * MPO-19 X10: `medium` was `ease-in-out 250ms` against stock `ease-in 300ms`,
11
+ * with no improvement argument written down anywhere.
12
+ *
13
+ * `tooltip`, `snappy` and `gentle` are additive — names Tamagui does not use,
14
+ * so they are ours to set and are not a contradiction.
15
+ */
3
16
  export const animationConfig = {
4
17
  bouncy: "ease-in 200ms",
5
18
  lazy: "ease-in 600ms",
6
19
  slow: "ease-in 500ms",
7
- medium: "ease-in-out 250ms",
20
+ medium: "ease-in 300ms",
8
21
  quick: "ease-in 100ms",
9
22
  tooltip: "ease-in 400ms",
10
23
  snappy: "ease-out 80ms",