@tamagui/static 2.3.3 → 2.4.0-1783110557100

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.
@@ -71,6 +71,39 @@ let hasLoggedBaseInfo = false;
71
71
  function isFullyDisabled(props) {
72
72
  return props.disableExtraction && props.disableDebugAttr;
73
73
  }
74
+ function styleValueHasToken(v) {
75
+ if (typeof v === "string") return v.charCodeAt(0) === 36;
76
+ if (Array.isArray(v)) return v.some(styleValueHasToken);
77
+ if (v && typeof v === "object") return Object.values(v).some(styleValueHasToken);
78
+ return false;
79
+ }
80
+ function hasUntilMeasuredAncestor(path, groupName) {
81
+ let current = path.parentPath;
82
+ while (current) {
83
+ if (current.isJSXElement()) {
84
+ const opening = current.node.openingElement;
85
+ let foundGroup = false;
86
+ let foundUntilMeasured = false;
87
+ for (const attr of opening.attributes) {
88
+ if (!t.isJSXAttribute(attr)) continue;
89
+ if (!t.isJSXIdentifier(attr.name)) continue;
90
+ const aName = attr.name.name;
91
+ if (aName === "group") {
92
+ if (t.isStringLiteral(attr.value) && attr.value.value === groupName) {
93
+ foundGroup = true;
94
+ } else if (t.isJSXExpressionContainer(attr.value) && t.isStringLiteral(attr.value.expression) && attr.value.expression.value === groupName) {
95
+ foundGroup = true;
96
+ }
97
+ } else if (aName === "untilMeasured") {
98
+ foundUntilMeasured = true;
99
+ }
100
+ }
101
+ if (foundGroup && foundUntilMeasured) return true;
102
+ }
103
+ current = current.parentPath;
104
+ }
105
+ return false;
106
+ }
74
107
  function createExtractor({
75
108
  logger = console,
76
109
  platform = "web"
@@ -293,6 +326,13 @@ function createExtractor({
293
326
  // don't disable variants or else you lose many things flattening
294
327
  staticConfig.variants?.[name] || projectInfo?.tamaguiConfig?.shorthands[name]);
295
328
  }
329
+ function getGroupPseudo(name) {
330
+ const [_, groupName, a, b, c] = name.split("-");
331
+ if (!groupName) return;
332
+ const m2 = a && b ? `${a}-${b}` : "";
333
+ const media = m2 && mediaQueryConfig[m2] && m2 || a && mediaQueryConfig[a] && a;
334
+ return media ? media === m2 ? c : b ? `${b}${c ? `-${c}` : ""}` : void 0 : a ? `${a}${b ? `-${b}` : ""}${c ? `-${c}` : ""}` : void 0;
335
+ }
296
336
  const isTargetingHTML = platform2 === "web";
297
337
  const ogDebug = shouldPrintDebug;
298
338
  const tm = (0, import_timer.timer)();
@@ -806,6 +846,10 @@ function createExtractor({
806
846
  }
807
847
  return [attribute.value, path.get("value")];
808
848
  })();
849
+ if (deoptProps.has(name) || platform2 === "native" && name[0] === "$" && (name.startsWith("$theme-") || name.startsWith("$group-") && getGroupPseudo(name) !== "hover")) {
850
+ inlined.set(name, true);
851
+ return attr;
852
+ }
809
853
  const remove = () => {
810
854
  Array.isArray(valuePath) ? valuePath.map(p => p.remove()) : valuePath.remove();
811
855
  };
@@ -843,7 +887,17 @@ function createExtractor({
843
887
  inlined.set("theme", attr.value);
844
888
  return attr;
845
889
  }
890
+ if (isTargetingHTML && name === "group" && t.isStringLiteral(value)) {
891
+ return [];
892
+ }
846
893
  const styleValue = attemptEvalSafe(value);
894
+ if (name[0] === "$" && (name.startsWith("$theme-") || name.startsWith("$group-")) && styleValue && typeof styleValue === "object" && Object.keys(styleValue).some(k => k[0] === "$")) {
895
+ if (shouldPrintDebug) {
896
+ logger.info(` ! nested media-like key inside ${name}, deopt to runtime`);
897
+ }
898
+ inlined.set(name, true);
899
+ return attr;
900
+ }
847
901
  if (!variants[name] && !isValidStyleKey(name, staticConfig)) {
848
902
  let out = null;
849
903
  propMapper(name, styleValue, propMapperStyleState, false, (key, val) => {
@@ -906,12 +960,15 @@ function createExtractor({
906
960
  }
907
961
  if (isValidStyleKey(name, staticConfig)) {
908
962
  if (name[0] === "$") {
909
- if (name.startsWith("$theme-") || name.startsWith("$group-")) {
910
- if (shouldPrintDebug) {
911
- logger.info(` ! not flattening media-like style: ${name}`);
963
+ if (name.startsWith("$group-")) {
964
+ const groupName = name.slice("$group-".length).split("-")[0];
965
+ if (groupName && hasUntilMeasuredAncestor(path, groupName)) {
966
+ if (shouldPrintDebug) {
967
+ logger.info(` ! group="${groupName}" ancestor has untilMeasured, not flattening: ${name}`);
968
+ }
969
+ inlined.set(name, true);
970
+ return attr;
912
971
  }
913
- inlined.set(name, true);
914
- return attr;
915
972
  }
916
973
  if (name.startsWith("$platform-")) {
917
974
  const platformName = name.slice(10);
@@ -1211,7 +1268,14 @@ function createExtractor({
1211
1268
  ...(restProps.inlineProps || []), ...(staticConfig.inlineProps || [])]);
1212
1269
  const deoptProps = /* @__PURE__ */new Set([
1213
1270
  // always de-opt animation these
1214
- "animation", "animateOnly", "animatePresence", "disableOptimization", ...(!isTargetingHTML ? ["pressStyle", "focusStyle", "focusVisibleStyle", "focusWithinStyle", "disabledStyle"] : []),
1271
+ "animation", "animateOnly", "animatePresence", "disableOptimization", ...(!isTargetingHTML ? [
1272
+ // native has no css pseudo selectors; these are runtime-only on
1273
+ // rn (event listeners + style merge in createComponent), and if
1274
+ // we let them flatten into the stylesheet they get written
1275
+ // under a literal key that rn treats as an unknown style prop.
1276
+ // hover is intentionally excluded: it is no-op on native and
1277
+ // should be dropped rather than preserved as runtime work.
1278
+ "pressStyle", "focusStyle", "focusVisibleStyle", "focusWithinStyle", "disabledStyle"] : []),
1215
1279
  // when using a non-CSS driver, de-opt on enterStyle/exitStyle
1216
1280
  ...(tamaguiConfig?.animations.isReactNative ? ["enterStyle", "exitStyle"] : [])]);
1217
1281
  const inlineWhenUnflattened = new Set(staticConfig.inlineWhenUnflattened || []);
@@ -1328,7 +1392,31 @@ function createExtractor({
1328
1392
  neverFlatten: staticConfig.neverFlatten
1329
1393
  })}`);
1330
1394
  }
1331
- node.attributes = ogAttributes;
1395
+ let partial = null;
1396
+ if (platform2 === "native" && !staticConfig.isHOC && !staticConfig.isStyledHOC && !ogAttributes.some(a => t.isJSXSpreadAttribute(a) || t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === "style")) {
1397
+ const staticStyle = {};
1398
+ const consumed = /* @__PURE__ */new Set();
1399
+ for (const a of attrs) {
1400
+ if (a.type !== "style" || !a.name || !a.attr) continue;
1401
+ if (pseudoDescriptors[a.name]) continue;
1402
+ if (!t.isJSXAttribute(a.attr) || !t.isJSXIdentifier(a.attr.name)) continue;
1403
+ if (styleValueHasToken(a.value)) continue;
1404
+ Object.assign(staticStyle, a.value);
1405
+ consumed.add(a.attr.name.name);
1406
+ }
1407
+ if (consumed.size >= 2) {
1408
+ const kept = ogAttributes.filter(a => {
1409
+ if (!t.isJSXAttribute(a) || !t.isJSXIdentifier(a.name)) return true;
1410
+ const n = a.name.name;
1411
+ if (consumed.has(n)) return false;
1412
+ if (n === "hoverStyle") return false;
1413
+ if (n.startsWith("$group-") && getGroupPseudo(n) === "hover") return false;
1414
+ return true;
1415
+ });
1416
+ partial = [t.jsxAttribute(t.jsxIdentifier("style"), t.jsxExpressionContainer((0, import_literalToAst.literalToAst)(staticStyle))), ...kept];
1417
+ }
1418
+ }
1419
+ node.attributes = partial ?? ogAttributes;
1332
1420
  return;
1333
1421
  }
1334
1422
  let skipMap = false;
@@ -1566,7 +1654,25 @@ function createExtractor({
1566
1654
  const before = process.env.IS_STATIC;
1567
1655
  process.env.IS_STATIC = "is_static";
1568
1656
  try {
1569
- const out = getSplitStyles(props, staticConfig, defaultTheme, "", componentState, {
1657
+ let extractedMediaLikeProps = null;
1658
+ let propsForSplit = props;
1659
+ for (const k in props) {
1660
+ if (k[0] === "$" && (k.startsWith("$group-") || k.startsWith("$theme-"))) {
1661
+ if (propsForSplit === props) {
1662
+ propsForSplit = {
1663
+ ...props
1664
+ };
1665
+ }
1666
+ if (platform2 === "native" && k.startsWith("$group-") && getGroupPseudo(k) === "hover") {
1667
+ delete propsForSplit[k];
1668
+ continue;
1669
+ }
1670
+ extractedMediaLikeProps ||= {};
1671
+ extractedMediaLikeProps[k] = propsForSplit[k];
1672
+ delete propsForSplit[k];
1673
+ }
1674
+ }
1675
+ const out = getSplitStyles(propsForSplit, staticConfig, defaultTheme, "", componentState, {
1570
1676
  ...styleProps,
1571
1677
  noClass: true,
1572
1678
  fallbackProps: completeProps,
@@ -1574,15 +1680,36 @@ function createExtractor({
1574
1680
  resolveValues: "except-theme"
1575
1681
  })
1576
1682
  }, void 0, void 0, void 0, void 0, false, debugPropValue || shouldPrintDebug);
1683
+ if (extractedMediaLikeProps && platform2 !== "native") {
1684
+ for (const k in extractedMediaLikeProps) {
1685
+ const block = extractedMediaLikeProps[k];
1686
+ if (!block || typeof block !== "object") continue;
1687
+ const blockOut = getSplitStyles(block, staticConfig, defaultTheme, "", componentState, {
1688
+ ...styleProps,
1689
+ noClass: true,
1690
+ fallbackProps: completeProps
1691
+ }, void 0, void 0, void 0, void 0, false, debugPropValue || shouldPrintDebug);
1692
+ if (blockOut) {
1693
+ extractedMediaLikeProps[k] = {
1694
+ ...blockOut.style,
1695
+ ...blockOut.pseudos
1696
+ };
1697
+ }
1698
+ }
1699
+ }
1577
1700
  let outProps = {
1578
1701
  ...(includeProps ? out.viewProps : {}),
1579
1702
  ...out.style,
1580
- ...out.pseudos
1703
+ ...out.pseudos,
1704
+ ...extractedMediaLikeProps
1581
1705
  };
1582
1706
  for (const key in outProps) {
1583
1707
  if (deoptProps.has(key)) {
1584
1708
  shouldFlatten = false;
1585
1709
  }
1710
+ if (platform2 === "native" && key[0] === "$" && (key.startsWith("$theme-") || key.startsWith("$group-") && getGroupPseudo(key) !== "hover")) {
1711
+ shouldFlatten = false;
1712
+ }
1586
1713
  }
1587
1714
  if (shouldPrintDebug) {
1588
1715
  logger.info(`(${debugName})`);
@@ -136,6 +136,39 @@ async function extractToClassNames({
136
136
  let baseFontFamily = "";
137
137
  let mediaStylesSeen = 1;
138
138
  const comment = util.format("/* %s:%s (%s) */", filePath, lineNumbers, originalNodeName);
139
+ let staticGroupName = null;
140
+ for (const attr of jsxPath.node.openingElement.attributes) {
141
+ if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name)) continue;
142
+ if (attr.name.name !== "group") continue;
143
+ if (t.isStringLiteral(attr.value)) {
144
+ staticGroupName = attr.value.value;
145
+ } else if (t.isJSXExpressionContainer(attr.value) && t.isStringLiteral(attr.value.expression)) {
146
+ staticGroupName = attr.value.expression.value;
147
+ }
148
+ break;
149
+ }
150
+ if (staticGroupName) {
151
+ const containerIdentifier = `t_group_${staticGroupName}`;
152
+ const containerType = tamaguiConfig.settings?.webContainerType || "inline-size";
153
+ const containerSelector = `.${containerIdentifier}`;
154
+ if (!cssMap.has(containerSelector)) {
155
+ cssMap.set(containerSelector, {
156
+ css: `${containerSelector} { container-name: ${staticGroupName}; container-type: ${containerType}; }`,
157
+ commentTexts: [comment]
158
+ });
159
+ } else {
160
+ cssMap.get(containerSelector).commentTexts.push(comment);
161
+ }
162
+ }
163
+ let elementIsAnimated = false;
164
+ for (const attr of jsxPath.node.openingElement.attributes) {
165
+ if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name)) continue;
166
+ const n = attr.name.name;
167
+ if (n === "animation" || n === "animateOnly" || n === "animatePresence" || n === "animatedBy" || n === "enterStyle" || n === "exitStyle") {
168
+ elementIsAnimated = true;
169
+ break;
170
+ }
171
+ }
139
172
  function addStyle(style) {
140
173
  const identifier = style[import_web.StyleObjectIdentifier];
141
174
  const rules = style[import_web.StyleObjectRules];
@@ -158,12 +191,22 @@ async function extractToClassNames({
158
191
  const property = style2[0];
159
192
  const mediaName = property.slice(1);
160
193
  if (mediaName.startsWith("group-")) {
161
- throw new import_errors.BailOptimizationError();
194
+ if (elementIsAnimated) {
195
+ throw new import_errors.BailOptimizationError();
196
+ }
197
+ const mediaStyle = createMediaStyle(style2, mediaName, extractor.getTamagui().media, "group", false, mediaStylesSeen);
198
+ const identifier2 = addStyle(mediaStyle);
199
+ classNames.push(identifier2);
200
+ continue;
162
201
  }
163
202
  const mediaTypeMatch = mediaName.match(/^(theme|platform)-/);
164
203
  if (mediaTypeMatch) {
165
204
  const mediaType = mediaTypeMatch[1];
166
- const mediaStyle = createMediaStyle(style2, mediaName, extractor.getTamagui().media, mediaType, false, mediaStylesSeen);
205
+ if (mediaType === "theme" && elementIsAnimated) {
206
+ throw new import_errors.BailOptimizationError();
207
+ }
208
+ const innerKey = mediaType === "theme" ? mediaName.slice("theme-".length) : mediaName;
209
+ const mediaStyle = createMediaStyle(style2, innerKey, extractor.getTamagui().media, mediaType, false, mediaStylesSeen);
167
210
  const identifier2 = addStyle(mediaStyle);
168
211
  classNames.push(identifier2);
169
212
  continue;
@@ -249,6 +292,9 @@ async function extractToClassNames({
249
292
  if (baseFontFamily) {
250
293
  baseClassNameStr = `font_${baseFontFamily}${baseClassNameStr ? ` ${baseClassNameStr}` : ""}`;
251
294
  }
295
+ if (staticGroupName) {
296
+ baseClassNameStr = `t_group_${staticGroupName}${baseClassNameStr ? ` ${baseClassNameStr}` : ""}`;
297
+ }
252
298
  const baseTypeClass = staticConfig.isText ? "is_Text" : "is_View";
253
299
  baseClassNameStr = `${baseTypeClass}${baseClassNameStr ? ` ${baseClassNameStr}` : ""}`;
254
300
  const componentNameFinal = staticConfig.componentName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/static",
3
- "version": "2.3.3",
3
+ "version": "2.4.0-1783110557100",
4
4
  "gitHead": "a49cc7ea6b93ba384e77a4880ae48ac4a5635c14",
5
5
  "license": "MIT",
6
6
  "source": "src/index.ts",
@@ -50,19 +50,19 @@
50
50
  "@babel/template": "^7.25.0",
51
51
  "@babel/traverse": "^7.25.4",
52
52
  "@babel/types": "^7.25.4",
53
- "@tamagui/cli-color": "2.3.3",
54
- "@tamagui/config-default": "2.3.3",
55
- "@tamagui/core": "2.3.3",
56
- "@tamagui/fake-react-native": "2.3.3",
57
- "@tamagui/generate-themes": "2.3.3",
58
- "@tamagui/helpers": "2.3.3",
59
- "@tamagui/helpers-node": "2.3.3",
60
- "@tamagui/proxy-worm": "2.3.3",
61
- "@tamagui/react-native-web-internals": "2.3.3",
62
- "@tamagui/react-native-web-lite": "2.3.3",
63
- "@tamagui/shorthands": "2.3.3",
64
- "@tamagui/types": "2.3.3",
65
- "@tamagui/web": "2.3.3",
53
+ "@tamagui/cli-color": "2.4.0-1783110557100",
54
+ "@tamagui/config-default": "2.4.0-1783110557100",
55
+ "@tamagui/core": "2.4.0-1783110557100",
56
+ "@tamagui/fake-react-native": "2.4.0-1783110557100",
57
+ "@tamagui/generate-themes": "2.4.0-1783110557100",
58
+ "@tamagui/helpers": "2.4.0-1783110557100",
59
+ "@tamagui/helpers-node": "2.4.0-1783110557100",
60
+ "@tamagui/proxy-worm": "2.4.0-1783110557100",
61
+ "@tamagui/react-native-web-internals": "2.4.0-1783110557100",
62
+ "@tamagui/react-native-web-lite": "2.4.0-1783110557100",
63
+ "@tamagui/shorthands": "2.4.0-1783110557100",
64
+ "@tamagui/types": "2.4.0-1783110557100",
65
+ "@tamagui/web": "2.4.0-1783110557100",
66
66
  "babel-literal-to-ast": "^2.1.0",
67
67
  "browserslist": "^4.28.1",
68
68
  "check-dependency-version-consistency": "^4.1.0",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "devDependencies": {
81
81
  "@babel/plugin-syntax-typescript": "^7.25.4",
82
- "@tamagui/build": "2.3.3",
82
+ "@tamagui/build": "2.4.0-1783110557100",
83
83
  "@types/babel__core": "^7.20.5",
84
84
  "@types/find-root": "^1.1.2",
85
85
  "@types/node": "^22.1.0",
@@ -76,6 +76,53 @@ function isFullyDisabled(props: TamaguiOptions) {
76
76
  return props.disableExtraction && props.disableDebugAttr
77
77
  }
78
78
 
79
+ // a compile-evaluated style value carries a theme token if any leaf string is
80
+ // "$"-prefixed (e.g. "$gray2"). such styles must NOT be flattened into a static
81
+ // `style` object — the token is resolved at runtime so theme switching keeps working.
82
+ function styleValueHasToken(v: any): boolean {
83
+ if (typeof v === 'string') return v.charCodeAt(0) === 36 // '$'
84
+ if (Array.isArray(v)) return v.some(styleValueHasToken)
85
+ if (v && typeof v === 'object') return Object.values(v).some(styleValueHasToken)
86
+ return false
87
+ }
88
+
89
+ // Walk up JSX ancestors looking for one that declares `group="<groupName>"` together
90
+ // with the `untilMeasured` prop. Used to deopt children whose styles depend on a
91
+ // parent that the runtime measures before emitting child styles (can't be modeled
92
+ // in static CSS).
93
+ function hasUntilMeasuredAncestor(path: NodePath<any>, groupName: string): boolean {
94
+ let current: NodePath<any> | null = path.parentPath
95
+ while (current) {
96
+ if (current.isJSXElement()) {
97
+ const opening = current.node.openingElement
98
+ let foundGroup = false
99
+ let foundUntilMeasured = false
100
+ for (const attr of opening.attributes) {
101
+ if (!t.isJSXAttribute(attr)) continue
102
+ if (!t.isJSXIdentifier(attr.name)) continue
103
+ const aName = attr.name.name
104
+ if (aName === 'group') {
105
+ // only literal string equality counts — dynamic group= is left to runtime
106
+ if (t.isStringLiteral(attr.value) && attr.value.value === groupName) {
107
+ foundGroup = true
108
+ } else if (
109
+ t.isJSXExpressionContainer(attr.value) &&
110
+ t.isStringLiteral(attr.value.expression) &&
111
+ attr.value.expression.value === groupName
112
+ ) {
113
+ foundGroup = true
114
+ }
115
+ } else if (aName === 'untilMeasured') {
116
+ foundUntilMeasured = true
117
+ }
118
+ }
119
+ if (foundGroup && foundUntilMeasured) return true
120
+ }
121
+ current = current.parentPath
122
+ }
123
+ return false
124
+ }
125
+
79
126
  export function createExtractor(
80
127
  { logger = console, platform = 'web' }: ExtractorOptions = { logger: console }
81
128
  ) {
@@ -359,6 +406,22 @@ export function createExtractor(
359
406
  )
360
407
  }
361
408
 
409
+ function getGroupPseudo(name: string) {
410
+ const [_, groupName, a, b, c] = name.split('-')
411
+ if (!groupName) return
412
+ const m2 = a && b ? `${a}-${b}` : ''
413
+ const media = (m2 && mediaQueryConfig[m2] && m2) || (a && mediaQueryConfig[a] && a)
414
+ return media
415
+ ? media === m2
416
+ ? c
417
+ : b
418
+ ? `${b}${c ? `-${c}` : ''}`
419
+ : undefined
420
+ : a
421
+ ? `${a}${b ? `-${b}` : ''}${c ? `-${c}` : ''}`
422
+ : undefined
423
+ }
424
+
362
425
  /**
363
426
  * Step 1: Determine if importing any statically extractable components
364
427
  */
@@ -1095,6 +1158,12 @@ export function createExtractor(
1095
1158
 
1096
1159
  ...(!isTargetingHTML
1097
1160
  ? [
1161
+ // native has no css pseudo selectors; these are runtime-only on
1162
+ // rn (event listeners + style merge in createComponent), and if
1163
+ // we let them flatten into the stylesheet they get written
1164
+ // under a literal key that rn treats as an unknown style prop.
1165
+ // hover is intentionally excluded: it is no-op on native and
1166
+ // should be dropped rather than preserved as runtime work.
1098
1167
  'pressStyle',
1099
1168
  'focusStyle',
1100
1169
  'focusVisibleStyle',
@@ -1368,6 +1437,20 @@ export function createExtractor(
1368
1437
  return [attribute.value!, path.get('value')!] as const
1369
1438
  })()
1370
1439
 
1440
+ // these props have runtime-only meaning on native. decide from the
1441
+ // original jsx attr, before getSplitStyles has a chance to drop
1442
+ // native-dead work like hoverStyle from its static output.
1443
+ if (
1444
+ deoptProps.has(name) ||
1445
+ (platform === 'native' &&
1446
+ name[0] === '$' &&
1447
+ (name.startsWith('$theme-') ||
1448
+ (name.startsWith('$group-') && getGroupPseudo(name) !== 'hover')))
1449
+ ) {
1450
+ inlined.set(name, true)
1451
+ return attr
1452
+ }
1453
+
1371
1454
  const remove = () => {
1372
1455
  Array.isArray(valuePath)
1373
1456
  ? valuePath.map((p) => p.remove())
@@ -1420,9 +1503,33 @@ export function createExtractor(
1420
1503
  return attr
1421
1504
  }
1422
1505
 
1506
+ // static `group="<literal>"` is handled at compile-time in
1507
+ // extractToClassNames (emits container CSS + adds `t_group_<name>`
1508
+ // className), so we drop the JSX attribute here without bailing
1509
+ // flattening. dynamic `group={expr}` falls through to runtime.
1510
+ if (isTargetingHTML && name === 'group' && t.isStringLiteral(value)) {
1511
+ return []
1512
+ }
1513
+
1423
1514
  // if value can be evaluated, extract it and filter it out
1424
1515
  const styleValue = attemptEvalSafe(value)
1425
1516
 
1517
+ // media-like blocks with nested $-keys (eg $theme-dark={{ $sm: {…} }})
1518
+ // can't resolve to static CSS in one pass — keep them on the runtime path
1519
+ if (
1520
+ name[0] === '$' &&
1521
+ (name.startsWith('$theme-') || name.startsWith('$group-')) &&
1522
+ styleValue &&
1523
+ typeof styleValue === 'object' &&
1524
+ Object.keys(styleValue).some((k) => k[0] === '$')
1525
+ ) {
1526
+ if (shouldPrintDebug) {
1527
+ logger.info(` ! nested media-like key inside ${name}, deopt to runtime`)
1528
+ }
1529
+ inlined.set(name, true)
1530
+ return attr
1531
+ }
1532
+
1426
1533
  // never flatten if a prop isn't a valid static attribute
1427
1534
  // only post prop-mapping
1428
1535
  if (!variants[name] && !isValidStyleKey(name, staticConfig)) {
@@ -1525,15 +1632,25 @@ export function createExtractor(
1525
1632
  }
1526
1633
 
1527
1634
  if (isValidStyleKey(name, staticConfig)) {
1528
- // $theme-, $group- styles should not be flattened (needs runtime handling)
1529
- // $platform- can be flattened if the platform matches
1635
+ // $theme- / $group- styles extract through the atomic-CSS pipeline
1636
+ // (extractToClassNames createMediaStyle), so they fall through to
1637
+ // the normal style return below. The one case we still bail is
1638
+ // $group-<name>-* when an ancestor element declares `group="<name>"`
1639
+ // together with `untilMeasured` — the runtime measures the parent
1640
+ // and only then emits child styles, which can't be modeled in CSS.
1641
+ // $platform- can be flattened if the platform matches.
1530
1642
  if (name[0] === '$') {
1531
- if (name.startsWith('$theme-') || name.startsWith('$group-')) {
1532
- if (shouldPrintDebug) {
1533
- logger.info(` ! not flattening media-like style: ${name}`)
1643
+ if (name.startsWith('$group-')) {
1644
+ const groupName = name.slice('$group-'.length).split('-')[0]
1645
+ if (groupName && hasUntilMeasuredAncestor(path, groupName)) {
1646
+ if (shouldPrintDebug) {
1647
+ logger.info(
1648
+ ` ! group="${groupName}" ancestor has untilMeasured, not flattening: ${name}`
1649
+ )
1650
+ }
1651
+ inlined.set(name, true)
1652
+ return attr
1534
1653
  }
1535
- inlined.set(name, true)
1536
- return attr
1537
1654
  }
1538
1655
 
1539
1656
  // $platform-web, $platform-native, $platform-ios, $platform-android, $platform-tv, $platform-androidtv, $platform-tvos
@@ -1901,7 +2018,6 @@ export function createExtractor(
1901
2018
  })
1902
2019
 
1903
2020
  if (!shouldFlatten) {
1904
- // were no longer partially optimizing, it adds a lot of complexity for dubious performance
1905
2021
  if (shouldPrintDebug) {
1906
2022
  logger.info(
1907
2023
  `Deopting ${JSON.stringify({
@@ -1913,7 +2029,55 @@ export function createExtractor(
1913
2029
  })}`
1914
2030
  )
1915
2031
  }
1916
- node.attributes = ogAttributes
2032
+ // PARTIAL FLATTEN (native): even when the element must stay on the runtime
2033
+ // path (pseudo/group/dynamic keep it deopted), pre-merge the pure-static
2034
+ // style props into a single `style={…}` so the runtime skips its per-prop
2035
+ // loop for them (the dominant deopt cost on RN). theme tokens ($…) and
2036
+ // dynamic props stay inline so theme/media switching + dynamics are
2037
+ // unaffected; dead native hoverStyle is dropped (no-op on touch).
2038
+ let partial: (t.JSXAttribute | t.JSXSpreadAttribute)[] | null = null
2039
+ if (
2040
+ platform === 'native' &&
2041
+ !staticConfig.isHOC &&
2042
+ !staticConfig.isStyledHOC &&
2043
+ !ogAttributes.some(
2044
+ (a) =>
2045
+ t.isJSXSpreadAttribute(a) ||
2046
+ (t.isJSXAttribute(a) &&
2047
+ t.isJSXIdentifier(a.name) &&
2048
+ a.name.name === 'style')
2049
+ )
2050
+ ) {
2051
+ const staticStyle: Record<string, any> = {}
2052
+ const consumed = new Set<string>()
2053
+ for (const a of attrs) {
2054
+ if (a.type !== 'style' || !a.name || !a.attr) continue
2055
+ if (pseudoDescriptors[a.name]) continue
2056
+ if (!t.isJSXAttribute(a.attr) || !t.isJSXIdentifier(a.attr.name)) continue
2057
+ if (styleValueHasToken(a.value)) continue
2058
+ Object.assign(staticStyle, a.value)
2059
+ consumed.add(a.attr.name.name)
2060
+ }
2061
+ if (consumed.size >= 2) {
2062
+ const kept = ogAttributes.filter((a) => {
2063
+ if (!t.isJSXAttribute(a) || !t.isJSXIdentifier(a.name)) return true
2064
+ const n = a.name.name
2065
+ if (consumed.has(n)) return false
2066
+ if (n === 'hoverStyle') return false
2067
+ if (n.startsWith('$group-') && getGroupPseudo(n) === 'hover')
2068
+ return false
2069
+ return true
2070
+ })
2071
+ partial = [
2072
+ t.jsxAttribute(
2073
+ t.jsxIdentifier('style'),
2074
+ t.jsxExpressionContainer(literalToAst(staticStyle) as t.Expression)
2075
+ ),
2076
+ ...kept,
2077
+ ]
2078
+ }
2079
+ }
2080
+ node.attributes = partial ?? ogAttributes
1917
2081
  return
1918
2082
  }
1919
2083
 
@@ -2302,8 +2466,37 @@ export function createExtractor(
2302
2466
  const before = process.env.IS_STATIC
2303
2467
  process.env.IS_STATIC = 'is_static'
2304
2468
  try {
2469
+ // $group-* / $theme-* keys carry block-form style objects that
2470
+ // getSplitStyles drops in static mode (no parent group context,
2471
+ // no theme value to read). Pluck them out so the atomic-CSS
2472
+ // pipeline in extractToClassNames can emit @container / theme
2473
+ // rules for them directly.
2474
+ let extractedMediaLikeProps: Record<string, any> | null = null
2475
+ let propsForSplit: any = props
2476
+ for (const k in props) {
2477
+ if (
2478
+ k[0] === '$' &&
2479
+ (k.startsWith('$group-') || k.startsWith('$theme-'))
2480
+ ) {
2481
+ if (propsForSplit === props) {
2482
+ propsForSplit = { ...props }
2483
+ }
2484
+ if (
2485
+ platform === 'native' &&
2486
+ k.startsWith('$group-') &&
2487
+ getGroupPseudo(k) === 'hover'
2488
+ ) {
2489
+ delete propsForSplit[k]
2490
+ continue
2491
+ }
2492
+ extractedMediaLikeProps ||= {}
2493
+ extractedMediaLikeProps[k] = propsForSplit[k]
2494
+ delete propsForSplit[k]
2495
+ }
2496
+ }
2497
+
2305
2498
  const out = getSplitStyles(
2306
- props,
2499
+ propsForSplit,
2307
2500
  staticConfig,
2308
2501
  defaultTheme,
2309
2502
  '',
@@ -2324,10 +2517,47 @@ export function createExtractor(
2324
2517
  debugPropValue || shouldPrintDebug
2325
2518
  )!
2326
2519
 
2520
+ // resolve tokens inside the plucked blocks: they skipped the main
2521
+ // split, so values like "$color5" would flow raw into the emitted
2522
+ // CSS where browsers reject the declaration. run each block through
2523
+ // its own static split so tokens become var(--x) references.
2524
+ // (native never extracts these — it deopts below — so web only.)
2525
+ if (extractedMediaLikeProps && platform !== 'native') {
2526
+ for (const k in extractedMediaLikeProps) {
2527
+ const block = extractedMediaLikeProps[k]
2528
+ if (!block || typeof block !== 'object') continue
2529
+ const blockOut = getSplitStyles(
2530
+ block,
2531
+ staticConfig,
2532
+ defaultTheme,
2533
+ '',
2534
+ componentState,
2535
+ {
2536
+ ...styleProps,
2537
+ noClass: true,
2538
+ fallbackProps: completeProps,
2539
+ },
2540
+ undefined,
2541
+ undefined,
2542
+ undefined,
2543
+ undefined,
2544
+ false,
2545
+ debugPropValue || shouldPrintDebug
2546
+ )
2547
+ if (blockOut) {
2548
+ extractedMediaLikeProps[k] = {
2549
+ ...blockOut.style,
2550
+ ...blockOut.pseudos,
2551
+ }
2552
+ }
2553
+ }
2554
+ }
2555
+
2327
2556
  let outProps = {
2328
2557
  ...(includeProps ? out.viewProps : {}),
2329
2558
  ...out.style,
2330
2559
  ...out.pseudos,
2560
+ ...extractedMediaLikeProps,
2331
2561
  }
2332
2562
 
2333
2563
  // check de-opt props again
@@ -2335,6 +2565,20 @@ export function createExtractor(
2335
2565
  if (deoptProps.has(key)) {
2336
2566
  shouldFlatten = false
2337
2567
  }
2568
+ // native has no atomic-CSS sink for theme- / group- pseudo
2569
+ // blocks; if they flatten they get serialized under the literal
2570
+ // `$theme-…` / `$group-…` key into the RN StyleSheet, where the
2571
+ // runtime's @container / theme-name matching never runs. de-opt
2572
+ // → preserve as inline prop so getSplitStyles handles them at
2573
+ // render time.
2574
+ if (
2575
+ platform === 'native' &&
2576
+ key[0] === '$' &&
2577
+ (key.startsWith('$theme-') ||
2578
+ (key.startsWith('$group-') && getGroupPseudo(key) !== 'hover'))
2579
+ ) {
2580
+ shouldFlatten = false
2581
+ }
2338
2582
  }
2339
2583
 
2340
2584
  if (shouldPrintDebug) {
@@ -170,6 +170,72 @@ export async function extractToClassNames({
170
170
  originalNodeName
171
171
  )
172
172
 
173
+ // detect static `group="<name>"` literal — runtime side normally emits the
174
+ // container-name/container-type CSS lazily via getSplitStyles (createComponent
175
+ // path), but extracted elements never go through that code, so we emit the
176
+ // equivalent CSS at compile time and add `t_group_<name>` to the className.
177
+ let staticGroupName: string | null = null
178
+ for (const attr of jsxPath.node.openingElement.attributes) {
179
+ if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name)) continue
180
+ if (attr.name.name !== 'group') continue
181
+ if (t.isStringLiteral(attr.value)) {
182
+ staticGroupName = attr.value.value
183
+ } else if (
184
+ t.isJSXExpressionContainer(attr.value) &&
185
+ t.isStringLiteral(attr.value.expression)
186
+ ) {
187
+ staticGroupName = attr.value.expression.value
188
+ }
189
+ // dynamic group={someProp} falls back to the runtime path.
190
+ break
191
+ }
192
+ if (staticGroupName) {
193
+ const containerIdentifier = `t_group_${staticGroupName}`
194
+ const containerType =
195
+ (tamaguiConfig.settings?.webContainerType as string | undefined) ||
196
+ 'inline-size'
197
+ const containerSelector = `.${containerIdentifier}`
198
+ if (!cssMap.has(containerSelector)) {
199
+ cssMap.set(containerSelector, {
200
+ css: `${containerSelector} { container-name: ${staticGroupName}; container-type: ${containerType}; }`,
201
+ commentTexts: [comment],
202
+ })
203
+ } else {
204
+ cssMap.get(containerSelector)!.commentTexts.push(comment)
205
+ }
206
+ }
207
+
208
+ // invariant guard for $group-/$theme- CSS extraction (below): those styles
209
+ // can only be extracted to static @container / theme CSS when a class toggle
210
+ // alone expresses the change. JS animation drivers (reanimated, motion, moti,
211
+ // react-native) interpolate in JS and need the runtime path to read hard
212
+ // values when the element animates — a static class can't drive them. gated
213
+ // on whether THIS element animates rather than on the driver, because motion
214
+ // is isReactNative:false yet still needs JS.
215
+ //
216
+ // in practice createExtractor's deoptProps already de-opts every animated
217
+ // element upstream (`animation`/`animateOnly`/`animatePresence` always;
218
+ // enterStyle/exitStyle on RN drivers), so a flattened element reaching here
219
+ // is normally non-animated and this never fires. it keeps the invariant
220
+ // local and correct regardless of future deoptProps changes — it only ever
221
+ // bails the animated case to runtime, never widens what extracts.
222
+ let elementIsAnimated = false
223
+ for (const attr of jsxPath.node.openingElement.attributes) {
224
+ if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name)) continue
225
+ const n = attr.name.name
226
+ if (
227
+ n === 'animation' ||
228
+ n === 'animateOnly' ||
229
+ n === 'animatePresence' ||
230
+ n === 'animatedBy' ||
231
+ n === 'enterStyle' ||
232
+ n === 'exitStyle'
233
+ ) {
234
+ elementIsAnimated = true
235
+ break
236
+ }
237
+ }
238
+
173
239
  function addStyle(style: StyleObject) {
174
240
  const identifier = style[StyleObjectIdentifier]
175
241
  const rules = style[StyleObjectRules]
@@ -194,20 +260,46 @@ export async function extractToClassNames({
194
260
  const property = style[0]
195
261
  const mediaName = property.slice(1)
196
262
 
197
- // $group- styles must bail out entirely - they need runtime handling because
198
- // group changes can affect children that may be animated and need hard values.
199
- // In the future, CSS animation drivers could potentially optimize this.
263
+ // $group- styles extract through createMediaStyle which emits @container
264
+ // rules keyed on the parent's `t_group_<name>` className.
200
265
  if (mediaName.startsWith('group-')) {
201
- throw new BailOptimizationError()
266
+ // a class toggle can't drive a JS animation driver's interpolation;
267
+ // keep animated elements on the runtime path (matches pre-extraction
268
+ // behavior for the group case).
269
+ if (elementIsAnimated) {
270
+ throw new BailOptimizationError()
271
+ }
272
+ const mediaStyle = createMediaStyle(
273
+ style,
274
+ mediaName,
275
+ extractor.getTamagui()!.media,
276
+ 'group',
277
+ false,
278
+ mediaStylesSeen
279
+ )
280
+ const identifier = addStyle(mediaStyle)
281
+ classNames.push(identifier)
282
+ continue
202
283
  }
203
284
 
204
285
  // Check for theme/platform media queries (e.g., $theme-dark, $platform-web)
205
286
  const mediaTypeMatch = mediaName.match(/^(theme|platform)-/)
206
287
  if (mediaTypeMatch) {
207
288
  const mediaType = mediaTypeMatch[1] as 'theme' | 'platform'
289
+ // $theme- values can change at runtime (theme switch); if this
290
+ // element animates, a JS driver must interpolate the change, so keep
291
+ // it on the runtime path. $platform- is build-time static — never gated.
292
+ if (mediaType === 'theme' && elementIsAnimated) {
293
+ throw new BailOptimizationError()
294
+ }
295
+ // createMediaStyle internally calls getGroupPropParts(`theme-` + key)
296
+ // for theme, so we must pass the short key ("dark", not "theme-dark")
297
+ // to avoid double-prefixing. Platform stays as-is (runtime parity).
298
+ const innerKey =
299
+ mediaType === 'theme' ? mediaName.slice('theme-'.length) : mediaName
208
300
  const mediaStyle = createMediaStyle(
209
301
  style,
210
- mediaName,
302
+ innerKey,
211
303
  extractor.getTamagui()!.media,
212
304
  mediaType,
213
305
  false,
@@ -343,6 +435,13 @@ export async function extractToClassNames({
343
435
  baseClassNameStr = `font_${baseFontFamily}${baseClassNameStr ? ` ${baseClassNameStr}` : ''}`
344
436
  }
345
437
 
438
+ // add t_group_<name> for elements with a static group="<name>" literal so
439
+ // descendants' @container <name> rules can match (runtime path normally
440
+ // appends this class; extracted elements bypass it).
441
+ if (staticGroupName) {
442
+ baseClassNameStr = `t_group_${staticGroupName}${baseClassNameStr ? ` ${baseClassNameStr}` : ''}`
443
+ }
444
+
346
445
  // add is_View or is_Text base class matching runtime behavior
347
446
  const baseTypeClass = staticConfig.isText ? 'is_Text' : 'is_View'
348
447
  baseClassNameStr = `${baseTypeClass}${baseClassNameStr ? ` ${baseClassNameStr}` : ''}`
@@ -1 +1 @@
1
- {"version":3,"file":"createExtractor.d.ts","sourceRoot":"","sources":["../../src/extractor/createExtractor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,iBAAiB,CAAA;AAEhE,OAAO,KAAK,CAAC,MAAM,cAAc,CAAA;AAmBjC,OAAO,KAAK,EAGV,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EAGf,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAAoB,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAc1E,OAAO,EAAE,iBAAiB,EAA6B,MAAM,6BAA6B,CAAA;AAwB1F,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAA;AAE1D,KAAK,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAQ9C,wBAAgB,eAAe,CAC7B,EAAE,MAAgB,EAAE,QAAgB,EAAE,GAAE,gBAAsC;;;;;yBAsJnD,cAAc;6BAPhB,cAAc;;mBAwBtB,UAAU,SAAS,mBAAmB;;;;;;;eAKpC,UAAU,SAAS,mBAAmB;;;;;;;EAu5E1D"}
1
+ {"version":3,"file":"createExtractor.d.ts","sourceRoot":"","sources":["../../src/extractor/createExtractor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,iBAAiB,CAAA;AAEhE,OAAO,KAAK,CAAC,MAAM,cAAc,CAAA;AAmBjC,OAAO,KAAK,EAGV,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EAGf,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAAoB,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAc1E,OAAO,EAAE,iBAAiB,EAA6B,MAAM,6BAA6B,CAAA;AAwB1F,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAA;AAE1D,KAAK,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAuD9C,wBAAgB,eAAe,CAC7B,EAAE,MAAgB,EAAE,QAAgB,EAAE,GAAE,gBAAsC;;;;;yBAsJnD,cAAc;6BAPhB,cAAc;;mBAwBtB,UAAU,SAAS,mBAAmB;;;;;;;eAKpC,UAAU,SAAS,mBAAmB;;;;;;;EA4lF1D"}
@@ -1 +1 @@
1
- {"version":3,"file":"extractToClassNames.d.ts","sourceRoot":"","sources":["../../src/extractor/extractToClassNames.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAA;AAKjC,OAAO,KAAK,EAAe,cAAc,EAAW,MAAM,UAAU,CAAA;AAEpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAYlD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,CAAC,CAAC,IAAI,CAAA;IACX,GAAG,EAAE,GAAG,CAAA;IACR,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAA;QACd,SAAS,EAAE,MAAM,CAAA;QACjB,SAAS,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,MAAM,CAAA;KACd,CAAA;CACF,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,cAAc,CAAA;IACvB,gBAAgB,EAAE,OAAO,GAAG,SAAS,CAAA;CACtC,CAAA;AAQD,wBAAsB,mBAAmB,CAAC,EACxC,SAAS,EACT,MAAM,EACN,UAAe,EACf,OAAO,EACP,gBAAgB,GACjB,EAAE,wBAAwB,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CA2gB9D"}
1
+ {"version":3,"file":"extractToClassNames.d.ts","sourceRoot":"","sources":["../../src/extractor/extractToClassNames.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAA;AAKjC,OAAO,KAAK,EAAe,cAAc,EAAW,MAAM,UAAU,CAAA;AAEpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAYlD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,CAAC,CAAC,IAAI,CAAA;IACX,GAAG,EAAE,GAAG,CAAA;IACR,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAA;QACd,SAAS,EAAE,MAAM,CAAA;QACjB,SAAS,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,MAAM,CAAA;KACd,CAAA;CACF,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,cAAc,CAAA;IACvB,gBAAgB,EAAE,OAAO,GAAG,SAAS,CAAA;CACtC,CAAA;AAQD,wBAAsB,mBAAmB,CAAC,EACxC,SAAS,EACT,MAAM,EACN,UAAe,EACf,OAAO,EACP,gBAAgB,GACjB,EAAE,wBAAwB,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CA8mB9D"}