@tamagui/web 1.55.2 → 1.57.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 (81) hide show
  1. package/dist/cjs/config.js +1 -1
  2. package/dist/cjs/config.js.map +1 -1
  3. package/dist/cjs/createComponent.js +79 -21
  4. package/dist/cjs/createComponent.js.map +2 -2
  5. package/dist/cjs/helpers/createMediaStyle.js +47 -39
  6. package/dist/cjs/helpers/createMediaStyle.js.map +1 -1
  7. package/dist/cjs/helpers/createShallowSetState.js +6 -4
  8. package/dist/cjs/helpers/createShallowSetState.js.map +1 -1
  9. package/dist/cjs/helpers/getGroupPropParts.js +41 -0
  10. package/dist/cjs/helpers/getGroupPropParts.js.map +6 -0
  11. package/dist/cjs/helpers/getSplitStyles.js +49 -18
  12. package/dist/cjs/helpers/getSplitStyles.js.map +2 -2
  13. package/dist/cjs/helpers/insertStyleRule.js +9 -2
  14. package/dist/cjs/helpers/insertStyleRule.js.map +1 -1
  15. package/dist/cjs/helpers/matchMedia.js +1 -0
  16. package/dist/cjs/helpers/matchMedia.js.map +1 -1
  17. package/dist/cjs/helpers/matchMedia.native.js +1 -0
  18. package/dist/cjs/helpers/matchMedia.native.js.map +1 -1
  19. package/dist/cjs/hooks/useMedia.js +31 -3
  20. package/dist/cjs/hooks/useMedia.js.map +1 -1
  21. package/dist/cjs/index.js +0 -2
  22. package/dist/cjs/index.js.map +1 -1
  23. package/dist/cjs/views/Slot.js +4 -11
  24. package/dist/cjs/views/Slot.js.map +1 -1
  25. package/dist/esm/config.js +1 -1
  26. package/dist/esm/config.js.map +1 -1
  27. package/dist/esm/createComponent.js +85 -24
  28. package/dist/esm/createComponent.js.map +2 -2
  29. package/dist/esm/helpers/createMediaStyle.js +47 -39
  30. package/dist/esm/helpers/createMediaStyle.js.map +1 -1
  31. package/dist/esm/helpers/createShallowSetState.js +4 -3
  32. package/dist/esm/helpers/createShallowSetState.js.map +1 -1
  33. package/dist/esm/helpers/getGroupPropParts.js +17 -0
  34. package/dist/esm/helpers/getGroupPropParts.js.map +6 -0
  35. package/dist/esm/helpers/getSplitStyles.js +50 -18
  36. package/dist/esm/helpers/getSplitStyles.js.map +2 -2
  37. package/dist/esm/helpers/insertStyleRule.js +9 -2
  38. package/dist/esm/helpers/insertStyleRule.js.map +1 -1
  39. package/dist/esm/helpers/matchMedia.js +1 -0
  40. package/dist/esm/helpers/matchMedia.js.map +1 -1
  41. package/dist/esm/helpers/matchMedia.native.js +1 -0
  42. package/dist/esm/helpers/matchMedia.native.js.map +1 -1
  43. package/dist/esm/hooks/useMedia.js +29 -3
  44. package/dist/esm/hooks/useMedia.js.map +1 -1
  45. package/dist/esm/index.js +0 -1
  46. package/dist/esm/index.js.map +1 -1
  47. package/dist/esm/views/Slot.js +3 -9
  48. package/dist/esm/views/Slot.js.map +1 -1
  49. package/package.json +9 -10
  50. package/src/config.ts +1 -1
  51. package/src/createComponent.tsx +102 -25
  52. package/src/helpers/createMediaStyle.ts +59 -45
  53. package/src/helpers/createShallowSetState.tsx +3 -3
  54. package/src/helpers/getGroupPropParts.ts +14 -0
  55. package/src/helpers/getSplitStyles.tsx +60 -24
  56. package/src/helpers/insertStyleRule.tsx +17 -5
  57. package/src/helpers/matchMedia.native.ts +1 -0
  58. package/src/helpers/matchMedia.ts +1 -0
  59. package/src/hooks/useMedia.tsx +34 -3
  60. package/src/index.ts +0 -1
  61. package/src/types.tsx +43 -6
  62. package/src/views/Slot.tsx +2 -8
  63. package/types/createComponent.d.ts.map +1 -1
  64. package/types/helpers/createMediaStyle.d.ts +1 -1
  65. package/types/helpers/createMediaStyle.d.ts.map +1 -1
  66. package/types/helpers/createShallowSetState.d.ts +2 -2
  67. package/types/helpers/createShallowSetState.d.ts.map +1 -1
  68. package/types/helpers/getGroupPropParts.d.ts +6 -0
  69. package/types/helpers/getGroupPropParts.d.ts.map +1 -0
  70. package/types/helpers/getSplitStyles.d.ts.map +1 -1
  71. package/types/helpers/insertStyleRule.d.ts.map +1 -1
  72. package/types/helpers/matchMedia.d.ts.map +1 -1
  73. package/types/helpers/matchMedia.native.d.ts.map +1 -1
  74. package/types/hooks/useMedia.d.ts +6 -1
  75. package/types/hooks/useMedia.d.ts.map +1 -1
  76. package/types/index.d.ts +0 -1
  77. package/types/index.d.ts.map +1 -1
  78. package/types/types.d.ts +38 -4
  79. package/types/types.d.ts.map +1 -1
  80. package/types/views/Slot.d.ts +0 -1
  81. package/types/views/Slot.d.ts.map +1 -1
@@ -1,6 +1,6 @@
1
1
  import { useComposedRefs } from '@tamagui/compose-refs'
2
2
  import { isClient, isServer, isWeb } from '@tamagui/constants'
3
- import { validStyles } from '@tamagui/helpers'
3
+ import { composeEventHandlers, validStyles } from '@tamagui/helpers'
4
4
  import { useDidFinishSSR } from '@tamagui/use-did-finish-ssr'
5
5
  import React, {
6
6
  Children,
@@ -21,18 +21,23 @@ import { getConfig, onConfiguredOnce } from './config'
21
21
  import { stackDefaultStyles } from './constants/constants'
22
22
  import { ComponentContext } from './contexts/ComponentContext'
23
23
  import { didGetVariableValue, setDidGetVariableValue } from './createVariable'
24
- import { createShallowSetState } from './helpers/createShallowSetState'
24
+ import {
25
+ createShallowSetState,
26
+ mergeIfNotShallowEqual,
27
+ } from './helpers/createShallowSetState'
25
28
  import { useSplitStyles } from './helpers/getSplitStyles'
26
29
  import { mergeProps } from './helpers/mergeProps'
27
30
  import { proxyThemeVariables } from './helpers/proxyThemeVariables'
28
31
  import { themeable } from './helpers/themeable'
29
- import { setMediaShouldUpdate, useMedia } from './hooks/useMedia'
32
+ import { mediaKeyMatch, setMediaShouldUpdate, useMedia } from './hooks/useMedia'
30
33
  import { useThemeWithState } from './hooks/useTheme'
31
34
  import { hooks } from './setupHooks'
32
35
  import {
36
+ ComponentContextI,
33
37
  DebugProp,
34
38
  DisposeFn,
35
- GroupContextType,
39
+ GroupState,
40
+ LayoutEvent,
36
41
  SpaceDirection,
37
42
  SpaceValue,
38
43
  SpacerProps,
@@ -235,6 +240,7 @@ export function createComponent<
235
240
  // [animated, inversed]
236
241
  const stateRef = useRef(
237
242
  undefined as any as {
243
+ hasMeasured?: boolean
238
244
  hasAnimated?: boolean
239
245
  themeShallow?: boolean
240
246
  isListeningToTheme?: boolean
@@ -291,13 +297,17 @@ export function createComponent<
291
297
  let setStateShallow = createShallowSetState(setState)
292
298
 
293
299
  const groupName = props.group as any as string
300
+ const groupClassName = groupName ? `t_group_${props.group}` : ''
301
+
294
302
  if (groupName) {
295
303
  // when we set state we also set our group state and emit an event for children listening:
296
304
  const groupContextState = componentContext.groups.state
297
305
  const og = setStateShallow
298
306
  setStateShallow = (state) => {
299
307
  og(state)
300
- componentContext.groups.emit(groupName, state)
308
+ componentContext.groups.emit(groupName, {
309
+ pseudo: state,
310
+ })
301
311
  // and mutate the current since its concurrent safe (children throw it in useState on mount)
302
312
  const next = {
303
313
  ...groupContextState[groupName],
@@ -502,6 +512,11 @@ export function createComponent<
502
512
  debugProp
503
513
  )
504
514
 
515
+ // hide strategy will set this opacity = 0 until measured
516
+ if (props.group && props.untilMeasured === 'hide' && !stateRef.current.hasMeasured) {
517
+ splitStyles.style.opacity = 0
518
+ }
519
+
505
520
  if (process.env.NODE_ENV === 'development' && time) time`split-styles`
506
521
 
507
522
  stateRef.current.isListeningToTheme = splitStyles.dynamicThemeAccess
@@ -616,6 +631,12 @@ export function createComponent<
616
631
  ...nonTamaguiProps
617
632
  } = viewPropsIn
618
633
 
634
+ if (process.env.NODE_ENV === 'development' && props.untilMeasured && !props.group) {
635
+ console.warn(
636
+ `You set the untilMeasured prop without setting group. This doesn't work, be sure to set untilMeasured on the parent that sets group, not the children that use the $group- prop.\n\nIf you meant to do this, you can disable this warning - either change untilMeasured and group at the same time, or do group={conditional ? 'name' : undefined}`
637
+ )
638
+ }
639
+
619
640
  if (process.env.NODE_ENV === 'development' && time) time`destructure`
620
641
 
621
642
  const disabled =
@@ -631,6 +652,24 @@ export function createComponent<
631
652
  viewProps.theme = _themeProp
632
653
  }
633
654
 
655
+ if (groupName) {
656
+ nonTamaguiProps.onLayout = composeEventHandlers(
657
+ nonTamaguiProps.onLayout,
658
+ (e: LayoutEvent) => {
659
+ componentContext.groups.emit(groupName, {
660
+ layout: e.nativeEvent.layout,
661
+ })
662
+
663
+ // force re-render if measure strategy is hide
664
+ if (!stateRef.current.hasMeasured && props.untilMeasured === 'hide') {
665
+ setState((prev) => ({ ...prev }))
666
+ }
667
+
668
+ stateRef.current.hasMeasured = true
669
+ }
670
+ )
671
+ }
672
+
634
673
  // if react-native-web view just pass all props down
635
674
  if (
636
675
  process.env.TAMAGUI_TARGET === 'web' &&
@@ -672,7 +711,8 @@ export function createComponent<
672
711
  // for example css driver needs to render once with the first styles, then again with the next
673
712
  // if its a layout effect it will just skip that first render output
674
713
  const shouldSetMounted = needsMount && state.unmounted
675
- const { pseudoGroups } = splitStyles
714
+ const { pseudoGroups, mediaGroups } = splitStyles
715
+
676
716
  useEffect(() => {
677
717
  if (shouldSetMounted) {
678
718
  const unmounted =
@@ -686,22 +726,36 @@ export function createComponent<
686
726
 
687
727
  // parent group pseudo listening
688
728
  let disposeGroupsListener: DisposeFn | undefined
689
- if (pseudoGroups) {
690
- const current = {}
691
- disposeGroupsListener = componentContext.groups.subscribe((name, next) => {
692
- if (pseudoGroups.has(name)) {
693
- // merge because we emit a partial of the state each time
694
- Object.assign(current, next)
695
- const group = {
696
- ...state.group,
697
- [name]: current,
729
+ if (pseudoGroups || mediaGroups) {
730
+ const current = {
731
+ pseudo: {},
732
+ media: {},
733
+ } satisfies GroupState
734
+ disposeGroupsListener = componentContext.groups.subscribe(
735
+ (name, { layout, pseudo }) => {
736
+ if (pseudo && pseudoGroups?.has(name)) {
737
+ // we emit a partial so merge it + change reference so mergeIfNotShallowEqual runs
738
+ Object.assign(current.pseudo, pseudo)
739
+ persist()
740
+ } else if (layout && mediaGroups) {
741
+ const mediaState = getMediaState(mediaGroups, layout)
742
+ const next = mergeIfNotShallowEqual(current.media, mediaState)
743
+ if (next !== current.media) {
744
+ Object.assign(current.media, next)
745
+ persist()
746
+ }
747
+ }
748
+ function persist() {
749
+ setStateShallow({
750
+ // force it to be referentially different so it always updates
751
+ group: {
752
+ ...state.group,
753
+ [name]: current,
754
+ },
755
+ })
698
756
  }
699
- setStateShallow({
700
- // force it to be referentially different so it always updates
701
- group,
702
- })
703
757
  }
704
- })
758
+ )
705
759
  }
706
760
 
707
761
  return () => {
@@ -712,6 +766,7 @@ export function createComponent<
712
766
  shouldSetMounted,
713
767
  state.unmounted,
714
768
  pseudoGroups ? Object.keys([...pseudoGroups]).join('') : 0,
769
+ mediaGroups ? Object.keys([...mediaGroups]).join('') : 0,
715
770
  ])
716
771
 
717
772
  const avoidAnimationStyle = keepStyleSSR && state.unmounted === true
@@ -735,7 +790,7 @@ export function createComponent<
735
790
  componentName ? componentClassName : '',
736
791
  fontFamilyClassName,
737
792
  classNames ? Object.values(classNames).join(' ') : '',
738
- props.group ? `t_group_${props.group}` : '',
793
+ groupClassName,
739
794
  ]
740
795
  className = classList.join(' ')
741
796
 
@@ -934,16 +989,24 @@ export function createComponent<
934
989
 
935
990
  // must override context so siblings don't clobber initial state
936
991
  const subGroupContext = useMemo(() => {
937
- if (!groupName) return null
992
+ if (!groupName) return
938
993
  // change reference so context value updates
939
994
  return {
940
995
  ...componentContext.groups,
941
996
  // change reference so as we mutate it doesn't affect siblings etc
942
997
  state: {
943
998
  ...componentContext.groups.state,
944
- [groupName]: initialState,
999
+ [groupName]: {
1000
+ pseudo: initialState,
1001
+ // capture just initial width and height if they exist
1002
+ // will have top, left, width, height (not x, y)
1003
+ layout: {
1004
+ width: fromPx(splitStyles.style.width as any),
1005
+ height: fromPx(splitStyles.style.height as any),
1006
+ } as any,
1007
+ },
945
1008
  },
946
- }
1009
+ } satisfies ComponentContextI['groups']
947
1010
  }, [groupName])
948
1011
 
949
1012
  if (groupName && subGroupContext) {
@@ -971,7 +1034,7 @@ export function createComponent<
971
1034
  if (events || isAnimatedReactNativeWeb) {
972
1035
  content = (
973
1036
  <span
974
- className={`${isAnimatedReactNativeWeb ? className : ''} _dsp_contents`}
1037
+ className={`${isAnimatedReactNativeWeb ? className : ''} _dsp_contents`}
975
1038
  {...(events && {
976
1039
  onMouseEnter: events.onMouseEnter,
977
1040
  onMouseLeave: events.onMouseLeave,
@@ -1320,3 +1383,17 @@ function hasAnimatedStyleValue(style: Object) {
1320
1383
  return val && typeof val === 'object' && '_animation' in val
1321
1384
  })
1322
1385
  }
1386
+
1387
+ function getMediaState(
1388
+ mediaGroups: Set<string>,
1389
+ layout: LayoutEvent['nativeEvent']['layout']
1390
+ ) {
1391
+ return Object.fromEntries(
1392
+ [...mediaGroups].map((mediaKey) => {
1393
+ return [mediaKey, mediaKeyMatch(mediaKey, layout as any)]
1394
+ })
1395
+ )
1396
+ }
1397
+
1398
+ const fromPx = (val?: number | string) =>
1399
+ typeof val !== 'string' ? val : +val.replace('px', '')
@@ -1,6 +1,7 @@
1
1
  import { getConfig } from '../config'
2
2
  import { mediaObjectToString } from '../hooks/useMedia'
3
3
  import type { MediaQueries, MediaStyleObject, StyleObject } from '../types'
4
+ import { getGroupPropParts } from './getGroupPropParts'
4
5
 
5
6
  // TODO have this be used by extractMediaStyle in tamagui static
6
7
  // not synced to static/constants for now
@@ -9,47 +10,60 @@ export const MEDIA_SEP = '_'
9
10
  let prefixes: Record<string, string> | null = null
10
11
  let selectors: Record<string, string> | null = null
11
12
 
12
- const parentPseudoToSelector = {
13
+ const groupPseudoToPseudoCSSMap = {
13
14
  press: 'active',
14
15
  }
15
16
 
16
17
  export const createMediaStyle = (
17
- { property, identifier, rules }: StyleObject,
18
- mediaKey: string,
18
+ styleObject: StyleObject,
19
+ mediaKeyIn: string,
19
20
  mediaQueries: MediaQueries,
20
21
  negate?: boolean,
21
22
  priority?: number
22
23
  ): MediaStyleObject => {
24
+ const { property, identifier, rules } = styleObject
23
25
  const conf = getConfig()
24
26
  const enableMediaPropOrder = conf.settings.mediaPropOrder
25
- const isThemeMedia = mediaKey.startsWith('theme-')
26
- const isPlatformMedia = !isThemeMedia && mediaKey.startsWith('platform-')
27
- const isGroupMedia = !isThemeMedia && !isPlatformMedia && mediaKey.startsWith('group-')
28
- const isNonWindowMedia = isThemeMedia || isPlatformMedia || isGroupMedia
27
+ const isThemeMedia = mediaKeyIn.startsWith('theme-')
28
+ const isPlatformMedia = !isThemeMedia && mediaKeyIn.startsWith('platform-')
29
+ const isGroup = !isThemeMedia && !isPlatformMedia && mediaKeyIn.startsWith('group-')
30
+ const isNonWindowMedia = isThemeMedia || isPlatformMedia || isGroup
29
31
  const negKey = negate ? '0' : ''
30
32
  const ogPrefix = identifier.slice(0, identifier.indexOf('-') + 1)
33
+ const id = `${ogPrefix}${MEDIA_SEP}${mediaKeyIn.replace('-', '')}${negKey}${MEDIA_SEP}`
31
34
 
32
- let styleRule: string
33
-
34
- const id = `${ogPrefix}${MEDIA_SEP}${mediaKey.replace('-', '')}${negKey}${MEDIA_SEP}`
35
- const nextIdentifier = identifier.replace(ogPrefix, id)
35
+ let styleRule = ''
36
+ let groupMediaKey: string | undefined
37
+ let containerName: string | undefined
38
+ let nextIdentifier = identifier.replace(ogPrefix, id)
39
+ let styleInner = rules.map((rule) => rule.replace(identifier, nextIdentifier)).join(';')
36
40
 
37
41
  if (isNonWindowMedia) {
38
- const precedencePrefix = new Array(priority).fill(':root').join('')
39
- const styleInner = rules
40
- .map((rule) => rule.replace(identifier, nextIdentifier))
41
- .join(';')
42
+ const precedenceImportancePrefix = new Array((priority || 0) + (isGroup ? 1 : 0))
43
+ .fill(':root')
44
+ .join('')
45
+
46
+ if (isThemeMedia || isGroup) {
47
+ const groupInfo = getGroupPropParts(mediaKeyIn)
48
+ const mediaName = groupInfo?.name
49
+ groupMediaKey = groupInfo?.media
50
+ if (isGroup) {
51
+ containerName = mediaName
52
+ }
53
+ const name = (isGroup ? 'group_' : '') + mediaName
42
54
 
43
- if (isThemeMedia || isGroupMedia) {
44
- const [_, groupName, groupPseudo] = mediaKey.split('-')
45
- const name = (isGroupMedia ? 'group_' : '') + groupName
46
55
  const selectorStart = styleInner.indexOf(':root')
47
56
  const selectorEnd = styleInner.lastIndexOf('{')
48
57
  const selector = styleInner.slice(selectorStart, selectorEnd)
49
58
  const precedenceSpace = conf.themeClassNameOnRoot ? '' : ' '
50
- const pseudoSelectorName = parentPseudoToSelector[groupPseudo] || groupPseudo
59
+ const pseudoSelectorName = groupInfo.pseudo
60
+ ? groupPseudoToPseudoCSSMap[groupInfo.pseudo] || groupInfo.pseudo
61
+ : undefined
62
+
51
63
  const pseudoSelector = pseudoSelectorName ? `:${pseudoSelectorName}` : ''
52
- const nextSelector = `:root${precedencePrefix}${precedenceSpace}.t_${name}${pseudoSelector} ${selector.replace(
64
+ const presedencePrefix = `:root${precedenceImportancePrefix}${precedenceSpace}`
65
+ const mediaSelector = `.t_${name}${pseudoSelector}`
66
+ const nextSelector = `${presedencePrefix}${mediaSelector} ${selector.replace(
53
67
  ':root',
54
68
  ''
55
69
  )}`
@@ -57,48 +71,48 @@ export const createMediaStyle = (
57
71
  // add back in the { we used to split
58
72
  styleRule = styleInner.replace(selector, nextSelector)
59
73
  } else {
60
- styleRule = `${precedencePrefix}${styleInner}`
74
+ styleRule = `${precedenceImportancePrefix}${styleInner}`
61
75
  }
62
- } else {
76
+ }
77
+
78
+ if (!isNonWindowMedia || groupMediaKey) {
79
+ // one time cost:
80
+ // TODO MOVE THIS INTO SETUP AREA AND EXPORT IT
63
81
  if (!selectors) {
64
- if (enableMediaPropOrder) {
65
- const mediaKeys = Object.keys(mediaQueries)
66
- selectors = Object.fromEntries(
67
- mediaKeys.map((key) => [key, mediaObjectToString(mediaQueries[key])])
68
- )
69
- } else {
70
- const mediaKeys = Object.keys(mediaQueries)
82
+ const mediaKeys = Object.keys(mediaQueries)
83
+ selectors = Object.fromEntries(
84
+ mediaKeys.map((key) => [key, mediaObjectToString(mediaQueries[key])])
85
+ )
86
+ if (!enableMediaPropOrder) {
71
87
  prefixes = Object.fromEntries(
72
- mediaKeys.map((key, index) => [
73
- key,
74
- new Array(index + 1).fill(':root').join(''),
75
- ])
76
- )
77
- selectors = Object.fromEntries(
78
- mediaKeys.map((key) => [key, mediaObjectToString(mediaQueries[key])])
88
+ mediaKeys.map((k, index) => [k, new Array(index + 1).fill(':root').join('')])
79
89
  )
80
90
  }
81
91
  }
82
92
 
83
- const precedencePrefix = enableMediaPropOrder
93
+ const mediaKey = groupMediaKey || mediaKeyIn
94
+ const mediaSelector = selectors[mediaKey]
95
+ const screenStr = negate ? 'not all and' : ''
96
+ const mediaQuery = `${screenStr} ${mediaSelector}`
97
+ const precedenceImportancePrefix = groupMediaKey
98
+ ? ''
99
+ : enableMediaPropOrder
84
100
  ? // this new array should be cached
85
101
  new Array(priority).fill(':root').join('')
86
102
  : // @ts-ignore
87
103
  prefixes[mediaKey]
104
+ const prefix = groupMediaKey ? `@container ${containerName}` : '@media'
88
105
 
89
- const mediaSelector = selectors[mediaKey]
90
- const screenStr = negate ? 'not all and' : ''
91
- const mediaQuery = `${screenStr} ${mediaSelector}`
92
- const styleInner = rules
93
- .map((rule) => rule.replace(identifier, nextIdentifier))
94
- .join(';')
106
+ if (groupMediaKey) {
107
+ styleInner = styleRule
108
+ }
95
109
 
96
110
  // combines media queries if they already exist
97
- if (styleInner.includes('@media')) {
111
+ if (styleInner.includes(prefix)) {
98
112
  // combine
99
113
  styleRule = styleInner.replace('{', ` and ${mediaQuery} {`)
100
114
  } else {
101
- styleRule = `@media ${mediaQuery}{${precedencePrefix}${styleInner}}`
115
+ styleRule = `${prefix} ${mediaQuery}{${precedenceImportancePrefix}${styleInner}}`
102
116
  }
103
117
  }
104
118
 
@@ -2,13 +2,13 @@ import React from 'react'
2
2
 
3
3
  import { TamaguiComponentState } from '../types'
4
4
 
5
- export function createShallowSetState<State extends TamaguiComponentState>(
5
+ export function createShallowSetState<State extends Object>(
6
6
  setter: React.Dispatch<React.SetStateAction<State>>
7
7
  ) {
8
- return (next: Partial<State>) => setter((prev) => shallow(prev, next))
8
+ return (next: Partial<State>) => setter((prev) => mergeIfNotShallowEqual(prev, next))
9
9
  }
10
10
 
11
- function shallow(prev, next) {
11
+ export function mergeIfNotShallowEqual(prev, next) {
12
12
  for (const key in next) {
13
13
  if (prev[key] !== next[key]) {
14
14
  return { ...prev, ...next }
@@ -0,0 +1,14 @@
1
+ import { getMedia } from '../hooks/useMedia'
2
+
3
+ export function getGroupPropParts(groupProp: string) {
4
+ const mediaQueries = getMedia()
5
+ const [_, name, part3, part4] = groupProp.split('-')
6
+ let pseudo: string | undefined
7
+ const media = part3 in mediaQueries ? part3 : undefined
8
+ if (!media) {
9
+ pseudo = part3
10
+ } else {
11
+ pseudo = part4
12
+ }
13
+ return { name, pseudo, media }
14
+ }
@@ -28,6 +28,8 @@ import {
28
28
  getMediaImportanceIfMoreImportant,
29
29
  mediaState as globalMediaState,
30
30
  isMediaKey,
31
+ mediaKeyMatch,
32
+ mediaKeyToQuery,
31
33
  mediaQueryConfig,
32
34
  mergeMediaByImportance,
33
35
  } from '../hooks/useMedia'
@@ -54,6 +56,7 @@ import type {
54
56
  import type { LanguageContextType } from '../views/FontLanguage.types'
55
57
  import { createMediaStyle } from './createMediaStyle'
56
58
  import { fixStyles } from './expandStyles'
59
+ import { getGroupPropParts } from './getGroupPropParts'
57
60
  import { generateAtomicStyles, getStylesAtomic, styleToCSS } from './getStylesAtomic'
58
61
  import {
59
62
  insertStyleRules,
@@ -145,27 +148,27 @@ export const getSplitStyles: StyleSplitter = (
145
148
  } = staticConfig
146
149
  const validStyleProps = isText ? stylePropsText : validStyles
147
150
  const viewProps: GetStyleResult['viewProps'] = {}
148
- let pseudos: PseudoStyles | null = null
149
151
  const mediaState = styleProps.mediaState || globalMediaState
150
152
  const usedKeys: Record<string, number> = {}
151
- let space: SpaceTokens | null = props.space
152
- let hasMedia: boolean | string[] = false
153
- let dynamicThemeAccess: boolean | undefined
154
- let pseudoGroups: Set<string> | undefined
155
153
  const shouldDoClasses = acceptsClassName && isWeb && !styleProps.noClassNames
156
-
157
- let style: ViewStyleWithPseudos = {}
158
154
  const rulesToInsert: RulesToInsert = []
159
155
  const classNames: ClassNamesObject = {}
160
- let className = '' // existing classNames
161
156
  // we need to gather these specific to each media query / pseudo
162
157
  // value is [hash, val], so ["-jnjad-asdnjk", "scaleX(1) rotate(10deg)"]
163
158
  const transforms: Record<TransformNamespaceKey, [string, string]> = {}
164
159
 
160
+ let pseudos: PseudoStyles | null = null
161
+ let space: SpaceTokens | null = props.space
162
+ let hasMedia: boolean | string[] = false
163
+ let dynamicThemeAccess: boolean | undefined
164
+ let pseudoGroups: Set<string> | undefined
165
+ let mediaGroups: Set<string> | undefined
166
+ let style: ViewStyleWithPseudos = {}
167
+ let className = '' // existing classNames
165
168
  let mediaStylesSeen = 0
166
169
 
167
170
  /**
168
- * Not the biggest fan of creating this object but it is a nice API
171
+ * Not the biggest fan of creating an object but it is a nice API
169
172
  */
170
173
  const styleState: GetStyleState = {
171
174
  curProps: Object.assign({}, props),
@@ -246,7 +249,21 @@ export const getSplitStyles: StyleSplitter = (
246
249
 
247
250
  if (keyInit === 'className') continue // handled above
248
251
  if (keyInit in usedKeys) continue
249
- if (keyInit in skipProps && !isHOC) continue
252
+ if (keyInit in skipProps && !isHOC) {
253
+ if (keyInit === 'group') {
254
+ // add container style
255
+ const identifier = `t_group_${valInit}`
256
+ const containerCSS = {
257
+ identifier,
258
+ property: 'container',
259
+ rules: [
260
+ `.${identifier} { container-name: ${valInit}; container-type: inline-size; }`,
261
+ ],
262
+ }
263
+ addStyleToInsertRules(rulesToInsert, containerCSS)
264
+ }
265
+ continue
266
+ }
250
267
 
251
268
  styleState.curProps[keyInit] = valInit
252
269
 
@@ -669,6 +686,7 @@ export const getSplitStyles: StyleSplitter = (
669
686
  if (!val) continue
670
687
 
671
688
  // TODO can avoid processing this if !shouldDoClasses + state is off
689
+ // (note: can't because we need to set defaults on enter/exit or else enforce that they should)
672
690
  const pseudoStyleObject = getSubStyle(
673
691
  styleState,
674
692
  key,
@@ -865,6 +883,7 @@ export const getSplitStyles: StyleSplitter = (
865
883
  if (shouldDoClasses) {
866
884
  if (hasSpace) {
867
885
  delete mediaStyle['space']
886
+ // TODO group/theme/platform + space support (or just make it official not supported in favor of gap)
868
887
  if (mediaState[mediaKeyShort]) {
869
888
  const importance = getMediaImportanceIfMoreImportant(
870
889
  mediaKeyShort,
@@ -923,31 +942,46 @@ export const getSplitStyles: StyleSplitter = (
923
942
  continue
924
943
  }
925
944
  } else if (isGroupMedia) {
926
- const [_, groupName, groupPseudoKey] = mediaKeyShort.split('-')
945
+ const groupInfo = getGroupPropParts(mediaKeyShort)
946
+ const groupName = groupInfo.name
927
947
 
928
948
  // $group-x
929
- if (!context?.groups.state[groupName]) {
949
+ const groupContext = context?.groups.state[groupName]
950
+ if (!groupContext) {
930
951
  if (process.env.NODE_ENV === 'development' && debug) {
931
952
  console.warn(`No parent with group prop, skipping styles: ${groupName}`)
932
953
  }
933
954
  continue
934
955
  }
935
956
 
936
- // $group-x-hover
937
- pseudoGroups ||= new Set()
938
- pseudoGroups.add(groupName)
957
+ const groupPseudoKey = groupInfo.pseudo
958
+ const groupMediaKey = groupInfo.media
959
+ const componentGroupState = componentState.group?.[groupName]
960
+
961
+ if (groupMediaKey) {
962
+ mediaGroups ||= new Set()
963
+ mediaGroups.add(groupMediaKey)
964
+ const mediaState = componentGroupState?.media
965
+ let isActive = mediaState?.[groupMediaKey]
966
+ // use parent styles if width and height hardcoded we can do an inline media match and avoid double render
967
+ if (!mediaState && groupContext.layout) {
968
+ isActive = mediaKeyMatch(groupMediaKey, groupContext.layout)
969
+ }
970
+ if (!isActive) continue
971
+ importanceBump = 2
972
+ }
973
+
939
974
  if (groupPseudoKey) {
940
- const groupPseudoKeyShort = groupPseudoKey
941
- const groupState =
942
- componentState.group?.[groupName] ||
975
+ pseudoGroups ||= new Set()
976
+ pseudoGroups.add(groupName)
977
+ const componentGroupPseudoState = (
978
+ componentGroupState ||
943
979
  // fallback to context initially
944
980
  context.groups.state[groupName]
945
-
946
- const isActive = groupState?.[groupPseudoKeyShort]
947
- if (!isActive) {
948
- continue
949
- }
950
- const priority = pseudoPriorities[groupPseudoKeyShort]
981
+ ).pseudo
982
+ const isActive = componentGroupPseudoState?.[groupPseudoKey]
983
+ if (!isActive) continue
984
+ const priority = pseudoPriorities[groupPseudoKey]
951
985
  importanceBump = priority
952
986
  }
953
987
  }
@@ -1198,6 +1232,7 @@ export const getSplitStyles: StyleSplitter = (
1198
1232
  rulesToInsert,
1199
1233
  dynamicThemeAccess,
1200
1234
  pseudoGroups,
1235
+ mediaGroups,
1201
1236
  }
1202
1237
 
1203
1238
  // native: swap out the right family based on weight/style
@@ -1417,6 +1452,7 @@ const mapTransformKeys = {
1417
1452
  }
1418
1453
 
1419
1454
  const skipProps = {
1455
+ untilMeasured: 1,
1420
1456
  animation: 1,
1421
1457
  space: 1,
1422
1458
  animateOnly: 1,
@@ -9,6 +9,10 @@ import type {
9
9
  TokensParsed,
10
10
  } from '../types'
11
11
 
12
+ // only cache tamagui styles
13
+ // TODO merge totalSelectorsInserted and allSelectors?
14
+ const scannedCache = new WeakMap<CSSStyleSheet, string>()
15
+ const totalSelectorsInserted = new Map<string, number>()
12
16
  const allSelectors: Record<string, string> = {}
13
17
  const allRules: Record<string, string> = {}
14
18
  export const insertedTransforms = {}
@@ -44,10 +48,6 @@ function addTransform(identifier: string, css: string, rule?: CSSRule) {
44
48
 
45
49
  // multiple sheets could have the same ids so we have to count
46
50
 
47
- // only cache tamagui styles
48
- const scannedCache = new WeakMap<CSSStyleSheet, string>()
49
- const totalSelectorsInserted = new Map<string, number>()
50
-
51
51
  export function listenForSheetChanges() {
52
52
  if (!isClient) return
53
53
 
@@ -378,6 +378,18 @@ export function shouldInsertStyleRules(identifier: string) {
378
378
  return true
379
379
  }
380
380
  const total = totalSelectorsInserted.get(identifier)
381
- // note, -1, we are being conservative and leaving some in
381
+
382
+ if (process.env.NODE_ENV === 'development') {
383
+ if (
384
+ totalSelectorsInserted.size >
385
+ +(process.env.TAMAGUI_STYLE_INSERTION_WARNING_LIMIT || 50000)
386
+ ) {
387
+ console.warn(
388
+ `Warning: inserting many CSS rules, you may be animating something and generating many CSS insertions, which can degrade performance. Instead, try using the "disableClassName" property on elements that change styles often. To disable this warning set TAMAGUI_STYLE_INSERTION_WARNING_LIMIT from 50000 to something higher`
389
+ )
390
+ }
391
+ }
392
+
393
+ // note we are being conservative allowing duplicates
382
394
  return total === undefined || total < 2
383
395
  }
@@ -9,6 +9,7 @@ function matchMediaFallback(query: string): MediaQueryList {
9
9
  console.warn('warning: matchMedia implementation is not provided.')
10
10
  }
11
11
  return {
12
+ match: (a, b) => false,
12
13
  addListener: () => {},
13
14
  removeListener: () => {},
14
15
  matches: false,
@@ -5,6 +5,7 @@ export const matchMedia =
5
5
 
6
6
  function matchMediaFallback(_: string): MediaQueryList {
7
7
  return {
8
+ match: (a, b) => false,
8
9
  addListener() {},
9
10
  removeListener() {},
10
11
  matches: false,