@multiplatform.one/theme 7.90.5 → 8.0.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 (186) hide show
  1. package/dist/cjs/index.gnome.cjs +432 -0
  2. package/dist/cjs/index.gnome.native.js +435 -0
  3. package/dist/cjs/index.gnome.native.js.map +1 -0
  4. package/dist/cjs/{theme/useColorScale.native.js → nativeTwins.cjs} +11 -10
  5. package/dist/cjs/{theme/useColorScale.cjs → nativeTwins.native.js} +14 -24
  6. package/dist/cjs/nativeTwins.native.js.map +1 -0
  7. package/dist/cjs/theme/Surface.cjs +2 -10
  8. package/dist/cjs/theme/Surface.native.js +3 -12
  9. package/dist/cjs/theme/Surface.native.js.map +1 -1
  10. package/dist/cjs/theme/devtools/ColorLineVisualizer.cjs +91 -89
  11. package/dist/cjs/theme/devtools/ColorLineVisualizer.native.js +79 -88
  12. package/dist/cjs/theme/devtools/ColorLineVisualizer.native.js.map +1 -1
  13. package/dist/cjs/theme/devtools/ThemeDevtoolsPanel.cjs +1 -1
  14. package/dist/cjs/theme/devtools/ThemeDevtoolsPanel.native.js +1 -1
  15. package/dist/cjs/theme/devtools/index.cjs +25 -0
  16. package/dist/cjs/theme/devtools/index.native.js +28 -0
  17. package/dist/cjs/theme/devtools/index.native.js.map +1 -0
  18. package/dist/cjs/theme/devtools/knobIcons.cjs +147 -149
  19. package/dist/cjs/theme/devtools/knobIcons.native.js +135 -149
  20. package/dist/cjs/theme/devtools/knobIcons.native.js.map +1 -1
  21. package/dist/cjs/theme/nativePresetSwitch.cjs +45 -0
  22. package/dist/cjs/theme/nativePresetSwitch.native.js +50 -0
  23. package/dist/cjs/theme/nativePresetSwitch.native.js.map +1 -0
  24. package/dist/cjs/theme/nativeSchemePreference.cjs +72 -0
  25. package/dist/cjs/theme/nativeSchemePreference.native.js +80 -0
  26. package/dist/cjs/theme/nativeSchemePreference.native.js.map +1 -0
  27. package/dist/cjs/theme/nativeTheme.cjs +107 -0
  28. package/dist/cjs/theme/nativeTheme.native.js +115 -0
  29. package/dist/cjs/theme/nativeTheme.native.js.map +1 -0
  30. package/dist/cjs/theme/nativeTransitionProps.cjs +69 -0
  31. package/dist/cjs/theme/nativeTransitionProps.native.js +81 -0
  32. package/dist/cjs/theme/nativeTransitionProps.native.js.map +1 -0
  33. package/dist/cjs/theme/recipeInputs.native.js.map +1 -1
  34. package/dist/cjs/theme/resolveKnobs.cjs +0 -5
  35. package/dist/cjs/theme/resolveKnobs.native.js +0 -5
  36. package/dist/cjs/theme/resolveKnobs.native.js.map +1 -1
  37. package/dist/cjs/theme/sizeRecipes.cjs +3 -13
  38. package/dist/cjs/theme/sizeRecipes.native.js +3 -13
  39. package/dist/cjs/theme/sizeRecipes.native.js.map +1 -1
  40. package/dist/cjs/theme/theme.native.js +2 -94
  41. package/dist/cjs/theme/theme.native.js.map +1 -1
  42. package/dist/cjs/theme/transitionProps.native.js +2 -60
  43. package/dist/cjs/theme/transitionProps.native.js.map +1 -1
  44. package/dist/cjs/theme/usePresetSwitch.native.js +2 -29
  45. package/dist/cjs/theme/usePresetSwitch.native.js.map +1 -1
  46. package/dist/cjs/theme/useSchemePreference.native.js +2 -59
  47. package/dist/cjs/theme/useSchemePreference.native.js.map +1 -1
  48. package/dist/esm/index.gnome.mjs +48 -0
  49. package/dist/esm/index.gnome.mjs.map +1 -0
  50. package/dist/esm/index.gnome.native.js +48 -0
  51. package/dist/esm/index.gnome.native.js.map +1 -0
  52. package/dist/esm/nativeTwins.mjs +6 -0
  53. package/dist/esm/nativeTwins.mjs.map +1 -0
  54. package/dist/esm/nativeTwins.native.js +6 -0
  55. package/dist/esm/nativeTwins.native.js.map +1 -0
  56. package/dist/esm/theme/Surface.mjs +2 -9
  57. package/dist/esm/theme/Surface.mjs.map +1 -1
  58. package/dist/esm/theme/Surface.native.js +3 -11
  59. package/dist/esm/theme/Surface.native.js.map +1 -1
  60. package/dist/esm/theme/devtools/ColorLineVisualizer.mjs +80 -89
  61. package/dist/esm/theme/devtools/ColorLineVisualizer.mjs.map +1 -1
  62. package/dist/esm/theme/devtools/ColorLineVisualizer.native.js +79 -88
  63. package/dist/esm/theme/devtools/ColorLineVisualizer.native.js.map +1 -1
  64. package/dist/esm/theme/devtools/ThemeDevtoolsPanel.mjs +1 -1
  65. package/dist/esm/theme/devtools/ThemeDevtoolsPanel.mjs.map +1 -1
  66. package/dist/esm/theme/devtools/ThemeDevtoolsPanel.native.js +1 -1
  67. package/dist/esm/theme/devtools/ThemeDevtoolsPanel.native.js.map +1 -1
  68. package/dist/esm/theme/devtools/index.mjs +7 -0
  69. package/dist/esm/theme/devtools/index.mjs.map +1 -0
  70. package/dist/esm/theme/devtools/index.native.js +7 -0
  71. package/dist/esm/theme/devtools/index.native.js.map +1 -0
  72. package/dist/esm/theme/devtools/knobIcons.mjs +136 -149
  73. package/dist/esm/theme/devtools/knobIcons.mjs.map +1 -1
  74. package/dist/esm/theme/devtools/knobIcons.native.js +135 -149
  75. package/dist/esm/theme/devtools/knobIcons.native.js.map +1 -1
  76. package/dist/esm/theme/nativePresetSwitch.mjs +20 -0
  77. package/dist/esm/theme/nativePresetSwitch.mjs.map +1 -0
  78. package/dist/esm/theme/nativePresetSwitch.native.js +22 -0
  79. package/dist/esm/theme/nativePresetSwitch.native.js.map +1 -0
  80. package/dist/esm/theme/nativeSchemePreference.mjs +47 -0
  81. package/dist/esm/theme/nativeSchemePreference.mjs.map +1 -0
  82. package/dist/esm/theme/nativeSchemePreference.native.js +52 -0
  83. package/dist/esm/theme/nativeSchemePreference.native.js.map +1 -0
  84. package/dist/esm/theme/nativeTheme.mjs +82 -0
  85. package/dist/esm/theme/nativeTheme.mjs.map +1 -0
  86. package/dist/esm/theme/nativeTheme.native.js +87 -0
  87. package/dist/esm/theme/nativeTheme.native.js.map +1 -0
  88. package/dist/esm/theme/nativeTransitionProps.mjs +44 -0
  89. package/dist/esm/theme/nativeTransitionProps.mjs.map +1 -0
  90. package/dist/esm/theme/nativeTransitionProps.native.js +53 -0
  91. package/dist/esm/theme/nativeTransitionProps.native.js.map +1 -0
  92. package/dist/esm/theme/recipeInputs.mjs.map +1 -1
  93. package/dist/esm/theme/recipeInputs.native.js.map +1 -1
  94. package/dist/esm/theme/resolveKnobs.mjs +0 -5
  95. package/dist/esm/theme/resolveKnobs.mjs.map +1 -1
  96. package/dist/esm/theme/resolveKnobs.native.js +0 -5
  97. package/dist/esm/theme/resolveKnobs.native.js.map +1 -1
  98. package/dist/esm/theme/sizeRecipes.mjs +4 -9
  99. package/dist/esm/theme/sizeRecipes.mjs.map +1 -1
  100. package/dist/esm/theme/sizeRecipes.native.js +4 -9
  101. package/dist/esm/theme/sizeRecipes.native.js.map +1 -1
  102. package/dist/esm/theme/theme.native.js +1 -86
  103. package/dist/esm/theme/theme.native.js.map +1 -1
  104. package/dist/esm/theme/transitionProps.native.js +1 -52
  105. package/dist/esm/theme/transitionProps.native.js.map +1 -1
  106. package/dist/esm/theme/usePresetSwitch.native.js +1 -21
  107. package/dist/esm/theme/usePresetSwitch.native.js.map +1 -1
  108. package/dist/esm/theme/useSchemePreference.native.js +1 -51
  109. package/dist/esm/theme/useSchemePreference.native.js.map +1 -1
  110. package/package.json +54 -6
  111. package/src/index.gnome.ts +544 -0
  112. package/src/nativeTwins.ts +11 -0
  113. package/src/theme/Surface.spec.tsx +0 -34
  114. package/src/theme/Surface.tsx +1 -20
  115. package/src/theme/devtools/ColorLineVisualizer.tsx +1 -0
  116. package/src/theme/devtools/ThemeDevtoolsPanel.tsx +1 -1
  117. package/src/theme/devtools/index.ts +14 -0
  118. package/src/theme/devtools/knobIcons.tsx +1 -0
  119. package/src/theme/mpo19Proof.spec.ts +0 -5
  120. package/src/theme/nativePresetSwitch.ts +32 -0
  121. package/src/theme/nativeSchemePreference.ts +58 -0
  122. package/src/theme/nativeTheme.tsx +113 -0
  123. package/src/theme/nativeTransitionProps.ts +74 -0
  124. package/src/theme/recipeInputs.ts +0 -5
  125. package/src/theme/recipes.ts +1 -7
  126. package/src/theme/resolveKnobs.spec.ts +4 -7
  127. package/src/theme/resolveKnobs.ts +0 -5
  128. package/src/theme/shared.tsx +4 -4
  129. package/src/theme/sizeLadder.spec.ts +4 -5
  130. package/src/theme/sizeRecipes.spec.ts +11 -16
  131. package/src/theme/sizeRecipes.ts +12 -19
  132. package/src/theme/theme.native.tsx +1 -113
  133. package/src/theme/theme.provider.spec.ts +1 -1
  134. package/src/theme/transitionProps.native.ts +1 -74
  135. package/src/theme/usePresetSwitch.native.ts +1 -32
  136. package/src/theme/useResolvedKnobs.hydration.spec.tsx +1 -1
  137. package/src/theme/useResolvedKnobsBehavior.spec.tsx +3 -3
  138. package/src/theme/useSchemePreference.native.ts +1 -58
  139. package/types/index.gnome.d.ts +84 -0
  140. package/types/index.gnome.d.ts.map +1 -0
  141. package/types/nativeTwins.d.ts +11 -0
  142. package/types/nativeTwins.d.ts.map +1 -0
  143. package/types/theme/Surface.d.ts +2 -13
  144. package/types/theme/Surface.d.ts.map +1 -1
  145. package/types/theme/devtools/ColorLineVisualizer.d.ts +3 -1
  146. package/types/theme/devtools/ColorLineVisualizer.d.ts.map +1 -1
  147. package/types/theme/devtools/index.d.ts +14 -0
  148. package/types/theme/devtools/index.d.ts.map +1 -0
  149. package/types/theme/devtools/knobIcons.d.ts +1 -0
  150. package/types/theme/devtools/knobIcons.d.ts.map +1 -1
  151. package/types/theme/nativePresetSwitch.d.ts +10 -0
  152. package/types/theme/nativePresetSwitch.d.ts.map +1 -0
  153. package/types/theme/nativeSchemePreference.d.ts +8 -0
  154. package/types/theme/nativeSchemePreference.d.ts.map +1 -0
  155. package/types/theme/nativeTheme.d.ts +19 -0
  156. package/types/theme/nativeTheme.d.ts.map +1 -0
  157. package/types/theme/nativeTransitionProps.d.ts +15 -0
  158. package/types/theme/nativeTransitionProps.d.ts.map +1 -0
  159. package/types/theme/recipeInputs.d.ts +0 -4
  160. package/types/theme/recipeInputs.d.ts.map +1 -1
  161. package/types/theme/recipes.d.ts +2 -9
  162. package/types/theme/recipes.d.ts.map +1 -1
  163. package/types/theme/resolveKnobs.d.ts.map +1 -1
  164. package/types/theme/shared.d.ts +4 -4
  165. package/types/theme/shared.d.ts.map +1 -1
  166. package/types/theme/sizeRecipes.d.ts +8 -13
  167. package/types/theme/sizeRecipes.d.ts.map +1 -1
  168. package/types/theme/theme.native.d.ts +1 -18
  169. package/types/theme/theme.native.d.ts.map +1 -1
  170. package/types/theme/transitionProps.native.d.ts +1 -14
  171. package/types/theme/transitionProps.native.d.ts.map +1 -1
  172. package/types/theme/usePresetSwitch.native.d.ts +1 -9
  173. package/types/theme/usePresetSwitch.native.d.ts.map +1 -1
  174. package/types/theme/useSchemePreference.native.d.ts +1 -7
  175. package/types/theme/useSchemePreference.native.d.ts.map +1 -1
  176. package/dist/cjs/theme/useColorScale.native.js.map +0 -1
  177. package/dist/esm/theme/useColorScale.mjs +0 -22
  178. package/dist/esm/theme/useColorScale.mjs.map +0 -1
  179. package/dist/esm/theme/useColorScale.native.js +0 -5
  180. package/dist/esm/theme/useColorScale.native.js.map +0 -1
  181. package/src/theme/useColorScale.native.ts +0 -7
  182. package/src/theme/useColorScale.ts +0 -29
  183. package/types/theme/useColorScale.d.ts +0 -7
  184. package/types/theme/useColorScale.d.ts.map +0 -1
  185. package/types/theme/useColorScale.native.d.ts +0 -6
  186. package/types/theme/useColorScale.native.d.ts.map +0 -1
@@ -22,11 +22,6 @@ export type SurfaceSize = SizeTokens | "unset";
22
22
  * is part of the field: a bare tinted glyph, never glass or a plate, even when
23
23
  * the field sits in a bar (LC-176, FZ-8711). Any explicit `painted` resets it,
24
24
  * so a menu opened from that glyph is content again.
25
- *
26
- * `topBar`: the bar is the page's top bar (a navigation bar, a page header's
27
- * actions), not an overlay's header, so a confirm there is a bar glyph like
28
- * back and "…", never the overlay's filled ✓ (LC-176, FZ-8989). Any explicit
29
- * `bar` or `painted` resets it, so a sheet's own header bar is not a top bar.
30
25
  */
31
26
  export type SurfaceIntent = {
32
27
  size: SurfaceSize;
@@ -34,7 +29,6 @@ export type SurfaceIntent = {
34
29
  bar: boolean;
35
30
  elevated: boolean;
36
31
  field: boolean;
37
- topBar: boolean;
38
32
  };
39
33
 
40
34
  export const SurfaceContext = createStyledContext<SurfaceIntent>({
@@ -43,7 +37,6 @@ export const SurfaceContext = createStyledContext<SurfaceIntent>({
43
37
  bar: false,
44
38
  elevated: false,
45
39
  field: false,
46
- topBar: false,
47
40
  });
48
41
 
49
42
  /** True inside a bar (LC-153): neutral buttons there are plain, not tinted. */
@@ -56,11 +49,6 @@ export function useFieldSurface(): boolean {
56
49
  return SurfaceContext.useStyledContext().field ?? false;
57
50
  }
58
51
 
59
- /** True inside the page's top bar (LC-176): a confirm there is a bar glyph, not the overlay's ✓ plate. */
60
- export function useTopBarSurface(): boolean {
61
- return SurfaceContext.useStyledContext().topBar ?? false;
62
- }
63
-
64
52
  /** True on an elevated ground (an overlay frame, an unnested Panel): controls step their wash up in dark. */
65
53
  export function useElevatedSurface(): boolean {
66
54
  return SurfaceContext.useStyledContext().elevated ?? false;
@@ -145,9 +133,7 @@ export function clampSurfaceSize(requested: SurfaceSize, parent: SurfaceSize): S
145
133
  * inherits it. `bar` marks a bar subtree (LC-153); omitted, it inherits unless
146
134
  * `painted` is given, which resets it. `elevated` marks a frame that paints
147
135
  * the elevated ground; omitted it inherits unless `painted` is given, which
148
- * resets it. `field` marks a text field's frame the same way. `topBar` marks
149
- * the page's top bar; omitted it inherits unless `bar` or `painted` is given,
150
- * which resets it. Either way a
136
+ * resets it. `field` marks a text field's frame the same way. Either way a
151
137
  * painted value opens a new surface with its own
152
138
  * inset, so the page gutter does not reach inside it (LC-146).
153
139
  *
@@ -160,7 +146,6 @@ export function Surface({
160
146
  bar,
161
147
  elevated,
162
148
  field,
163
- topBar,
164
149
  children,
165
150
  }: {
166
151
  size?: SizeTokens;
@@ -168,7 +153,6 @@ export function Surface({
168
153
  bar?: boolean;
169
154
  elevated?: boolean;
170
155
  field?: boolean;
171
- topBar?: boolean;
172
156
  children?: ReactNode;
173
157
  }) {
174
158
  const parent = SurfaceContext.useStyledContext();
@@ -177,8 +161,6 @@ export function Surface({
177
161
  const nextBar = bar ?? (painted === undefined ? (parent.bar ?? false) : false);
178
162
  const nextElevated = elevated ?? (painted === undefined ? (parent.elevated ?? false) : false);
179
163
  const nextField = field ?? (painted === undefined ? (parent.field ?? false) : false);
180
- const nextTopBar =
181
- topBar ?? (painted === undefined && bar === undefined ? (parent.topBar ?? false) : false);
182
164
  return (
183
165
  <SurfaceContext.Provider
184
166
  size={nextSize}
@@ -186,7 +168,6 @@ export function Surface({
186
168
  bar={nextBar}
187
169
  elevated={nextElevated}
188
170
  field={nextField}
189
- topBar={nextTopBar}
190
171
  >
191
172
  {painted === undefined ? children : <PageGutter inset={null}>{children}</PageGutter>}
192
173
  </SurfaceContext.Provider>
@@ -1,3 +1,4 @@
1
+ /** @jsxRuntime classic */
1
2
  /**
2
3
  * Color palette visualizer for devtools.
3
4
  *
@@ -372,7 +372,7 @@ function PinCheckbox({
372
372
  }) {
373
373
  return (
374
374
  <label
375
- title={pinned ? "Pinned — random won't change this" : "Pin to protect from random"}
375
+ title={pinned ? "Pinned. Random won't change this" : "Pin to protect from random"}
376
376
  style={{
377
377
  position: "absolute" as const,
378
378
  top: 3,
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The theme devtools pieces that load without tamagui or React Native: knob
3
+ * definitions, knob icons, the snapshot builder and the colour-line
4
+ * visualizer. Exported as `@multiplatform.one/theme/devtools` for hosts such
5
+ * as the Storybook manager frame, which renders outside TamaguiProvider.
6
+ * ThemeDevtoolsPanel needs tamagui, so it stays on the package root.
7
+ */
8
+
9
+ export * from "../knobs";
10
+ export * from "../motionStops";
11
+ export * from "./knobIcons";
12
+ export * from "./buildSnapshot";
13
+ export * from "./ColorLineVisualizer";
14
+ export * from "./vizConstants";
@@ -1,3 +1,4 @@
1
+ /** @jsxRuntime classic */
1
2
  import React, { type ReactNode } from "react";
2
3
 
3
4
  /**
@@ -279,9 +279,4 @@ describe("MPO-19 X12 — the nested radius is named for what it resolves to", ()
279
279
  expect(nested, `borderRadius: ${knob}`).toBeLessThanOrEqual(outer);
280
280
  }
281
281
  });
282
-
283
- it("the old name is the SAME object, so the two cannot drift while both exist", () => {
284
- const props = knobProps();
285
- expect(props.borderRadiusOuter).toBe(props.borderRadiusNested);
286
- });
287
282
  });
@@ -0,0 +1,32 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ import { usePresetContext } from "./PresetContext";
3
+ import {
4
+ getStoredPreset,
5
+ recordPresetChoice,
6
+ setStoredPreset,
7
+ subscribeStoredPreset,
8
+ } from "./presetStore";
9
+
10
+ export interface PresetSwitch {
11
+ /** The provider's active preset; "default" when none is stored. */
12
+ presetName: string;
13
+ /** Switch the active preset without recording anyone's choice. */
14
+ applyPreset(name: string): void;
15
+ /** Switch and record it as this user's choice (no user = this device's signed-out choice). */
16
+ choosePreset(name: string, userId?: string | null): void;
17
+ }
18
+
19
+ export function usePresetSwitch(): PresetSwitch {
20
+ const context = usePresetContext();
21
+ const stored = useSyncExternalStore(subscribeStoredPreset, getStoredPreset, getStoredPreset);
22
+ const presetName = context?.presetName || stored.presetName || "default";
23
+ const applyPreset = useCallback((name: string) => setStoredPreset(name), []);
24
+ const choosePreset = useCallback(
25
+ (name: string, userId?: string | null) => {
26
+ recordPresetChoice(name, userId);
27
+ applyPreset(name);
28
+ },
29
+ [applyPreset],
30
+ );
31
+ return { presetName, applyPreset, choosePreset };
32
+ }
@@ -0,0 +1,58 @@
1
+ import { useEffect, useSyncExternalStore } from "react";
2
+ import { Appearance } from "react-native";
3
+ import { useDidFinishSSR } from "tamagui";
4
+ import { isWeb } from "@multiplatform.one/platform";
5
+ import { getStoredScheme, setStoredScheme, subscribeStoredScheme } from "./schemeStore";
6
+ import type { ThemeScheme } from "./useTheme";
7
+
8
+ export interface SchemePreference {
9
+ setting: ThemeScheme;
10
+ value: "light" | "dark";
11
+ set(scheme: ThemeScheme): void;
12
+ }
13
+ const serverScheme = (): ThemeScheme => "system";
14
+
15
+ // RN's Appearance.setColorScheme("unspecified") writes "unspecified" into the
16
+ // JS cache that getColorScheme/useColorScheme read, with no change event, so
17
+ // System resolved light until the OS flipped (FZ-8708). The native scheme is
18
+ // read once before the first override and then only from change events.
19
+ let nativeScheme: "light" | "dark" | undefined;
20
+ const nativeListeners = new Set<() => void>();
21
+
22
+ function getNativeScheme(): "light" | "dark" {
23
+ if (nativeScheme === undefined) {
24
+ nativeScheme = Appearance.getColorScheme() === "dark" ? "dark" : "light";
25
+ Appearance.addChangeListener(({ colorScheme }) => {
26
+ if ((colorScheme !== "light" && colorScheme !== "dark") || colorScheme === nativeScheme) {
27
+ return;
28
+ }
29
+ nativeScheme = colorScheme;
30
+ nativeListeners.forEach((listener) => listener());
31
+ });
32
+ }
33
+ return nativeScheme;
34
+ }
35
+
36
+ function subscribeNativeScheme(listener: () => void) {
37
+ getNativeScheme();
38
+ nativeListeners.add(listener);
39
+ return () => {
40
+ nativeListeners.delete(listener);
41
+ };
42
+ }
43
+
44
+ export function useSchemePreference(): SchemePreference {
45
+ const setting = useSyncExternalStore(subscribeStoredScheme, getStoredScheme, serverScheme);
46
+ const os = useSyncExternalStore(subscribeNativeScheme, getNativeScheme, getNativeScheme);
47
+ const hydrated = useDidFinishSSR();
48
+ useEffect(() => {
49
+ if (isWeb) return;
50
+ getNativeScheme();
51
+ Appearance.setColorScheme(setting === "system" ? "unspecified" : setting);
52
+ }, [setting]);
53
+ return {
54
+ setting: hydrated ? setting : "system",
55
+ value: !hydrated ? "light" : setting === "system" ? os : setting,
56
+ set: setStoredScheme,
57
+ };
58
+ }
@@ -0,0 +1,113 @@
1
+ import { ToastProvider } from "@tamagui/toast";
2
+ import { Fragment, useContext, useMemo, useSyncExternalStore } from "react";
3
+ import {
4
+ type CreateTamaguiProps,
5
+ TamaguiProvider,
6
+ type TamaguiProviderProps,
7
+ Theme,
8
+ } from "tamagui";
9
+ import type { PropsWithChildren } from "react";
10
+ import { CornerSmoothingStyles } from "./cornerSmoothing";
11
+ import { FontKnobStyles } from "./FontKnobStyles";
12
+ import type { Knobs } from "./knobs";
13
+ import { PresetContext, type PresetContextValue } from "./PresetContext";
14
+ import { getStoredPreset, subscribeStoredPreset } from "./presetStore";
15
+ import { defaultPreset } from "./presets";
16
+ import { resolveThemeScale, useActiveScale } from "./scale/useActiveScale";
17
+ import { type ThemeConfig, getPreset } from "./shared";
18
+ import { useSchemePreference } from "./useSchemePreference";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // ThemeProviderProps (simplified — knob-based theming)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface ThemeProviderProps<
25
+ T extends CreateTamaguiProps = CreateTamaguiProps,
26
+ > extends PropsWithChildren<Omit<TamaguiProviderProps, "config" | "defaultTheme">> {
27
+ config?: {
28
+ tamagui: ReturnType<typeof import("tamagui").createTamagui<T>>;
29
+ } & Partial<Pick<ThemeConfig, "scales" | "defaultPreset">>;
30
+ systemTheme?: "light" | "dark";
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // ThemeProvider (simplified — TamaguiProvider + scheme + ToastProvider)
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Native resolves token values (`.val`) at render and only theme subscribers
39
+ * re-render on a config change, so a preset whose scale differs remounts the
40
+ * subtree under the provider (keyed on the scale id). That is a settings-level
41
+ * action: local component state under the provider does not survive it.
42
+ * The preset persists in `@multiplatform.one/store` storage (`mp.preset`);
43
+ * first render waits for storage so a relaunch paints the stored scale.
44
+ */
45
+ export function ThemeProvider<T extends CreateTamaguiProps>({
46
+ children,
47
+ config,
48
+ systemTheme,
49
+ ...tamaguiProps
50
+ }: ThemeProviderProps<T>) {
51
+ const scheme = useSchemePreference();
52
+ const activeTheme = systemTheme ?? scheme.value;
53
+ const ancestor = useContext(PresetContext);
54
+ const stored = useSyncExternalStore(subscribeStoredPreset, getStoredPreset, getStoredPreset);
55
+ const presetName = (ancestor ? ancestor.presetName : stored.presetName) ?? config?.defaultPreset;
56
+ const scaleId = useActiveScale(config, presetName);
57
+ const scaleConfig = resolveThemeScale(config, presetName)?.tamagui() ?? config?.tamagui;
58
+
59
+ return (
60
+ <TamaguiProvider
61
+ {...tamaguiProps}
62
+ config={scaleConfig as TamaguiProviderProps["config"]}
63
+ defaultTheme={activeTheme}
64
+ disableInjectCSS={false}
65
+ >
66
+ {ancestor || stored.hydrated ? (
67
+ <Fragment key={scaleId}>
68
+ <ToastProvider>
69
+ <KnobBridge presetName={presetName} overrides={stored.overrides} scaleId={scaleId}>
70
+ {children}
71
+ </KnobBridge>
72
+ </ToastProvider>
73
+ </Fragment>
74
+ ) : null}
75
+ </TamaguiProvider>
76
+ );
77
+ }
78
+
79
+ function KnobBridge({
80
+ children,
81
+ presetName,
82
+ overrides,
83
+ scaleId,
84
+ }: PropsWithChildren<{ presetName?: string; overrides: Partial<Knobs>; scaleId: string }>) {
85
+ const existingCtx = useContext(PresetContext);
86
+
87
+ const presetCtxValue = useMemo<PresetContextValue>(() => {
88
+ const base = (presetName ? getPreset(presetName) : undefined) ?? defaultPreset;
89
+ return {
90
+ preset: { ...base, knobs: { ...base.knobs, ...overrides } },
91
+ overrides,
92
+ presetName,
93
+ scaleId,
94
+ };
95
+ }, [presetName, overrides, scaleId]);
96
+
97
+ const tree = (
98
+ <>
99
+ <FontKnobStyles />
100
+ <CornerSmoothingStyles />
101
+ {children}
102
+ </>
103
+ );
104
+
105
+ const theme = (existingCtx ?? presetCtxValue).preset.theme;
106
+ const themedTree = theme ? <Theme name={theme}>{tree}</Theme> : tree;
107
+
108
+ if (existingCtx) {
109
+ return themedTree;
110
+ }
111
+
112
+ return <PresetContext.Provider value={presetCtxValue}>{themedTree}</PresetContext.Provider>;
113
+ }
@@ -0,0 +1,74 @@
1
+ import type { MotionRole } from "./motion";
2
+ import type { TransitionProp } from "tamagui";
3
+ import { animationConfig as nativeAnimationConfig } from "./animations/reactNative";
4
+
5
+ /**
6
+ * Native twin of `transitionProps.ts`.
7
+ *
8
+ * The React Native driver resolves an undefined `transition` to an empty
9
+ * config and animates with a default spring (LC-59), so keeping the key at
10
+ * animation `none` would put motion on every part that is meant to be still.
11
+ * Native keeps omitting it until the driver has an instant token to pass.
12
+ */
13
+ export function transitionProps(
14
+ role: MotionRole | TransitionProp | undefined,
15
+ extra: readonly string[] = [],
16
+ ): {
17
+ transition?: TransitionProp;
18
+ animateOnly?: string[];
19
+ } {
20
+ const isRole = isMotionRole(role);
21
+ const transition = isRole ? role.transition : role;
22
+ if (transition === undefined) return {};
23
+ if (!isRole) return { transition };
24
+ const properties = [...role.animateOnly, ...extra].flatMap(
25
+ (property) => nativeLonghands[property] ?? [property],
26
+ );
27
+ if (typeof transition !== "string") return { transition, animateOnly: properties };
28
+ const exit = typeof role.exit === "string" && role.exit !== transition ? role.exit : undefined;
29
+ const clamp = isSpring(transition) && !presetBounce.has(transition);
30
+ if (!exit && !clamp) return { transition, animateOnly: properties };
31
+ return {
32
+ transition: [
33
+ transition,
34
+ { ...(clamp ? { overshootClamping: true } : {}), ...(exit ? { exit } : {}) },
35
+ ],
36
+ animateOnly: properties,
37
+ };
38
+ }
39
+
40
+ function isSpring(stop: string): boolean {
41
+ const config = nativeAnimationConfig[stop as keyof typeof nativeAnimationConfig] as
42
+ | { type?: string }
43
+ | undefined;
44
+ return config !== undefined && config.type !== "timing";
45
+ }
46
+
47
+ /**
48
+ * FZ-7919: tamagui expands `borderColor` into its four sides before the React
49
+ * Native driver sees the style, and the driver matches `animateOnly` by exact
50
+ * key. A render therefore painted the sides as static, while a pseudo state
51
+ * (the bubbled focus of a field's text input) animated them. The animated
52
+ * copy outranked the static one and held the previous focus or theme colour.
53
+ */
54
+ const presetBounce = new Set(["bouncy", "superBouncy"]);
55
+
56
+ const nativeLonghands: Record<string, readonly string[]> = {
57
+ borderColor: [
58
+ "borderColor",
59
+ "borderTopColor",
60
+ "borderRightColor",
61
+ "borderBottomColor",
62
+ "borderLeftColor",
63
+ ],
64
+ };
65
+
66
+ function isMotionRole(value: MotionRole | TransitionProp | undefined): value is MotionRole {
67
+ return (
68
+ typeof value === "object" &&
69
+ value !== null &&
70
+ "transition" in value &&
71
+ "animateOnly" in value &&
72
+ Array.isArray(value.animateOnly)
73
+ );
74
+ }
@@ -32,8 +32,6 @@ export { SIZE_RECIPE_TOKENS, SIZE_TOKEN_EXPONENTS } from "./scale/scaleInputs";
32
32
  export type { SizeRecipeToken } from "./scale/scaleInputs";
33
33
 
34
34
  export type SizeRecipeFamilyName = "control" | "chip" | "menuRow" | "tab";
35
- /** @deprecated Use `SizeRecipeFamilyName`. */
36
- export type RecipeFamilyName = SizeRecipeFamilyName;
37
35
 
38
36
  export type SizeRecipeRatios = {
39
37
  padX: number;
@@ -99,9 +97,6 @@ export type SizeRecipeInputs = {
99
97
  tile?: Partial<TileSizeInputs>;
100
98
  };
101
99
 
102
- /** @deprecated Use `SizeRecipeInputs`. */
103
- export type RecipeInputs = SizeRecipeInputs;
104
-
105
100
  export type SizeRecipe = {
106
101
  height: number;
107
102
  paddingHorizontal: number;
@@ -4,7 +4,6 @@ import type {
4
4
  FontLetterSpacingTokens,
5
5
  FontLineHeightTokens,
6
6
  FontSizeTokens,
7
- TransitionProp,
8
7
  } from "tamagui";
9
8
  import type { Knobs } from "./knobs";
10
9
  import type { HitSlopInsets } from "./layoutTokens";
@@ -186,12 +185,9 @@ export interface KnobProps {
186
185
  *
187
186
  * MPO-19 X12 (00 L9c): this was `borderRadiusOuter`, which named the
188
187
  * opposite of what it resolves to — every one of its consumers uses it for
189
- * inner chrome, so the name told each of them the wrong story. Renamed;
190
- * `borderRadiusOuter` stays as a deprecated alias for one release.
188
+ * inner chrome, so the name told each of them the wrong story. Renamed.
191
189
  */
192
190
  borderRadiusNested: { borderRadius: string } & CornerSmoothingCarrier;
193
- /** @deprecated MPO-19 X12 — use `borderRadiusNested`. Removed after one release. */
194
- borderRadiusOuter: { borderRadius: string } & CornerSmoothingCarrier;
195
191
  /**
196
192
  * A FILLED control's border. Same width as the outlined one, so switching
197
193
  * fill style never moves a layout by 2px, but `transparent` — which is what
@@ -380,8 +376,6 @@ export interface KnobProps {
380
376
  };
381
377
  textWeight: { fontWeight: string };
382
378
  motion: Record<MotionRoleName, MotionRole>;
383
- /** @deprecated read a role from knobProps.motion */
384
- transition: TransitionProp | undefined;
385
379
  outlined: boolean;
386
380
  pointy: boolean;
387
381
  textAccent: string;
@@ -65,7 +65,6 @@ const defaultScaleTokens = generateScale(defaultScaleInputs).tokens;
65
65
  const expectedKnobPropKeys: (keyof KnobProps)[] = [
66
66
  "borderRadius",
67
67
  "borderRadiusNested",
68
- "borderRadiusOuter",
69
68
  "containerRadius",
70
69
  "fieldRadius",
71
70
  "elevation",
@@ -99,7 +98,6 @@ const expectedKnobPropKeys: (keyof KnobProps)[] = [
99
98
  "body",
100
99
  "typography",
101
100
  "textWeight",
102
- "transition",
103
101
  "outlined",
104
102
  "pointy",
105
103
  "textAccent",
@@ -1349,14 +1347,13 @@ describe("resolveKnobs", () => {
1349
1347
  // ── animation ─────────────────────────────────────────────
1350
1348
 
1351
1349
  describe("animation knob", () => {
1352
- it("resolves each role while keeping the deprecated control alias", () => {
1350
+ it("resolves each role", () => {
1353
1351
  const { knobProps } = resolveKnobs(knobsWith({ animation: "quick" }));
1354
1352
  expect(knobProps.motion.control.transition).toBe("quick");
1355
1353
  expect(knobProps.motion.overlay.transition).toBe("quicker");
1356
1354
  expect(knobProps.motion.scrim.transition).toBe("slow");
1357
1355
  expect(knobProps.motion.reveal.transition).toBe("medium");
1358
1356
  expect(knobProps.motion.slide.transition).toBe("medium");
1359
- expect(knobProps.transition).toBe(knobProps.motion.control.transition);
1360
1357
  for (const role of Object.values(
1361
1358
  resolveKnobs(knobsWith({ animation: "none" })).knobProps.motion,
1362
1359
  ))
@@ -1364,17 +1361,17 @@ describe("resolveKnobs", () => {
1364
1361
  });
1365
1362
  it('"none" → transition is undefined', () => {
1366
1363
  const { knobProps } = resolveKnobs(knobsWith({ animation: "none" }));
1367
- expect(knobProps.transition).toBeUndefined();
1364
+ expect(knobProps.motion.control.transition).toBeUndefined();
1368
1365
  });
1369
1366
 
1370
1367
  it('"bouncy" → transition is "bouncy"', () => {
1371
1368
  const { knobProps } = resolveKnobs(knobsWith({ animation: "bouncy" }));
1372
- expect(knobProps.transition).toBe("bouncy");
1369
+ expect(knobProps.motion.control.transition).toBe("bouncy");
1373
1370
  });
1374
1371
 
1375
1372
  it('"quick" → transition is "quick"', () => {
1376
1373
  const { knobProps } = resolveKnobs(knobsWith({ animation: "quick" }));
1377
- expect(knobProps.transition).toBe("quick");
1374
+ expect(knobProps.motion.control.transition).toBe("quick");
1378
1375
  });
1379
1376
  });
1380
1377
 
@@ -869,10 +869,6 @@ export function resolveKnobs(
869
869
  ...scaleSmoothing,
870
870
  },
871
871
  borderRadiusNested: nestedRadius,
872
- // MPO-19 X12: the old name for the same fragment, kept for one release so
873
- // an out-of-tree consumer does not break on the rename. Same object, so
874
- // the two cannot drift while both exist.
875
- borderRadiusOuter: nestedRadius,
876
872
  controlBorderFilled: { borderColor: "transparent", borderWidth: borderWidthValue },
877
873
  controlBorderOutlined: {
878
874
  borderColor: edge("$borderColor", Math.max(borderWidthValue, 1)),
@@ -992,7 +988,6 @@ export function resolveKnobs(
992
988
  },
993
989
  textWeight: { fontWeight: fontWeightValue },
994
990
  motion,
995
- transition: motion.control.transition,
996
991
  outlined,
997
992
  pointy: knobs.borderRadius === "none",
998
993
  textAccent: knobs.textAccent,
@@ -3,7 +3,7 @@ import { type CreateTamaguiProps, createTamagui } from "tamagui";
3
3
  import type { Knobs } from "./knobs";
4
4
  import { defaultKnobs } from "./knobs";
5
5
  import type { Preset } from "./preset.types";
6
- import type { RecipeFamilies, RecipeInputs } from "./recipeInputs";
6
+ import type { RecipeFamilies, SizeRecipeInputs } from "./recipeInputs";
7
7
  import type { ScaleFonts } from "./scale/generateScale";
8
8
  import { type ThemeScale, buildThemeScales } from "./scale/themeScales";
9
9
 
@@ -90,7 +90,7 @@ export type ThemeConfig<T extends CreateTamaguiProps = CreateTamaguiProps> = {
90
90
  * Size-recipe inputs + generated families (LC-70). Orthogonal to Tamagui
91
91
  * `themes` (those are color palettes from createThemes / theme-builder).
92
92
  */
93
- recipeInputs?: RecipeInputs;
93
+ recipeInputs?: SizeRecipeInputs;
94
94
  recipeFamilies?: RecipeFamilies;
95
95
  /**
96
96
  * The token scales this config can switch to at runtime, keyed by preset
@@ -106,7 +106,7 @@ export interface ThemeConfigScaleOptions {
106
106
  /** The font map a scale's type tables produce. */
107
107
  fonts: (tables: ScaleFonts) => Record<string, GenericFont>;
108
108
  /** Consumer recipe overrides that are not scale-derived (tile ladder, touch floor). */
109
- recipeOverrides?: Partial<RecipeInputs>;
109
+ recipeOverrides?: Partial<SizeRecipeInputs>;
110
110
  }
111
111
 
112
112
  /**
@@ -148,7 +148,7 @@ export function createThemeConfig<T extends CreateTamaguiProps>(
148
148
  options?: {
149
149
  presets?: Record<string, Preset>;
150
150
  defaultPreset?: string;
151
- recipeInputs?: RecipeInputs;
151
+ recipeInputs?: SizeRecipeInputs;
152
152
  recipeFamilies?: RecipeFamilies;
153
153
  scale?: ThemeConfigScaleOptions;
154
154
  },
@@ -3,7 +3,6 @@ import { defaultScaleInputs } from "./scale/scaleInputs";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import {
5
5
  DEFAULT_SIZE_RECIPE_INPUTS,
6
- SIZE_RECIPE_RADII,
7
6
  SIZE_RECIPE_RADIUS_STOPS,
8
7
  applyRadiusStop,
9
8
  generateSizeRecipes,
@@ -25,7 +24,7 @@ describe("MPO-46 size ladder — generated table, not a ratio (R6)", () => {
25
24
  expect(recipe.paddingHorizontal, `${token} padX`).toBe(row.paddingHorizontal);
26
25
  expect(recipe.fontSize, `${token} label`).toBe(row.fontSize);
27
26
  expect(recipe.iconSize, `${token} icon`).toBe(row.iconSize);
28
- expect(SIZE_RECIPE_RADII[token], `${token} radius`).toBe(row.radius);
27
+ expect(recipeFamilies.control.radii[token], `${token} radius`).toBe(row.radius);
29
28
  expect(DEFAULT_SIZE_RECIPE_INPUTS.tokenTable[token]).toEqual(row);
30
29
  }
31
30
  });
@@ -34,15 +33,15 @@ describe("MPO-46 size ladder — generated table, not a ratio (R6)", () => {
34
33
  const two = sizeRecipeForToken("$2");
35
34
  expect(two.paddingHorizontal).not.toBe(11);
36
35
  expect(two.fontSize).not.toBe(9);
37
- expect(SIZE_RECIPE_RADII.$2).not.toBe(9);
38
- expect(SIZE_RECIPE_RADII.$2).not.toBe(SIZE_RECIPE_RADII.$6);
36
+ expect(recipeFamilies.control.radii.$2).not.toBe(9);
37
+ expect(recipeFamilies.control.radii.$2).not.toBe(recipeFamilies.control.radii.$6);
39
38
  });
40
39
 
41
40
  it("Button / Select trigger / ListItem $4 is 40 / padX 16 / radius 8; Input padX is 14", () => {
42
41
  const button = sizeRecipes.$4;
43
42
  expect(button.height).toBe(40);
44
43
  expect(button.paddingHorizontal).toBe(16);
45
- expect(SIZE_RECIPE_RADII.$4).toBe(8);
44
+ expect(recipeFamilies.control.radii.$4).toBe(8);
46
45
  expect(DEFAULT_SIZE_RECIPE_INPUTS.inputPadX.$4).toBe(14);
47
46
  expect(recipeFamilies.chip.recipes.$4.paddingHorizontal).toBe(14);
48
47
  expect(recipeFamilies.chip.recipes.$4.height).toBe(40);