@tamagui/web 2.0.0-1768586279389 → 2.0.0-1768696252732

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 (216) hide show
  1. package/dist/cjs/animationDriverTypes.test-d.cjs +49 -0
  2. package/dist/cjs/animationDriverTypes.test-d.js +51 -0
  3. package/dist/cjs/animationDriverTypes.test-d.js.map +6 -0
  4. package/dist/cjs/animationDriverTypes.test-d.native.js +52 -0
  5. package/dist/cjs/animationDriverTypes.test-d.native.js.map +1 -0
  6. package/dist/cjs/config.cjs +14 -0
  7. package/dist/cjs/config.js +14 -0
  8. package/dist/cjs/config.js.map +1 -1
  9. package/dist/cjs/config.native.js +14 -0
  10. package/dist/cjs/config.native.js.map +1 -1
  11. package/dist/cjs/createComponent.cjs +39 -16
  12. package/dist/cjs/createComponent.js +35 -15
  13. package/dist/cjs/createComponent.js.map +1 -1
  14. package/dist/cjs/createComponent.native.js +48 -19
  15. package/dist/cjs/createComponent.native.js.map +1 -1
  16. package/dist/cjs/createTamagui.cjs +3 -2
  17. package/dist/cjs/createTamagui.js +3 -2
  18. package/dist/cjs/createTamagui.js.map +1 -1
  19. package/dist/cjs/createTamagui.native.js +1 -1
  20. package/dist/cjs/createTamagui.native.js.map +1 -1
  21. package/dist/cjs/helpers/getSplitStyles.cjs +8 -2
  22. package/dist/cjs/helpers/getSplitStyles.js +7 -2
  23. package/dist/cjs/helpers/getSplitStyles.js.map +1 -1
  24. package/dist/cjs/helpers/getSplitStyles.native.js +7 -1
  25. package/dist/cjs/helpers/getSplitStyles.native.js.map +1 -1
  26. package/dist/cjs/helpers/getThemeCSSRules.cjs +23 -26
  27. package/dist/cjs/helpers/getThemeCSSRules.js +2 -4
  28. package/dist/cjs/helpers/getThemeCSSRules.js.map +1 -1
  29. package/dist/cjs/{createTheme.cjs → helpers/mergeRenderElementProps.cjs} +11 -5
  30. package/dist/cjs/{createTheme.js → helpers/mergeRenderElementProps.js} +10 -6
  31. package/dist/cjs/helpers/mergeRenderElementProps.js.map +6 -0
  32. package/dist/cjs/{createTheme.native.js → helpers/mergeRenderElementProps.native.js} +12 -8
  33. package/dist/cjs/helpers/mergeRenderElementProps.native.js.map +1 -0
  34. package/dist/cjs/helpers/mergeSlotStyleProps.cjs +39 -0
  35. package/dist/cjs/helpers/mergeSlotStyleProps.js +29 -0
  36. package/dist/cjs/helpers/mergeSlotStyleProps.js.map +6 -0
  37. package/dist/cjs/helpers/mergeSlotStyleProps.native.js +42 -0
  38. package/dist/cjs/helpers/mergeSlotStyleProps.native.js.map +1 -0
  39. package/dist/cjs/helpers/skipProps.cjs +1 -1
  40. package/dist/cjs/helpers/skipProps.js +1 -1
  41. package/dist/cjs/helpers/skipProps.js.map +1 -1
  42. package/dist/cjs/helpers/skipProps.native.js +1 -1
  43. package/dist/cjs/helpers/skipProps.native.js.map +1 -1
  44. package/dist/cjs/helpers/useRenderElement.cjs +48 -0
  45. package/dist/cjs/helpers/useRenderElement.js +42 -0
  46. package/dist/cjs/helpers/useRenderElement.js.map +6 -0
  47. package/dist/cjs/helpers/useRenderElement.native.js +52 -0
  48. package/dist/cjs/helpers/useRenderElement.native.js.map +1 -0
  49. package/dist/cjs/hooks/getThemeProxied.js.map +1 -1
  50. package/dist/cjs/hooks/getThemeProxied.native.js +1 -1
  51. package/dist/cjs/hooks/getThemeProxied.native.js.map +1 -1
  52. package/dist/cjs/index.cjs +1 -0
  53. package/dist/cjs/index.js +1 -0
  54. package/dist/cjs/index.js.map +1 -1
  55. package/dist/cjs/index.native.js +1 -0
  56. package/dist/cjs/index.native.js.map +1 -1
  57. package/dist/cjs/styled.js.map +1 -1
  58. package/dist/cjs/styled.native.js.map +1 -1
  59. package/dist/cjs/views/Configuration.js.map +1 -1
  60. package/dist/cjs/views/Configuration.native.js.map +1 -1
  61. package/dist/cjs/views/Slot.cjs +7 -21
  62. package/dist/cjs/views/Slot.js +9 -13
  63. package/dist/cjs/views/Slot.js.map +1 -1
  64. package/dist/cjs/views/Slot.native.js +5 -21
  65. package/dist/cjs/views/Slot.native.js.map +1 -1
  66. package/dist/cjs/views/TamaguiProvider.cjs +6 -2
  67. package/dist/cjs/views/TamaguiProvider.js +5 -2
  68. package/dist/cjs/views/TamaguiProvider.js.map +1 -1
  69. package/dist/cjs/views/TamaguiProvider.native.js +5 -1
  70. package/dist/cjs/views/TamaguiProvider.native.js.map +1 -1
  71. package/dist/cjs/views/Theme.js.map +1 -1
  72. package/dist/cjs/views/Theme.native.js.map +1 -1
  73. package/dist/esm/animationDriverTypes.test-d.js +51 -0
  74. package/dist/esm/animationDriverTypes.test-d.js.map +6 -0
  75. package/dist/esm/animationDriverTypes.test-d.mjs +50 -0
  76. package/dist/esm/animationDriverTypes.test-d.mjs.map +1 -0
  77. package/dist/esm/animationDriverTypes.test-d.native.js +50 -0
  78. package/dist/esm/animationDriverTypes.test-d.native.js.map +1 -0
  79. package/dist/esm/config.js +14 -0
  80. package/dist/esm/config.js.map +1 -1
  81. package/dist/esm/config.mjs +14 -1
  82. package/dist/esm/config.mjs.map +1 -1
  83. package/dist/esm/config.native.js +14 -1
  84. package/dist/esm/config.native.js.map +1 -1
  85. package/dist/esm/createComponent.js +35 -15
  86. package/dist/esm/createComponent.js.map +1 -1
  87. package/dist/esm/createComponent.mjs +40 -17
  88. package/dist/esm/createComponent.mjs.map +1 -1
  89. package/dist/esm/createComponent.native.js +49 -20
  90. package/dist/esm/createComponent.native.js.map +1 -1
  91. package/dist/esm/createTamagui.js +3 -2
  92. package/dist/esm/createTamagui.js.map +1 -1
  93. package/dist/esm/createTamagui.mjs +3 -2
  94. package/dist/esm/createTamagui.mjs.map +1 -1
  95. package/dist/esm/createTamagui.native.js +1 -1
  96. package/dist/esm/createTamagui.native.js.map +1 -1
  97. package/dist/esm/helpers/getSplitStyles.js +7 -2
  98. package/dist/esm/helpers/getSplitStyles.js.map +1 -1
  99. package/dist/esm/helpers/getSplitStyles.mjs +8 -2
  100. package/dist/esm/helpers/getSplitStyles.mjs.map +1 -1
  101. package/dist/esm/helpers/getSplitStyles.native.js +7 -1
  102. package/dist/esm/helpers/getSplitStyles.native.js.map +1 -1
  103. package/dist/esm/helpers/getThemeCSSRules.js +2 -4
  104. package/dist/esm/helpers/getThemeCSSRules.js.map +1 -1
  105. package/dist/esm/helpers/getThemeCSSRules.mjs +23 -26
  106. package/dist/esm/helpers/getThemeCSSRules.mjs.map +1 -1
  107. package/dist/esm/helpers/mergeRenderElementProps.js +9 -0
  108. package/dist/esm/helpers/mergeRenderElementProps.js.map +6 -0
  109. package/dist/esm/helpers/mergeRenderElementProps.mjs +9 -0
  110. package/dist/esm/helpers/mergeRenderElementProps.mjs.map +1 -0
  111. package/dist/esm/helpers/mergeRenderElementProps.native.js +9 -0
  112. package/dist/esm/helpers/mergeRenderElementProps.native.js.map +1 -0
  113. package/dist/esm/helpers/mergeSlotStyleProps.js +14 -0
  114. package/dist/esm/helpers/mergeSlotStyleProps.js.map +6 -0
  115. package/dist/esm/helpers/mergeSlotStyleProps.mjs +16 -0
  116. package/dist/esm/helpers/mergeSlotStyleProps.mjs.map +1 -0
  117. package/dist/esm/helpers/mergeSlotStyleProps.native.js +16 -0
  118. package/dist/esm/helpers/mergeSlotStyleProps.native.js.map +1 -0
  119. package/dist/esm/helpers/skipProps.js +1 -1
  120. package/dist/esm/helpers/skipProps.js.map +1 -1
  121. package/dist/esm/helpers/skipProps.mjs +1 -1
  122. package/dist/esm/helpers/skipProps.mjs.map +1 -1
  123. package/dist/esm/helpers/skipProps.native.js +1 -1
  124. package/dist/esm/helpers/skipProps.native.js.map +1 -1
  125. package/dist/esm/helpers/useRenderElement.js +28 -0
  126. package/dist/esm/helpers/useRenderElement.js.map +6 -0
  127. package/dist/esm/helpers/useRenderElement.mjs +25 -0
  128. package/dist/esm/helpers/useRenderElement.mjs.map +1 -0
  129. package/dist/esm/helpers/useRenderElement.native.js +26 -0
  130. package/dist/esm/helpers/useRenderElement.native.js.map +1 -0
  131. package/dist/esm/hooks/getThemeProxied.js.map +1 -1
  132. package/dist/esm/hooks/getThemeProxied.mjs.map +1 -1
  133. package/dist/esm/hooks/getThemeProxied.native.js +1 -1
  134. package/dist/esm/hooks/getThemeProxied.native.js.map +1 -1
  135. package/dist/esm/index.js +2 -0
  136. package/dist/esm/index.js.map +1 -1
  137. package/dist/esm/index.mjs +2 -2
  138. package/dist/esm/index.mjs.map +1 -1
  139. package/dist/esm/index.native.js +2 -2
  140. package/dist/esm/index.native.js.map +1 -1
  141. package/dist/esm/styled.js.map +1 -1
  142. package/dist/esm/styled.mjs.map +1 -1
  143. package/dist/esm/styled.native.js.map +1 -1
  144. package/dist/esm/views/Configuration.js.map +1 -1
  145. package/dist/esm/views/Configuration.mjs.map +1 -1
  146. package/dist/esm/views/Configuration.native.js.map +1 -1
  147. package/dist/esm/views/Slot.js +10 -14
  148. package/dist/esm/views/Slot.js.map +1 -1
  149. package/dist/esm/views/Slot.mjs +7 -21
  150. package/dist/esm/views/Slot.mjs.map +1 -1
  151. package/dist/esm/views/Slot.native.js +7 -23
  152. package/dist/esm/views/Slot.native.js.map +1 -1
  153. package/dist/esm/views/TamaguiProvider.js +5 -2
  154. package/dist/esm/views/TamaguiProvider.js.map +1 -1
  155. package/dist/esm/views/TamaguiProvider.mjs +6 -2
  156. package/dist/esm/views/TamaguiProvider.mjs.map +1 -1
  157. package/dist/esm/views/TamaguiProvider.native.js +5 -1
  158. package/dist/esm/views/TamaguiProvider.native.js.map +1 -1
  159. package/dist/esm/views/Theme.js.map +1 -1
  160. package/dist/esm/views/Theme.mjs.map +1 -1
  161. package/dist/esm/views/Theme.native.js.map +1 -1
  162. package/package.json +12 -12
  163. package/src/animationDriverTypes.test-d.ts +256 -0
  164. package/src/config.ts +41 -0
  165. package/src/createComponent.tsx +69 -31
  166. package/src/createTamagui.ts +2 -2
  167. package/src/helpers/getSplitStyles.tsx +10 -3
  168. package/src/helpers/getThemeCSSRules.ts +2 -9
  169. package/src/helpers/mergeRenderElementProps.ts +17 -0
  170. package/src/helpers/mergeSlotStyleProps.ts +46 -0
  171. package/src/helpers/skipProps.ts +1 -1
  172. package/src/helpers/useRenderElement.tsx +66 -0
  173. package/src/hooks/getThemeProxied.ts +2 -1
  174. package/src/index.ts +1 -0
  175. package/src/interfaces/TamaguiComponentPropsBaseBase.tsx +19 -4
  176. package/src/styled.tsx +4 -3
  177. package/src/types.tsx +85 -25
  178. package/src/views/Configuration.tsx +0 -2
  179. package/src/views/Slot.tsx +14 -32
  180. package/src/views/TamaguiProvider.tsx +12 -1
  181. package/src/views/Theme.tsx +0 -6
  182. package/types/animationDriverTypes.type-test.d.ts +37 -0
  183. package/types/config.d.ts +15 -1
  184. package/types/config.d.ts.map +1 -1
  185. package/types/createComponent.d.ts.map +1 -1
  186. package/types/helpers/getSplitStyles.d.ts.map +1 -1
  187. package/types/helpers/getThemeCSSRules.d.ts.map +1 -1
  188. package/types/helpers/mergeRenderElementProps.d.ts +7 -0
  189. package/types/helpers/mergeRenderElementProps.d.ts.map +1 -0
  190. package/types/helpers/mergeSlotStyleProps.d.ts +10 -0
  191. package/types/helpers/mergeSlotStyleProps.d.ts.map +1 -0
  192. package/types/helpers/skipProps.d.ts +1 -1
  193. package/types/helpers/useRenderElement.d.ts +13 -0
  194. package/types/helpers/useRenderElement.d.ts.map +1 -0
  195. package/types/hooks/getThemeProxied.d.ts.map +1 -1
  196. package/types/index.d.ts +1 -1
  197. package/types/index.d.ts.map +1 -1
  198. package/types/interfaces/TamaguiComponentPropsBaseBase.d.ts +8 -4
  199. package/types/interfaces/TamaguiComponentPropsBaseBase.d.ts.map +1 -1
  200. package/types/styled.d.ts +1 -0
  201. package/types/styled.d.ts.map +1 -1
  202. package/types/types.d.ts +50 -22
  203. package/types/types.d.ts.map +1 -1
  204. package/types/views/Configuration.d.ts +0 -2
  205. package/types/views/Configuration.d.ts.map +1 -1
  206. package/types/views/Slot.d.ts.map +1 -1
  207. package/types/views/TamaguiProvider.d.ts.map +1 -1
  208. package/dist/cjs/createTheme.js.map +0 -6
  209. package/dist/cjs/createTheme.native.js.map +0 -1
  210. package/dist/esm/createTheme.js +0 -5
  211. package/dist/esm/createTheme.js.map +0 -6
  212. package/dist/esm/createTheme.mjs +0 -3
  213. package/dist/esm/createTheme.mjs.map +0 -1
  214. package/dist/esm/createTheme.native.js +0 -5
  215. package/dist/esm/createTheme.native.js.map +0 -1
  216. package/types/createTheme.d.ts.map +0 -1
@@ -1,7 +1,6 @@
1
1
  import { composeRefs } from '@tamagui/compose-refs'
2
2
  import {
3
3
  IS_REACT_19,
4
- isAndroid,
5
4
  isClient,
6
5
  isServer,
7
6
  isWeb,
@@ -19,6 +18,7 @@ import { defaultComponentStateMounted } from './defaultComponentState'
19
18
  import { getSplitStyles, useSplitStyles } from './helpers/getSplitStyles'
20
19
  import { log } from './helpers/log'
21
20
  import { type GenericProps, mergeComponentProps } from './helpers/mergeProps'
21
+ import { mergeRenderElementProps } from './helpers/mergeRenderElementProps'
22
22
  import { objectIdentityKey } from './helpers/objectIdentityKey'
23
23
  import { setElementProps } from './helpers/setElementProps'
24
24
  import { subscribeToContextGroup } from './helpers/subscribeToContextGroup'
@@ -415,7 +415,23 @@ export function createComponent<
415
415
  }
416
416
 
417
417
  const groupContextParent = React.useContext(GroupContext)
418
- const animationDriver = componentContext.animationDriver
418
+
419
+ // Get animation driver - either from animatedBy prop lookup or context
420
+ const animationDriver = (() => {
421
+ if (props.animatedBy && config?.animations) {
422
+ const animations = config.animations
423
+ // If animations is an object with named drivers (has 'default' key)
424
+ if ('default' in animations) {
425
+ return (
426
+ (animations as Record<string, any>)[props.animatedBy] ?? animations.default
427
+ )
428
+ }
429
+ // Single driver config - only 'default' makes sense
430
+ return props.animatedBy === 'default' ? animations : null
431
+ }
432
+ return componentContext.animationDriver
433
+ })()
434
+
419
435
  const useAnimations = animationDriver?.useAnimations as UseAnimationHook | undefined
420
436
 
421
437
  const componentState = useComponentState(
@@ -498,7 +514,7 @@ export function createComponent<
498
514
  }
499
515
  }, [stateRef, groupName, groupContextParent])
500
516
 
501
- // if our animation driver supports noReRender, we'll replace this below with
517
+ // if our animation driver supports avoidReRenders, we'll replace this below with
502
518
  // a version that essentially uses an internall emitter rather than setting state
503
519
  // but still stores the current state and applies if it it needs to during render
504
520
  let setStateShallow = componentState.setStateShallow
@@ -506,9 +522,9 @@ export function createComponent<
506
522
  if (process.env.NODE_ENV === 'development' && time) time`use-state`
507
523
 
508
524
  const isTaggable = !Component || typeof Component === 'string'
509
- const tagProp = props.tag
510
- // default to tag, fallback to component (when both strings)
511
- const element = isWeb ? (isTaggable ? tagProp || Component : Component) : Component
525
+ const renderProp = props.render
526
+ // default to render prop, fallback to component (when both strings)
527
+ const element = isWeb ? (isTaggable ? renderProp || Component : Component) : Component
512
528
 
513
529
  const BaseTextComponent = BaseText || element || 'span'
514
530
  const BaseViewComponent = BaseView || element || (hasTextAncestor ? 'span' : 'div')
@@ -627,7 +643,7 @@ export function createComponent<
627
643
 
628
644
  if (process.env.NODE_ENV === 'development' && time) time`theme`
629
645
 
630
- elementType = Component || elementType
646
+ elementType = element || elementType
631
647
  const isStringElement = typeof elementType === 'string'
632
648
 
633
649
  const mediaState = useMedia(componentContext, debugProp)
@@ -919,8 +935,8 @@ export function createComponent<
919
935
  }
920
936
  }
921
937
 
922
- if (tagProp && elementType['acceptTagProp']) {
923
- viewProps.tag = tagProp
938
+ if (renderProp && elementType['acceptTagProp']) {
939
+ viewProps.render = renderProp
924
940
  }
925
941
 
926
942
  // once you set animation prop don't remove it, you can set to undefined/false
@@ -950,6 +966,7 @@ export function createComponent<
950
966
  componentState: state,
951
967
  styleProps,
952
968
  theme,
969
+ themeName,
953
970
  pseudos: pseudos || null,
954
971
  staticConfig,
955
972
  stateRef,
@@ -1068,37 +1085,45 @@ export function createComponent<
1068
1085
  })
1069
1086
  }
1070
1087
 
1071
- // TODO should this be regular effect to allow the render in-between?
1088
+ // Animation enter state machine: true -> 'should-enter' -> false
1089
+ // Stage 1: Set 'should-enter' synchronously before paint to apply enterStyle classes
1090
+ // Stage 2: After browser paint, set false to trigger CSS transition
1091
+ //
1092
+ // CRITICAL: useEffect does NOT guarantee post-paint execution!
1093
+ // See: https://thoughtspile.github.io/2021/11/15/unintentional-layout-effect/
1094
+ // When layoutEffect updates state → re-render before paint → useEffect flushes pre-paint
1095
+ // Solution: Double RAF ensures browser has actually painted before we transition
1072
1096
  useIsomorphicLayoutEffect(() => {
1073
- // Note: We removed the early return on disabled to allow animations to work
1074
- // This fixes the issue where animations wouldn't work on disabled components
1075
1097
  if (state.unmounted === true && hasEnterStyle) {
1076
1098
  setStateShallow({ unmounted: 'should-enter' })
1077
1099
  return
1078
1100
  }
1079
1101
 
1080
- let tm: NodeJS.Timeout
1081
1102
  if (state.unmounted) {
1082
- if (animationDriver?.supportsCSS || isAndroid) {
1083
- // this setTimeout fixes css driver enter animations - not sure why
1084
- // this setTimeout fixes the conflict when with the safe area view in android
1085
- tm = setTimeout(() => {
1086
- setStateShallow({ unmounted: false })
1103
+ // For CSS transitions, we need browser to paint enterStyle before removing it.
1104
+ // Double RAF guarantees paint: first RAF schedules after current frame,
1105
+ // second RAF schedules after that frame completes (including paint).
1106
+ if (supportsCSS) {
1107
+ let cancelled = false
1108
+ requestAnimationFrame(() => {
1109
+ if (cancelled) return
1110
+ requestAnimationFrame(() => {
1111
+ if (cancelled) return
1112
+ setStateShallow({ unmounted: false })
1113
+ })
1087
1114
  })
1088
- return () => clearTimeout(tm)
1089
- // don't clearTimeout! safari gets bugs it just doesn't ever set unmounted: false
1115
+ return () => {
1116
+ cancelled = true
1117
+ }
1090
1118
  }
1091
-
1119
+ // Non-CSS drivers handle their own animation timing
1092
1120
  setStateShallow({ unmounted: false })
1093
- return
1094
1121
  }
1095
1122
 
1096
- // Only subscribe to context group if not disabled
1097
-
1098
1123
  return () => {
1099
1124
  componentSetStates.delete(setState)
1100
1125
  }
1101
- }, [state.unmounted, disabled])
1126
+ }, [state.unmounted, supportsCSS])
1102
1127
 
1103
1128
  useIsomorphicLayoutEffect(() => {
1104
1129
  if (disabled) return
@@ -1377,11 +1402,28 @@ export function createComponent<
1377
1402
  if (useChildrenResult) {
1378
1403
  content = useChildrenResult
1379
1404
  } else {
1380
- content = React.createElement(elementType, viewProps, content)
1405
+ // Handle render prop variants: function, JSX element, or string
1406
+ if (typeof renderProp === 'function') {
1407
+ // Render function: full control with props and state
1408
+ const renderProps = { ...viewProps, children: content }
1409
+ content = renderProp(renderProps, state)
1410
+ } else if (
1411
+ renderProp &&
1412
+ typeof renderProp === 'object' &&
1413
+ React.isValidElement(renderProp)
1414
+ ) {
1415
+ // JSX element: clone with merged props
1416
+ const elementProps = (renderProp as React.ReactElement).props || {}
1417
+ const mergedProps = mergeRenderElementProps(elementProps, viewProps, content)
1418
+ content = React.cloneElement(renderProp as React.ReactElement, mergedProps)
1419
+ } else {
1420
+ content = React.createElement(elementType, viewProps, content)
1421
+ }
1381
1422
  }
1382
1423
 
1383
1424
  // needs to reset the presence state for nested children
1384
- const ResetPresence = config?.animations?.ResetPresence
1425
+ // Use the resolved animationDriver (handles multi-driver config)
1426
+ const ResetPresence = animationDriver?.ResetPresence
1385
1427
  const needsReset = Boolean(
1386
1428
  // not when passing down to child
1387
1429
  !asChild &&
@@ -1463,10 +1505,6 @@ export function createComponent<
1463
1505
  }
1464
1506
  }
1465
1507
 
1466
- if (debugProp) {
1467
- console.info('overriddenContextProps', overriddenContextProps)
1468
- }
1469
-
1470
1508
  content = (
1471
1509
  <Provider __disableMergeDefaultValues {...overriddenContextProps}>
1472
1510
  {content}
@@ -166,9 +166,9 @@ export function createTamagui<Conf extends CreateTamaguiProps>(
166
166
  }
167
167
  }
168
168
 
169
- const sep = configIn.settings?.cssStyleSeparator || ''
169
+ const sep = ` `
170
170
  function declarationsToRuleSet(decs: string[], selector = '') {
171
- return `:root${selector} {${sep}${[...decs].join(`;${sep}`)}${sep}}`
171
+ return `:root${selector} {${sep}${[...decs].join(`;${sep}`)}\n}`
172
172
  }
173
173
 
174
174
  // non-font
@@ -37,6 +37,7 @@ import type {
37
37
  GetStyleState,
38
38
  PseudoStyles,
39
39
  RulesToInsert,
40
+ SpaceTokens,
40
41
  SplitStyleProps,
41
42
  StaticConfig,
42
43
  StyleObject,
@@ -203,6 +204,7 @@ export const getSplitStyles: StyleSplitter = (
203
204
  process.env.TAMAGUI_TARGET === 'native' ? (undefined as any) : {}
204
205
  const classNames: ClassNamesObject = {}
205
206
 
207
+ let space: SpaceTokens | null = props.space
206
208
  let pseudos: PseudoStyles | null = null
207
209
  let hasMedia: boolean | Set<string> = false
208
210
  let dynamicThemeAccess: boolean | undefined
@@ -923,10 +925,10 @@ export const getSplitStyles: StyleSplitter = (
923
925
  dynamicThemeAccess = true
924
926
  // only apply if this is the current theme
925
927
  if (isCurrentScheme) {
926
- // update mediastyle so the later merge loop uses correct value
928
+ // update mediaStyle so the later merge loop uses correct value
927
929
  mediaStyle[subKey] = val
928
930
  } else {
929
- // Remove from mediaStyle so it doesn't get merged with wrong theme's value
931
+ // remove from mediaStyle so it doesn't get merged with wrong theme's value
930
932
  delete mediaStyle[subKey]
931
933
  }
932
934
  continue
@@ -1599,9 +1601,14 @@ const animatableDefaults = {
1599
1601
  ),
1600
1602
  opacity: 1,
1601
1603
  scale: 1,
1604
+ scaleX: 1,
1605
+ scaleY: 1,
1602
1606
  rotate: '0deg',
1603
- rotateY: '0deg',
1604
1607
  rotateX: '0deg',
1608
+ rotateY: '0deg',
1609
+ rotateZ: '0deg',
1610
+ skewX: '0deg',
1611
+ skewY: '0deg',
1605
1612
  x: 0,
1606
1613
  y: 0,
1607
1614
  borderRadius: 0,
@@ -36,8 +36,6 @@ export function getThemeCSSRules(props: {
36
36
  const CNP = `.${THEME_CLASSNAME_PREFIX}`
37
37
  let vars = ''
38
38
 
39
- // themeToVariableToValueMap.set(theme, {})
40
- // const varToValMap = themeToVariableToValueMap.get(theme)
41
39
  for (const themeKey in theme) {
42
40
  const variable = theme[themeKey] as Variable
43
41
  let value: any = null
@@ -60,9 +58,9 @@ export function getThemeCSSRules(props: {
60
58
  const selectorsSet = new Set(isDarkBase || isLightBase ? baseSelectors : [])
61
59
 
62
60
  // since we dont specify dark/light in classnames we have to do an awkward specificity war
63
- // use config.maxDarkLightNesting to determine how deep you can nest until it breaks
61
+ // hardcoded to support 2 levels of nesting (e.g. light > dark or dark > light)
64
62
  if (hasDarkLight) {
65
- const maxDepth = getSetting('maxDarkLightNesting') ?? 3
63
+ const maxDepth = 2
66
64
 
67
65
  for (const subName of names) {
68
66
  const isDark = isDarkBase || subName.startsWith('dark_')
@@ -83,7 +81,6 @@ export function getThemeCSSRules(props: {
83
81
  for (let depth = 0; depth < numSelectors; depth++) {
84
82
  const isOdd = depth % 2 === 1
85
83
 
86
- // wtf is this continue:
87
84
  if (isOdd && depth < 3) {
88
85
  continue
89
86
  }
@@ -106,9 +103,6 @@ export function getThemeCSSRules(props: {
106
103
  // for light/dark/light:
107
104
  const parentSelectorString = parentSelectors.join(' ')
108
105
  selectorsSet.add(`${parentSelectorString} ${nextChildSelector}`)
109
- // selectorsSet.add(
110
- // `${parentSelectors.join(' ')} ${nextChildSelector}.is_inversed`.trim()
111
- // )
112
106
  }
113
107
  }
114
108
  }
@@ -116,7 +110,6 @@ export function getThemeCSSRules(props: {
116
110
  const selectors = [...selectorsSet].sort(sortString)
117
111
 
118
112
  // only do our :root attach if it's not light/dark - not support sub themes on root saves a lot of effort/size
119
- // this isBaseTheme logic could probably be done more efficiently above
120
113
  const selectorsString =
121
114
  selectors
122
115
  .map((x) => {
@@ -0,0 +1,17 @@
1
+ import { mergeSlotStyleProps } from './mergeSlotStyleProps'
2
+
3
+ /**
4
+ * Merges props from a render element with viewProps from Tamagui.
5
+ * viewProps takes precedence, elementProps provides fallbacks.
6
+ * Style/className are merged, refs and event handlers are composed.
7
+ */
8
+ export function mergeRenderElementProps(
9
+ elementProps: Record<string, any>,
10
+ viewProps: Record<string, any>,
11
+ children: any
12
+ ): Record<string, any> {
13
+ // elementProps as base, viewProps as overlay (viewProps wins)
14
+ const merged = mergeSlotStyleProps({ ...elementProps }, viewProps)
15
+ merged.children = children
16
+ return merged
17
+ }
@@ -0,0 +1,46 @@
1
+ import { composeRefs } from '@tamagui/compose-refs'
2
+ import { composeEventHandlers } from '@tamagui/helpers'
3
+
4
+ const isEventHandler = /^on[A-Z]/
5
+
6
+ /**
7
+ * Merges props with special handling for style, className, ref, and event handlers.
8
+ * Used by Slot and render prop implementations.
9
+ *
10
+ * @param base - Base props (typically from parent/slot)
11
+ * @param overlay - Props to merge on top (typically from child/element)
12
+ * @returns Merged props object (mutates and returns base for perf)
13
+ */
14
+ export function mergeSlotStyleProps(
15
+ base: Record<string, any>,
16
+ overlay: Record<string, any>
17
+ ): Record<string, any> {
18
+ for (const key in overlay) {
19
+ const baseVal = base[key]
20
+ const overlayVal = overlay[key]
21
+
22
+ if (overlayVal === undefined) continue
23
+
24
+ if (key === 'style') {
25
+ base.style =
26
+ baseVal && overlayVal ? { ...baseVal, ...overlayVal } : overlayVal || baseVal
27
+ } else if (key === 'className') {
28
+ base.className =
29
+ baseVal && overlayVal ? `${baseVal} ${overlayVal}` : overlayVal || baseVal
30
+ } else if (key === 'ref') {
31
+ base.ref =
32
+ baseVal && overlayVal ? composeRefs(baseVal, overlayVal) : overlayVal || baseVal
33
+ } else if (
34
+ isEventHandler.test(key) &&
35
+ typeof baseVal === 'function' &&
36
+ typeof overlayVal === 'function'
37
+ ) {
38
+ base[key] = composeEventHandlers(baseVal, overlayVal)
39
+ } else {
40
+ // overlay wins for regular props
41
+ base[key] = overlayVal
42
+ }
43
+ }
44
+
45
+ return base
46
+ }
@@ -13,7 +13,7 @@ export const skipProps = {
13
13
  debug: 1,
14
14
  componentName: 1,
15
15
  disableOptimization: 1,
16
- tag: 1,
16
+ render: 1,
17
17
  style: 1, // handled after loop so pseudos set usedKeys and override it if necessary
18
18
  group: 1,
19
19
  animatePresence: 1,
@@ -0,0 +1,66 @@
1
+ import type React from 'react'
2
+ import { cloneElement, createElement, isValidElement } from 'react'
3
+ import { composeRefs } from '@tamagui/compose-refs'
4
+ import type { TamaguiComponentState } from '../types'
5
+ import { mergeSlotStyleProps } from './mergeSlotStyleProps'
6
+
7
+ export type RenderProp<Props = Record<string, any>> =
8
+ | string
9
+ | React.ReactElement
10
+ | ((props: Props, state: TamaguiComponentState) => React.ReactElement)
11
+
12
+ /**
13
+ * Evaluates a render prop and returns the element to render.
14
+ *
15
+ * @param render - The render prop (tag string, JSX element, or function)
16
+ * @param props - Props to pass to the rendered element (including ref)
17
+ * @param state - Component state for render functions
18
+ * @param defaultElement - Fallback element if render prop is not provided
19
+ */
20
+ export function evaluateRenderProp(
21
+ render: RenderProp | undefined,
22
+ props: Record<string, any>,
23
+ state: TamaguiComponentState,
24
+ defaultElement: React.ReactElement<any>
25
+ ): React.ReactElement {
26
+ if (!render) {
27
+ return defaultElement
28
+ }
29
+
30
+ const defaultChildren = defaultElement.props.children
31
+
32
+ // String tag - swap element type, reuse props from defaultElement
33
+ if (typeof render === 'string') {
34
+ return createElement(render, props, defaultChildren)
35
+ }
36
+
37
+ // Render function - call with props and state
38
+ if (typeof render === 'function') {
39
+ return render(props, state)
40
+ }
41
+
42
+ // JSX element - clone with merged props
43
+ if (isValidElement(render)) {
44
+ const renderProps = render.props as Record<string, any>
45
+ const renderRef = renderProps?.ref
46
+
47
+ // Fast path: no props to merge
48
+ if (!renderProps || Object.keys(renderProps).length === 0) {
49
+ if (renderRef) {
50
+ return cloneElement(
51
+ render,
52
+ { ...props, ref: composeRefs(props.ref, renderRef) } as any,
53
+ defaultChildren
54
+ )
55
+ }
56
+ return cloneElement(render, props as any, defaultChildren)
57
+ }
58
+
59
+ // Merge props (component props as base, render props as overlay)
60
+ const merged = mergeSlotStyleProps({ ...props }, renderProps)
61
+ const children = renderProps.children ?? defaultChildren
62
+ return cloneElement(render, merged as any, children)
63
+ }
64
+
65
+ return defaultElement
66
+ }
@@ -112,12 +112,13 @@ export function getThemeProxied(
112
112
 
113
113
  if (process.env.TAMAGUI_TARGET === 'native') {
114
114
  // ios can avoid re-rendering in some cases when we are using a root light/dark
115
- // disabled in cases where we have animations
115
+ // disabled in cases where we have animations or when scheme changes from parent (isInverse)
116
116
  const shouldOptimize =
117
117
  scheme &&
118
118
  platform !== 'web' &&
119
119
  isIos &&
120
120
  !curProps.deopt &&
121
+ !curState.isInverse &&
121
122
  getSetting('fastSchemeChange') &&
122
123
  doesRootSchemeMatchSystem()
123
124
 
package/src/index.ts CHANGED
@@ -56,6 +56,7 @@ export {
56
56
  getToken,
57
57
  getTokens,
58
58
  getTokenValue,
59
+ loadAnimationDriver,
59
60
  setConfig,
60
61
  setupDev,
61
62
  updateConfig,
@@ -1,4 +1,10 @@
1
- import type { DebugProp, ThemeName, GroupNames, Role } from '../types'
1
+ import type {
2
+ DebugProp,
3
+ ThemeName,
4
+ GroupNames,
5
+ Role,
6
+ TamaguiComponentState,
7
+ } from '../types'
2
8
 
3
9
  export type TamaguiComponentPropsBaseBase = {
4
10
  target?: string
@@ -41,10 +47,19 @@ export type TamaguiComponentPropsBaseBase = {
41
47
  id?: string
42
48
 
43
49
  /**
44
- * Controls the output tag on web
45
- * {@see https://developer.mozilla.org/en-US/docs/Web/HTML/Element}
50
+ * Controls the rendered element on web.
51
+ * - String: renders as that HTML element (e.g., `render="button"`)
52
+ * - JSX Element: clones element with merged props (e.g., `render={<a href="/" />}`)
53
+ * - Function: full control with props and state (e.g., `render={(props) => <Custom {...props} />}`)
46
54
  */
47
- tag?: keyof HTMLElementTagNameMap | (string & {})
55
+ render?:
56
+ | keyof HTMLElementTagNameMap
57
+ | (string & {})
58
+ | React.ReactElement
59
+ | ((
60
+ props: Record<string, any> & { ref?: React.Ref<any> },
61
+ state: TamaguiComponentState
62
+ ) => React.ReactElement)
48
63
 
49
64
  /**
50
65
  * Applies a theme to this element
package/src/styled.tsx CHANGED
@@ -46,6 +46,7 @@ export function styled<
46
46
  variants?: Variants | undefined
47
47
  defaultVariants?: GetVariantAcceptedValues<Variants>
48
48
  context?: StyledContext
49
+ render?: string
49
50
  },
50
51
  config?: StyledConfig
51
52
  ) {
@@ -289,13 +290,13 @@ export function styled<
289
290
  // })
290
291
 
291
292
  // const Test2 = styled(Text1, {
292
- // tag: 'p',
293
+ // render: 'p',
293
294
  // userSelect: 'auto',
294
295
  // color: '$color',
295
296
  // })
296
297
 
297
298
  // const Test3 = styled(Test2, {
298
- // tag: 'p',
299
+ // render: 'p',
299
300
  // userSelect: 'auto',
300
301
  // color: '$color',
301
302
 
@@ -307,7 +308,7 @@ export function styled<
307
308
  // })
308
309
 
309
310
  // const Test = styled(Paragraph, {
310
- // tag: 'p',
311
+ // render: 'p',
311
312
  // userSelect: 'auto',
312
313
  // color: '$color',
313
314