@multiplatform.one/theme 6.0.4 → 6.3.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 (187) hide show
  1. package/README.md +73 -0
  2. package/package.json +22 -9
  3. package/src/dates.spec.ts +75 -0
  4. package/src/dates.ts +130 -0
  5. package/src/devWarn.spec.ts +144 -0
  6. package/src/devWarn.ts +235 -0
  7. package/src/figma/figmaTokens.spec.ts +244 -0
  8. package/src/figma/figmaTokens.ts +503 -0
  9. package/src/font.ts +6 -8
  10. package/src/fonts/createFontLoader.web.ts +15 -4
  11. package/src/index.ts +7 -0
  12. package/src/keyboardFocusRing.ts +45 -0
  13. package/src/menuRow.spec.ts +32 -0
  14. package/src/menuRow.ts +31 -0
  15. package/src/theme/FontKnobStyles.native.tsx +24 -0
  16. package/src/theme/FontKnobStyles.tsx +256 -0
  17. package/src/theme/animations/reactNative.ts +6 -1
  18. package/src/theme/chartPalette.spec.ts +159 -0
  19. package/src/theme/chartPalette.ts +170 -0
  20. package/src/theme/colorRules.ts +33 -1
  21. package/src/theme/cookies.spec.ts +32 -0
  22. package/src/theme/cookiesPersistence.spec.ts +86 -0
  23. package/src/theme/cornerSmoothing.native.ts +16 -0
  24. package/src/theme/cornerSmoothing.spec.ts +27 -0
  25. package/src/theme/cornerSmoothing.ts +46 -0
  26. package/src/theme/createDefaultThemeConfig.ts +32 -8
  27. package/src/theme/createThemeConfig.spec.ts +137 -0
  28. package/src/theme/createThemes.ts +77 -2
  29. package/src/theme/defaults/accent.ts +39 -0
  30. package/src/theme/defaults/base.ts +63 -0
  31. package/src/theme/defaults/builderOptions.ts +174 -0
  32. package/src/theme/defaults/fonts.spec.ts +87 -0
  33. package/src/theme/defaults/fonts.ts +89 -0
  34. package/src/theme/defaults/index.ts +4 -0
  35. package/src/theme/devtools/ThemeDevtoolsPanel.tsx +196 -2
  36. package/src/theme/devtools/buildSnapshot.ts +11 -0
  37. package/src/theme/devtools/knobIcons.tsx +141 -0
  38. package/src/theme/devtools/vizConstants.ts +1 -1
  39. package/src/theme/focusState.spec.ts +174 -0
  40. package/src/theme/focusState.ts +112 -0
  41. package/src/theme/fontKnobStyles.spec.ts +98 -0
  42. package/src/theme/hairline.native.ts +43 -0
  43. package/src/theme/hairline.spec.ts +63 -0
  44. package/src/theme/hairline.ts +101 -0
  45. package/src/theme/index.ts +8 -0
  46. package/src/theme/intent.spec.tsx +2 -2
  47. package/src/theme/intents.ts +6 -2
  48. package/src/theme/knobs.ts +134 -2
  49. package/src/theme/layoutTokens.spec.ts +75 -0
  50. package/src/theme/layoutTokens.ts +213 -0
  51. package/src/theme/layoutTokensHooks.spec.tsx +157 -0
  52. package/src/theme/presets.spec.ts +17 -0
  53. package/src/theme/presets.ts +22 -2
  54. package/src/theme/readableColor.ts +101 -0
  55. package/src/theme/recipes.ts +130 -7
  56. package/src/theme/resolveKnobs.spec.ts +560 -21
  57. package/src/theme/resolveKnobs.ts +267 -23
  58. package/src/theme/shared.tsx +26 -2
  59. package/src/theme/useResolvedKnobs.ts +67 -26
  60. package/src/theme/useResolvedKnobsBehavior.spec.tsx +166 -0
  61. package/types/audit/constraintAudit.d.ts +83 -0
  62. package/types/audit/constraintAudit.d.ts.map +1 -0
  63. package/types/audit/diffReport.d.ts +45 -0
  64. package/types/audit/diffReport.d.ts.map +1 -0
  65. package/types/audit/index.d.ts +5 -0
  66. package/types/audit/index.d.ts.map +1 -0
  67. package/types/dates.d.ts +52 -0
  68. package/types/dates.d.ts.map +1 -0
  69. package/types/devWarn.d.ts +104 -0
  70. package/types/devWarn.d.ts.map +1 -0
  71. package/types/figma/figmaTokens.d.ts +168 -0
  72. package/types/figma/figmaTokens.d.ts.map +1 -0
  73. package/types/font.d.ts +5 -0
  74. package/types/font.d.ts.map +1 -0
  75. package/types/fonts/createFontLoader.d.ts +3 -0
  76. package/types/fonts/createFontLoader.d.ts.map +1 -0
  77. package/types/fonts/createFontLoader.web.d.ts +3 -0
  78. package/types/fonts/createFontLoader.web.d.ts.map +1 -0
  79. package/types/fonts/index.d.ts +3 -0
  80. package/types/fonts/index.d.ts.map +1 -0
  81. package/types/fonts/types.d.ts +12 -0
  82. package/types/fonts/types.d.ts.map +1 -0
  83. package/types/index.d.ts +9 -0
  84. package/types/index.d.ts.map +1 -0
  85. package/types/keyboardFocusRing.d.ts +32 -0
  86. package/types/keyboardFocusRing.d.ts.map +1 -0
  87. package/types/menuRow.d.ts +32 -0
  88. package/types/menuRow.d.ts.map +1 -0
  89. package/types/theme/FontKnobStyles.d.ts +49 -0
  90. package/types/theme/FontKnobStyles.d.ts.map +1 -0
  91. package/types/theme/FontKnobStyles.native.d.ts +12 -0
  92. package/types/theme/FontKnobStyles.native.d.ts.map +1 -0
  93. package/types/theme/Intent.d.ts +20 -0
  94. package/types/theme/Intent.d.ts.map +1 -0
  95. package/types/theme/Preset.d.ts +15 -0
  96. package/types/theme/Preset.d.ts.map +1 -0
  97. package/types/theme/PresetContext.d.ts +18 -0
  98. package/types/theme/PresetContext.d.ts.map +1 -0
  99. package/types/theme/Tint.d.ts +19 -0
  100. package/types/theme/Tint.d.ts.map +1 -0
  101. package/types/theme/animations/css.d.ts +24 -0
  102. package/types/theme/animations/css.d.ts.map +1 -0
  103. package/types/theme/animations/index.d.ts +2 -0
  104. package/types/theme/animations/index.d.ts.map +1 -0
  105. package/types/theme/animations/index.web.d.ts +2 -0
  106. package/types/theme/animations/index.web.d.ts.map +1 -0
  107. package/types/theme/animations/reactNative.d.ts +104 -0
  108. package/types/theme/animations/reactNative.d.ts.map +1 -0
  109. package/types/theme/chartPalette.d.ts +62 -0
  110. package/types/theme/chartPalette.d.ts.map +1 -0
  111. package/types/theme/colorRules.d.ts +69 -0
  112. package/types/theme/colorRules.d.ts.map +1 -0
  113. package/types/theme/cookies.d.ts +76 -0
  114. package/types/theme/cookies.d.ts.map +1 -0
  115. package/types/theme/cornerSmoothing.d.ts +31 -0
  116. package/types/theme/cornerSmoothing.d.ts.map +1 -0
  117. package/types/theme/cornerSmoothing.native.d.ts +14 -0
  118. package/types/theme/cornerSmoothing.native.d.ts.map +1 -0
  119. package/types/theme/createDefaultThemeConfig.d.ts +37 -0
  120. package/types/theme/createDefaultThemeConfig.d.ts.map +1 -0
  121. package/types/theme/createThemes.d.ts +38 -0
  122. package/types/theme/createThemes.d.ts.map +1 -0
  123. package/types/theme/defaults/accent.d.ts +3 -0
  124. package/types/theme/defaults/accent.d.ts.map +1 -0
  125. package/types/theme/defaults/base.d.ts +3 -0
  126. package/types/theme/defaults/base.d.ts.map +1 -0
  127. package/types/theme/defaults/builderOptions.d.ts +3 -0
  128. package/types/theme/defaults/builderOptions.d.ts.map +1 -0
  129. package/types/theme/defaults/fonts.d.ts +7 -0
  130. package/types/theme/defaults/fonts.d.ts.map +1 -0
  131. package/types/theme/defaults/index.d.ts +5 -0
  132. package/types/theme/defaults/index.d.ts.map +1 -0
  133. package/types/theme/devtools/ColorLineVisualizer.d.ts +17 -0
  134. package/types/theme/devtools/ColorLineVisualizer.d.ts.map +1 -0
  135. package/types/theme/devtools/ThemeDevtoolsPanel.d.ts +87 -0
  136. package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -0
  137. package/types/theme/devtools/buildSnapshot.d.ts +12 -0
  138. package/types/theme/devtools/buildSnapshot.d.ts.map +1 -0
  139. package/types/theme/devtools/knobIcons.d.ts +33 -0
  140. package/types/theme/devtools/knobIcons.d.ts.map +1 -0
  141. package/types/theme/devtools/vizConstants.d.ts +5 -0
  142. package/types/theme/devtools/vizConstants.d.ts.map +1 -0
  143. package/types/theme/focusState.d.ts +42 -0
  144. package/types/theme/focusState.d.ts.map +1 -0
  145. package/types/theme/hairline.d.ts +88 -0
  146. package/types/theme/hairline.d.ts.map +1 -0
  147. package/types/theme/hairline.native.d.ts +42 -0
  148. package/types/theme/hairline.native.d.ts.map +1 -0
  149. package/types/theme/index.d.ts +29 -0
  150. package/types/theme/index.d.ts.map +1 -0
  151. package/types/theme/intents.d.ts +3 -0
  152. package/types/theme/intents.d.ts.map +1 -0
  153. package/types/theme/knobs.d.ts +236 -0
  154. package/types/theme/knobs.d.ts.map +1 -0
  155. package/types/theme/layoutTokens.d.ts +104 -0
  156. package/types/theme/layoutTokens.d.ts.map +1 -0
  157. package/types/theme/preset.types.d.ts +21 -0
  158. package/types/theme/preset.types.d.ts.map +1 -0
  159. package/types/theme/presets.d.ts +4 -0
  160. package/types/theme/presets.d.ts.map +1 -0
  161. package/types/theme/readableColor.d.ts +17 -0
  162. package/types/theme/readableColor.d.ts.map +1 -0
  163. package/types/theme/recipes.d.ts +200 -0
  164. package/types/theme/recipes.d.ts.map +1 -0
  165. package/types/theme/resolveKnobs.d.ts +22 -0
  166. package/types/theme/resolveKnobs.d.ts.map +1 -0
  167. package/types/theme/shared.d.ts +40 -0
  168. package/types/theme/shared.d.ts.map +1 -0
  169. package/types/theme/theme.d.ts +19 -0
  170. package/types/theme/theme.d.ts.map +1 -0
  171. package/types/theme/theme.native.d.ts +10 -0
  172. package/types/theme/theme.native.d.ts.map +1 -0
  173. package/types/theme/useColorScale.d.ts +7 -0
  174. package/types/theme/useColorScale.d.ts.map +1 -0
  175. package/types/theme/useColorScale.native.d.ts +6 -0
  176. package/types/theme/useColorScale.native.d.ts.map +1 -0
  177. package/types/theme/useResolvedKnobs.d.ts +32 -0
  178. package/types/theme/useResolvedKnobs.d.ts.map +1 -0
  179. package/types/theme/useTheme.d.ts +31 -0
  180. package/types/theme/useTheme.d.ts.map +1 -0
  181. package/types/theme/useTheme.native.d.ts +10 -0
  182. package/types/theme/useTheme.native.d.ts.map +1 -0
  183. package/CHANGELOG.md +0 -46
  184. package/src/theme/__snapshots__/resolveKnobs.spec.ts.snap +0 -69
  185. package/src/theme/pokemonScreen.spec.ts +0 -109
  186. package/tsconfig.json +0 -11
  187. package/vitest.config.mjs +0 -9
@@ -0,0 +1,174 @@
1
+ import type { CreateThemesBuilderOptions, GetThemeProps } from "../createThemes";
2
+ import {
3
+ contrastRatio,
4
+ minContrastRatio,
5
+ normalizeToHex,
6
+ pickReadableForeground,
7
+ relativeLuminance,
8
+ } from "../colorRules";
9
+
10
+ /**
11
+ * DG-A11Y-01 / Axiom 12 — resolve a solid `$outlineColor` for the keyboard
12
+ * focus-visible ring that clears the ≥3:1 contrast floor against BOTH page
13
+ * surfaces (color1/color2) in this scheme's own ramp. The house themes never
14
+ * set `$outlineColor`, so it fell through to Tamagui's stock 20%-alpha ink
15
+ * (measured 1.36:1) — every keyboard user got a near-invisible ring (DF-03).
16
+ *
17
+ * Steps are tried strongest-visible-first; the first that clears 3:1 vs both
18
+ * page steps wins, else the highest-contrast candidate. The ring sits offset
19
+ * outside the control on the page, so page contrast is the governing pair.
20
+ */
21
+ function pickFocusRingColorFromSteps(step: (n: number) => string | undefined): string {
22
+ const fallback = step(11) ?? step(12) ?? step(9) ?? "transparent";
23
+ const bg1 = normalizeToHex(step(1) ?? "");
24
+ const bg2 = normalizeToHex(step(2) ?? "");
25
+ if (!bg1 || !bg2) return fallback;
26
+ const l1 = relativeLuminance(bg1);
27
+ const l2 = relativeLuminance(bg2);
28
+ // Prefer a strong-but-not-ink neutral so the ring is clearly visible in both
29
+ // schemes without reading as heavy black/white text.
30
+ const candidates = [11, 10, 9, 12, 8];
31
+ let best = fallback;
32
+ let bestMin = 0;
33
+ for (const key of candidates) {
34
+ const raw = step(key);
35
+ const hex = raw ? normalizeToHex(raw) : null;
36
+ if (!raw || !hex) continue;
37
+ const l = relativeLuminance(hex);
38
+ const minC = Math.min(contrastRatio(l, l1), contrastRatio(l, l2));
39
+ if (minC >= minContrastRatio) return raw;
40
+ if (minC > bestMin) {
41
+ bestMin = minC;
42
+ best = raw;
43
+ }
44
+ }
45
+ return best;
46
+ }
47
+
48
+ function pickFocusRingColor(theme: Record<string, string>): string {
49
+ return pickFocusRingColorFromSteps((n) => theme[`color${n}`]);
50
+ }
51
+
52
+ function getBaseTheme(theme: Record<string, string>) {
53
+ return {
54
+ // DG-A11Y-01: solid, scheme-aware focus ring (≥3:1 vs page surfaces).
55
+ outlineColor: pickFocusRingColor(theme),
56
+ // Normalize interaction ramp across components:
57
+ // background: 5 -> 6 -> 4, border: 5 -> 6 -> 7.
58
+ backgroundPress: theme.color5,
59
+ backgroundFocus: theme.color6,
60
+ backgroundActive: theme.color4,
61
+ // Align placeholder/disabled text with the dedicated muted slot.
62
+ placeholderColor: theme.color10,
63
+ // Subtle default borders; focus/press one step deeper than hover.
64
+ borderColor: theme.color5,
65
+ borderColorHover: theme.color6,
66
+ borderColorFocus: theme.color7,
67
+ borderColorPress: theme.color7,
68
+ // Project-level semantic aliases for palette slots not covered by Tamagui defaults.
69
+ // textMuted sits on the $color11 tier: the lowest ramp step that keeps
70
+ // secondary text AA-readable on color1/2 surfaces (SB-R contrast sweep).
71
+ textMuted: theme.color11,
72
+ textSubtle: theme.color6,
73
+ borderStrong: theme.color7,
74
+ borderStrongHover: theme.color8,
75
+ borderStrongPress: theme.color6,
76
+ surfaceStrong: theme.color7,
77
+ surfaceMuted: theme.color10,
78
+ surfacePressStrong: theme.color6,
79
+ };
80
+ }
81
+
82
+ // Semantic intents whose Buttons render SOLID (one emphasis system with
83
+ // accent — SB-E-01). The accent theme is solid by palette shape already.
84
+ const solidIntentParents = new Set(["error", "warning", "success"]);
85
+
86
+ /**
87
+ * Adjust a background's lightness until it clears WCAG AA (4.5:1) against
88
+ * the given foreground. Steps away from the foreground's lightness 1.5% at a
89
+ * time; bails (returns the original) on unparseable colors.
90
+ */
91
+ function clampBgForReadableText(bgRaw: string, fgRaw: string): string {
92
+ const fgHex = normalizeToHex(fgRaw);
93
+ let bgHex = normalizeToHex(bgRaw);
94
+ if (!fgHex || !bgHex) return bgRaw;
95
+ const fgLum = relativeLuminance(fgHex);
96
+ const hslMatch = bgRaw.match(/hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/);
97
+ if (!hslMatch) return bgRaw;
98
+ const h = Number(hslMatch[1]);
99
+ const s = Number(hslMatch[2]);
100
+ let l = Number(hslMatch[3]);
101
+ const dir = fgLum > relativeLuminance(bgHex) ? -1 : 1;
102
+ for (let i = 0; i < 24; i++) {
103
+ if (contrastRatio(fgLum, relativeLuminance(bgHex)) >= 4.5) break;
104
+ l = Math.min(100, Math.max(0, l + dir * 1.5));
105
+ const next = normalizeToHex(`hsl(${h}, ${s}%, ${l}%)`);
106
+ if (!next) return bgRaw;
107
+ bgHex = next;
108
+ }
109
+ return `hsl(${h}, ${s}%, ${l}%)`;
110
+ }
111
+
112
+ export const defaultBuilderOptions: CreateThemesBuilderOptions = {
113
+ getTheme: ({ name, theme, palette }: GetThemeProps): Record<string, string> => {
114
+ // Component sub-themes (t_Card, t_ListItem, t_Button, ...) are narrow
115
+ // templates -- background/color steps only, NO colorN/border tokens. An
116
+ // element inside one that sets borderColor="$borderColor" resolves
117
+ // nothing and CSS falls back to currentColor: near-black borders on
118
+ // every bordered card/row. Anchor border tokens from the scheme
119
+ // palette (padded shape: [(len-12)/2 alpha anchors] + 12 colors +
120
+ // [anchors]; colorN = colors[N-1]).
121
+ if (!theme.color5) {
122
+ if (!palette) return {};
123
+ const colors0 = Math.max(0, Math.floor((palette.length - 12) / 2));
124
+ const step = (i: number): string => palette[colors0 + i] ?? "transparent";
125
+ // DF-03: component sub-theme templates (t_Input, t_Button, ...) carry
126
+ // Tamagui's stock 20%-alpha `outlineColor`, which silently overrides the
127
+ // base theme's solid ring inside those scopes. Re-derive the solid,
128
+ // scheme-aware ring from this sub-theme's own palette steps.
129
+ const outlineColor = pickFocusRingColorFromSteps((n) => step(n - 1));
130
+
131
+ // Solid semantic-intent Buttons (light_error_Button, dark_warning_Button,
132
+ // ...): surface on the hue's step 11 with a LUMINANCE-COMPUTED foreground
133
+ // (Axiom 15 / LC-05) — the hue's step-1 or step-12 anchor, whichever
134
+ // reads better on the resting fill. The sub-theme `color` carries it so
135
+ // labels ($color via useResolvedKnobs), icons (currentColor) and any
136
+ // inherited text all pair correctly. Every interaction fill is then
137
+ // darkened/lightened until it holds that foreground at the 4.5 AA floor.
138
+ const parts = name.split("_");
139
+ if (parts[parts.length - 1] === "Button" && solidIntentParents.has(parts[parts.length - 2])) {
140
+ const scheme = parts[0];
141
+ // Luminance-computed on-fill foreground (Axiom 15): the hue's step-1
142
+ // (pale) or step-12 (deep) anchor, whichever reads better on the fill.
143
+ const fg = pickReadableForeground(step(10), step(0), step(11));
144
+ const bg = clampBgForReadableText(step(10), fg);
145
+ const hover = clampBgForReadableText(scheme === "light" ? step(9) : step(11), fg);
146
+ const press = clampBgForReadableText(scheme === "light" ? step(11) : step(9), fg);
147
+ return {
148
+ outlineColor,
149
+ background: bg,
150
+ backgroundHover: hover,
151
+ backgroundPress: press,
152
+ backgroundFocus: hover,
153
+ color: fg,
154
+ colorHover: fg,
155
+ colorPress: fg,
156
+ colorFocus: fg,
157
+ borderColor: bg,
158
+ borderColorHover: hover,
159
+ borderColorFocus: hover,
160
+ borderColorPress: press,
161
+ };
162
+ }
163
+
164
+ return {
165
+ outlineColor,
166
+ borderColor: step(4),
167
+ borderColorHover: step(5),
168
+ borderColorFocus: step(6),
169
+ borderColorPress: step(6),
170
+ };
171
+ }
172
+ return getBaseTheme(theme);
173
+ },
174
+ };
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { defaultBodyFont, defaultHeadingFont, interTrackingPx } from "./fonts";
3
+
4
+ type NumTable = Record<string, number>;
5
+
6
+ /** Unwrap a font table whose values may be tamagui Variables. */
7
+ function table(font: unknown, key: "size" | "lineHeight" | "letterSpacing"): NumTable {
8
+ const raw = (font as Record<string, Record<string, number | { val: number }>>)[key] ?? {};
9
+ const out: NumTable = {};
10
+ for (const [k, v] of Object.entries(raw)) {
11
+ const num = typeof v === "number" ? v : v && typeof v.val === "number" ? v.val : undefined;
12
+ if (num !== undefined) out[k.replace(/^\$/, "")] = num;
13
+ }
14
+ return out;
15
+ }
16
+
17
+ describe("default heading/body fonts — tracking-by-size (Axiom 15 OPTICS, LC-11)", () => {
18
+ for (const [name, font] of [
19
+ ["heading", defaultHeadingFont],
20
+ ["body", defaultBodyFont],
21
+ ] as const) {
22
+ describe(`${name} font`, () => {
23
+ const size = table(font, "size");
24
+ const letterSpacing = table(font, "letterSpacing");
25
+
26
+ it("defines tracking for every size key, computed from the rendered size", () => {
27
+ expect(Object.keys(size).length).toBeGreaterThan(0);
28
+ for (const [key, px] of Object.entries(size)) {
29
+ expect(letterSpacing[key], `letterSpacing[${key}]`).toBe(interTrackingPx(px));
30
+ }
31
+ });
32
+
33
+ it("is looser at caption sizes and tighter at display sizes", () => {
34
+ // caption end (≤12px rendered) is neutral-to-loose, never tighter
35
+ for (const [key, px] of Object.entries(size)) {
36
+ if (px <= 12) expect(letterSpacing[key]).toBeGreaterThanOrEqual(0);
37
+ if (px >= 28) expect(letterSpacing[key]).toBeLessThanOrEqual(-0.55);
38
+ }
39
+ });
40
+
41
+ it("tracking decreases monotonically as size grows", () => {
42
+ const bySize = Object.entries(size)
43
+ .filter(([key]) => key !== "true")
44
+ .sort((a, b) => a[1] - b[1]);
45
+ for (let i = 1; i < bySize.length; i++) {
46
+ const [prevKey, prevPx] = bySize[i - 1] as [string, number];
47
+ const [key, px] = bySize[i] as [string, number];
48
+ if (px === prevPx) continue;
49
+ expect(letterSpacing[key], `letterSpacing[${key}]`).toBeLessThan(
50
+ letterSpacing[prevKey] as number,
51
+ );
52
+ }
53
+ });
54
+ });
55
+ }
56
+
57
+ it("heading H1–H4 land on the expected optical tracking (snapshot)", () => {
58
+ const size = table(defaultHeadingFont, "size");
59
+ const letterSpacing = table(defaultHeadingFont, "letterSpacing");
60
+ // H1 $10=64px, H2 $9=42px, H3 $8=32px, H4 $7=28px on the 1.4x scale
61
+ expect({
62
+ h1: { size: size["10"], tracking: letterSpacing["10"] },
63
+ h2: { size: size["9"], tracking: letterSpacing["9"] },
64
+ h3: { size: size["8"], tracking: letterSpacing["8"] },
65
+ h4: { size: size["7"], tracking: letterSpacing["7"] },
66
+ }).toMatchInlineSnapshot(`
67
+ {
68
+ "h1": {
69
+ "size": 64,
70
+ "tracking": -1.43,
71
+ },
72
+ "h2": {
73
+ "size": 42,
74
+ "tracking": -0.93,
75
+ },
76
+ "h3": {
77
+ "size": 32,
78
+ "tracking": -0.69,
79
+ },
80
+ "h4": {
81
+ "size": 28,
82
+ "tracking": -0.59,
83
+ },
84
+ }
85
+ `);
86
+ });
87
+ });
@@ -0,0 +1,89 @@
1
+ import { createInterFont } from "@tamagui/font-inter";
2
+
3
+ // Default purpose fonts for components that reference $heading / $body
4
+ // directly, both backed by Inter.
5
+ // Tables match @tamagui/config/v4 (the scale Bento is authored against):
6
+ // headings 1.4x the base size scale (H1 $10 = 64px, H2 $9 = 42, H3 $8 = 32,
7
+ // H4 $7 = 28) at default weight with zero tracking — large and light, not
8
+ // small and bold — and body at the unscaled 14px/$true with lineHeight
9
+ // size+10. No uppercase transforms.
10
+ //
11
+ // Tracking-by-size (Axiom 15 OPTICS + LC-11): letter-spacing is a function
12
+ // of the rendered size, not a constant — slightly loose at caption sizes,
13
+ // increasingly tight at display sizes (the same optical behavior Apple's SF
14
+ // implements per point size). The curve is Inter's own dynamic-metrics
15
+ // model (rsms.me/inter/dynmetrics): tracking(em) = a + b·e^(c·size).
16
+ // These values are Inter-specific: when the headingFont/bodyFont knob picks
17
+ // another family, FontKnobStyles' per-category metrics (LC-11) neutralize
18
+ // this table for fixed-pitch and connected-script categories.
19
+
20
+ const TRACKING_A = -0.0223;
21
+ const TRACKING_B = 0.185;
22
+ const TRACKING_C = -0.1745;
23
+
24
+ /** Inter dynamic tracking for a rendered `sizePx`, in em. */
25
+ export function interTrackingEm(sizePx: number): number {
26
+ return TRACKING_A + TRACKING_B * Math.exp(TRACKING_C * sizePx);
27
+ }
28
+
29
+ /** Inter dynamic tracking for a rendered `sizePx`, in px (2-decimal). */
30
+ export function interTrackingPx(sizePx: number): number {
31
+ return Math.round(interTrackingEm(sizePx) * sizePx * 100) / 100;
32
+ }
33
+
34
+ // Mirrors @tamagui/font-inter's default size table (pinned 2.0.0-rc.41) so
35
+ // tracking can be computed per size key. The fonts.spec.ts invariant test
36
+ // (letterSpacing[k] === interTrackingPx(size[k]) on the BUILT font) catches
37
+ // any upstream drift.
38
+ const interBaseSizes: Record<string, number> = {
39
+ 1: 11,
40
+ 2: 12,
41
+ 3: 13,
42
+ 4: 14,
43
+ true: 14,
44
+ 5: 16,
45
+ 6: 18,
46
+ 7: 20,
47
+ 8: 23,
48
+ 9: 30,
49
+ 10: 46,
50
+ 11: 55,
51
+ 12: 62,
52
+ 13: 72,
53
+ 14: 92,
54
+ 15: 114,
55
+ 16: 134,
56
+ };
57
+
58
+ function trackingTable(scaleSize: (size: number) => number): Record<string, number> {
59
+ return Object.fromEntries(
60
+ Object.entries(interBaseSizes).map(([key, size]) => [key, interTrackingPx(scaleSize(size))]),
61
+ );
62
+ }
63
+
64
+ const headingSizeOf = (size: number) => Math.round(size * 1.4);
65
+
66
+ export const defaultHeadingFont = createInterFont(
67
+ {
68
+ face: {
69
+ 700: { normal: "InterBold" },
70
+ },
71
+ letterSpacing: trackingTable(headingSizeOf),
72
+ },
73
+ {
74
+ sizeSize: headingSizeOf,
75
+ sizeLineHeight: (size) => size + 10,
76
+ },
77
+ );
78
+
79
+ export const defaultBodyFont = createInterFont(
80
+ {
81
+ face: {
82
+ 700: { normal: "InterBold" },
83
+ },
84
+ letterSpacing: trackingTable((size) => size),
85
+ },
86
+ {
87
+ sizeLineHeight: (size) => size + 10,
88
+ },
89
+ );
@@ -0,0 +1,4 @@
1
+ export * from "./accent";
2
+ export * from "./base";
3
+ export * from "./builderOptions";
4
+ export * from "./fonts";
@@ -6,8 +6,12 @@
6
6
  * Controls:
7
7
  * - Color scheme (system / light / dark) via @vxrn/color-scheme
8
8
  * - Preset picker (using registered presets)
9
- * - All knobs: fillStyle, borderRadius, borderWidth, elevation, space, size,
10
- * textAccent, headingFont, bodyFont, fontWeight, animation
9
+ * - All knobs: fillStyle, borderRadius, cornerSmoothing, borderWidth,
10
+ * elevation, space, size, density, textAccent, headingFont, bodyFont,
11
+ * fontWeight, animation,
12
+ * plus house decisions (fieldLabelPlacement, requiredMarking, tableZebra,
13
+ * bulkBarPlacement, selectAllScope, timestampStyle, disabledStyle,
14
+ * formAutofocus)
11
15
  *
12
16
  * This panel is rendered *inside* the TanStack Devtools shell so it shares
13
17
  * the same React tree and can call hooks like `useTheme` / `useUserScheme`.
@@ -48,14 +52,24 @@ import {
48
52
  defaultKnobs,
49
53
  FillStyle,
50
54
  BorderRadius,
55
+ CornerSmoothing,
51
56
  BorderWidth,
52
57
  Elevation,
53
58
  Space,
54
59
  Size,
60
+ Density,
55
61
  TextAccent,
56
62
  HeadingFont,
57
63
  BodyFont,
58
64
  FontWeight,
65
+ FieldLabelPlacement,
66
+ RequiredMarking,
67
+ TableZebra,
68
+ BulkBarPlacement,
69
+ SelectAllScope,
70
+ TimestampStyle,
71
+ DisabledStyle,
72
+ FormAutofocus,
59
73
  interactionStates,
60
74
  } from "../knobs";
61
75
  import { animationNames } from "../animations/index";
@@ -66,7 +80,9 @@ import {
66
80
  fillIcon as _fillIcon,
67
81
  spaceIcon as _spaceIcon,
68
82
  sizeIcon as _sizeIcon,
83
+ densityIcon as _densityIcon,
69
84
  borderRadiusIcon as _borderRadiusIcon,
85
+ cornerSmoothingIcon as _cornerSmoothingIcon,
70
86
  borderWidthIcon as _borderWidthIcon,
71
87
  elevationIcon as _elevationIcon,
72
88
  textAccentIcon as _textAccentIcon,
@@ -74,6 +90,14 @@ import {
74
90
  bodyFontIcon as _bodyFontIcon,
75
91
  fontWeightIcon as _fontWeightIcon,
76
92
  modeIcon as _modeIcon,
93
+ fieldLabelPlacementIcon as _fieldLabelPlacementIcon,
94
+ requiredMarkingIcon as _requiredMarkingIcon,
95
+ tableZebraIcon as _tableZebraIcon,
96
+ bulkBarPlacementIcon as _bulkBarPlacementIcon,
97
+ selectAllScopeIcon as _selectAllScopeIcon,
98
+ timestampStyleIcon as _timestampStyleIcon,
99
+ disabledStyleIcon as _disabledStyleIcon,
100
+ formAutofocusIcon as _formAutofocusIcon,
77
101
  } from "./knobIcons";
78
102
 
79
103
  function extractAnimationNames(tamaguiConfig: unknown): string[] {
@@ -111,15 +135,25 @@ const stateDisplayLabels: Record<InteractionState, string> = {
111
135
  const knobKeys: (keyof Knobs)[] = [
112
136
  "fillStyle",
113
137
  "borderRadius",
138
+ "cornerSmoothing",
114
139
  "borderWidth",
115
140
  "elevation",
116
141
  "space",
117
142
  "size",
143
+ "density",
118
144
  "textAccent",
119
145
  "headingFont",
120
146
  "bodyFont",
121
147
  "fontWeight",
122
148
  "animation",
149
+ "fieldLabelPlacement",
150
+ "requiredMarking",
151
+ "tableZebra",
152
+ "bulkBarPlacement",
153
+ "selectAllScope",
154
+ "timestampStyle",
155
+ "disabledStyle",
156
+ "formAutofocus",
123
157
  ];
124
158
 
125
159
  /** Knob keys that support per-state overrides. */
@@ -685,10 +719,18 @@ function sizeIcon(v: string, active: boolean, dark: boolean): ReactNode {
685
719
  return _sizeIcon(v, ic(active, dark));
686
720
  }
687
721
 
722
+ function densityIcon(v: string, active: boolean, dark: boolean): ReactNode {
723
+ return _densityIcon(v, ic(active, dark));
724
+ }
725
+
688
726
  function borderRadiusIcon(v: string, active: boolean, dark: boolean): ReactNode {
689
727
  return _borderRadiusIcon(v, ic(active, dark));
690
728
  }
691
729
 
730
+ function cornerSmoothingIcon(v: string, active: boolean, dark: boolean): ReactNode {
731
+ return _cornerSmoothingIcon(v, ic(active, dark));
732
+ }
733
+
692
734
  function borderWidthIcon(v: string, active: boolean, dark: boolean): ReactNode {
693
735
  return _borderWidthIcon(v, ic(active, dark));
694
736
  }
@@ -713,6 +755,38 @@ function fontWeightIcon(v: string, active: boolean, dark: boolean): ReactNode {
713
755
  return _fontWeightIcon(v, ic(active, dark));
714
756
  }
715
757
 
758
+ function fieldLabelPlacementIcon(v: string, active: boolean, dark: boolean): ReactNode {
759
+ return _fieldLabelPlacementIcon(v, ic(active, dark));
760
+ }
761
+
762
+ function requiredMarkingIcon(v: string, active: boolean, dark: boolean): ReactNode {
763
+ return _requiredMarkingIcon(v, ic(active, dark));
764
+ }
765
+
766
+ function tableZebraIcon(v: string, active: boolean, dark: boolean): ReactNode {
767
+ return _tableZebraIcon(v, ic(active, dark));
768
+ }
769
+
770
+ function bulkBarPlacementIcon(v: string, active: boolean, dark: boolean): ReactNode {
771
+ return _bulkBarPlacementIcon(v, ic(active, dark));
772
+ }
773
+
774
+ function selectAllScopeIcon(v: string, active: boolean, dark: boolean): ReactNode {
775
+ return _selectAllScopeIcon(v, ic(active, dark));
776
+ }
777
+
778
+ function timestampStyleIcon(v: string, active: boolean, dark: boolean): ReactNode {
779
+ return _timestampStyleIcon(v, ic(active, dark));
780
+ }
781
+
782
+ function disabledStyleIcon(v: string, active: boolean, dark: boolean): ReactNode {
783
+ return _disabledStyleIcon(v, ic(active, dark));
784
+ }
785
+
786
+ function formAutofocusIcon(v: string, active: boolean, dark: boolean): ReactNode {
787
+ return _formAutofocusIcon(v, ic(active, dark));
788
+ }
789
+
716
790
  // ---------------------------------------------------------------------------
717
791
  // Main panel
718
792
  // ---------------------------------------------------------------------------
@@ -1365,6 +1439,18 @@ export function ThemeDevtoolsPanel({
1365
1439
  >
1366
1440
  {stateRows("space", Object.values(Space), spaceIcon)}
1367
1441
  </IconGroup>
1442
+ <IconGroup
1443
+ label="Density"
1444
+ options={Object.values(Density)}
1445
+ value={getOverride("density")}
1446
+ onChange={(v) => set("density", v)}
1447
+ icon={densityIcon}
1448
+ styles={styles}
1449
+ isDark={isDark}
1450
+ pinned={pinnedKnobs.has("density")}
1451
+ onTogglePin={() => togglePin("density")}
1452
+ onUnset={() => unset("density")}
1453
+ />
1368
1454
  <IconGroup
1369
1455
  label="Radius"
1370
1456
  options={Object.values(BorderRadius)}
@@ -1381,6 +1467,18 @@ export function ThemeDevtoolsPanel({
1381
1467
  >
1382
1468
  {stateRows("borderRadius", Object.values(BorderRadius), borderRadiusIcon)}
1383
1469
  </IconGroup>
1470
+ <IconGroup
1471
+ label="Smoothing"
1472
+ options={Object.values(CornerSmoothing)}
1473
+ value={getOverride("cornerSmoothing")}
1474
+ onChange={(v) => set("cornerSmoothing", v)}
1475
+ icon={cornerSmoothingIcon}
1476
+ styles={styles}
1477
+ isDark={isDark}
1478
+ pinned={pinnedKnobs.has("cornerSmoothing")}
1479
+ onTogglePin={() => togglePin("cornerSmoothing")}
1480
+ onUnset={() => unset("cornerSmoothing")}
1481
+ />
1384
1482
  <IconGroup
1385
1483
  label="Border"
1386
1484
  options={Object.values(BorderWidth)}
@@ -1488,6 +1586,102 @@ export function ThemeDevtoolsPanel({
1488
1586
  onTogglePin={() => togglePin("animation")}
1489
1587
  isDark={isDark}
1490
1588
  />
1589
+ <IconGroup
1590
+ label="Label place"
1591
+ options={Object.values(FieldLabelPlacement)}
1592
+ value={getOverride("fieldLabelPlacement")}
1593
+ onChange={(v) => set("fieldLabelPlacement", v)}
1594
+ icon={fieldLabelPlacementIcon}
1595
+ styles={styles}
1596
+ isDark={isDark}
1597
+ pinned={pinnedKnobs.has("fieldLabelPlacement")}
1598
+ onTogglePin={() => togglePin("fieldLabelPlacement")}
1599
+ onUnset={() => unset("fieldLabelPlacement")}
1600
+ />
1601
+ <IconGroup
1602
+ label="Required mark"
1603
+ options={Object.values(RequiredMarking)}
1604
+ value={getOverride("requiredMarking")}
1605
+ onChange={(v) => set("requiredMarking", v)}
1606
+ icon={requiredMarkingIcon}
1607
+ styles={styles}
1608
+ isDark={isDark}
1609
+ pinned={pinnedKnobs.has("requiredMarking")}
1610
+ onTogglePin={() => togglePin("requiredMarking")}
1611
+ onUnset={() => unset("requiredMarking")}
1612
+ />
1613
+ <IconGroup
1614
+ label="Zebra"
1615
+ options={Object.values(TableZebra)}
1616
+ value={getOverride("tableZebra")}
1617
+ onChange={(v) => set("tableZebra", v)}
1618
+ icon={tableZebraIcon}
1619
+ styles={styles}
1620
+ isDark={isDark}
1621
+ pinned={pinnedKnobs.has("tableZebra")}
1622
+ onTogglePin={() => togglePin("tableZebra")}
1623
+ onUnset={() => unset("tableZebra")}
1624
+ />
1625
+ <IconGroup
1626
+ label="Bulk bar"
1627
+ options={Object.values(BulkBarPlacement)}
1628
+ value={getOverride("bulkBarPlacement")}
1629
+ onChange={(v) => set("bulkBarPlacement", v)}
1630
+ icon={bulkBarPlacementIcon}
1631
+ styles={styles}
1632
+ isDark={isDark}
1633
+ pinned={pinnedKnobs.has("bulkBarPlacement")}
1634
+ onTogglePin={() => togglePin("bulkBarPlacement")}
1635
+ onUnset={() => unset("bulkBarPlacement")}
1636
+ />
1637
+ <IconGroup
1638
+ label="Select all"
1639
+ options={Object.values(SelectAllScope)}
1640
+ value={getOverride("selectAllScope")}
1641
+ onChange={(v) => set("selectAllScope", v)}
1642
+ icon={selectAllScopeIcon}
1643
+ styles={styles}
1644
+ isDark={isDark}
1645
+ pinned={pinnedKnobs.has("selectAllScope")}
1646
+ onTogglePin={() => togglePin("selectAllScope")}
1647
+ onUnset={() => unset("selectAllScope")}
1648
+ />
1649
+ <IconGroup
1650
+ label="Timestamp"
1651
+ options={Object.values(TimestampStyle)}
1652
+ value={getOverride("timestampStyle")}
1653
+ onChange={(v) => set("timestampStyle", v)}
1654
+ icon={timestampStyleIcon}
1655
+ styles={styles}
1656
+ isDark={isDark}
1657
+ pinned={pinnedKnobs.has("timestampStyle")}
1658
+ onTogglePin={() => togglePin("timestampStyle")}
1659
+ onUnset={() => unset("timestampStyle")}
1660
+ />
1661
+ <IconGroup
1662
+ label="Disabled"
1663
+ options={Object.values(DisabledStyle)}
1664
+ value={getOverride("disabledStyle")}
1665
+ onChange={(v) => set("disabledStyle", v)}
1666
+ icon={disabledStyleIcon}
1667
+ styles={styles}
1668
+ isDark={isDark}
1669
+ pinned={pinnedKnobs.has("disabledStyle")}
1670
+ onTogglePin={() => togglePin("disabledStyle")}
1671
+ onUnset={() => unset("disabledStyle")}
1672
+ />
1673
+ <IconGroup
1674
+ label="Autofocus"
1675
+ options={Object.values(FormAutofocus)}
1676
+ value={getOverride("formAutofocus")}
1677
+ onChange={(v) => set("formAutofocus", v)}
1678
+ icon={formAutofocusIcon}
1679
+ styles={styles}
1680
+ isDark={isDark}
1681
+ pinned={pinnedKnobs.has("formAutofocus")}
1682
+ onTogglePin={() => togglePin("formAutofocus")}
1683
+ onUnset={() => unset("formAutofocus")}
1684
+ />
1491
1685
  <div
1492
1686
  style={{
1493
1687
  ...styles.card,
@@ -3,14 +3,25 @@ import { semanticGroups } from "../colorRules";
3
3
  const knobKeys = [
4
4
  "fillStyle",
5
5
  "borderRadius",
6
+ "cornerSmoothing",
6
7
  "borderWidth",
7
8
  "elevation",
8
9
  "space",
10
+ "size",
11
+ "density",
9
12
  "textAccent",
10
13
  "headingFont",
11
14
  "bodyFont",
12
15
  "fontWeight",
13
16
  "animation",
17
+ "fieldLabelPlacement",
18
+ "requiredMarking",
19
+ "tableZebra",
20
+ "bulkBarPlacement",
21
+ "selectAllScope",
22
+ "timestampStyle",
23
+ "disabledStyle",
24
+ "formAutofocus",
14
25
  ];
15
26
 
16
27
  const stateKnobKeys = ["elevation", "borderWidth", "borderRadius"] as const;