@tamagui/tailwind 0.0.0-bootstrap.0 → 3.0.0-beta.1097.1

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 (97) hide show
  1. package/dist/cjs/candidate.cjs +540 -0
  2. package/dist/cjs/candidate.native.cjs +578 -0
  3. package/dist/cjs/candidate.native.js +580 -0
  4. package/dist/cjs/candidate.native.js.map +1 -0
  5. package/dist/cjs/compose.cjs +352 -0
  6. package/dist/cjs/compose.native.cjs +358 -0
  7. package/dist/cjs/compose.native.js +360 -0
  8. package/dist/cjs/compose.native.js.map +1 -0
  9. package/dist/cjs/composedResolver.cjs +175 -0
  10. package/dist/cjs/composedResolver.native.cjs +201 -0
  11. package/dist/cjs/composedResolver.native.js +203 -0
  12. package/dist/cjs/composedResolver.native.js.map +1 -0
  13. package/dist/cjs/frontend.cjs +54 -0
  14. package/dist/cjs/frontend.native.cjs +62 -0
  15. package/dist/cjs/frontend.native.js +64 -0
  16. package/dist/cjs/frontend.native.js.map +1 -0
  17. package/dist/cjs/index.cjs +38 -0
  18. package/dist/cjs/index.native.cjs +40 -0
  19. package/dist/cjs/index.native.js +42 -0
  20. package/dist/cjs/index.native.js.map +1 -0
  21. package/dist/cjs/styled.cjs +29 -0
  22. package/dist/cjs/styled.native.cjs +31 -0
  23. package/dist/cjs/styled.native.js +33 -0
  24. package/dist/cjs/styled.native.js.map +1 -0
  25. package/dist/cjs/types.cjs +16 -0
  26. package/dist/cjs/types.native.cjs +18 -0
  27. package/dist/cjs/types.native.js +20 -0
  28. package/dist/cjs/types.native.js.map +1 -0
  29. package/dist/cjs/vite/state.cjs +249 -0
  30. package/dist/cjs/vite/state.native.cjs +331 -0
  31. package/dist/cjs/vite/state.native.js +333 -0
  32. package/dist/cjs/vite/state.native.js.map +1 -0
  33. package/dist/cjs/vite.cjs +147 -0
  34. package/dist/cjs/vite.native.cjs +187 -0
  35. package/dist/cjs/vite.native.js +189 -0
  36. package/dist/cjs/vite.native.js.map +1 -0
  37. package/dist/esm/candidate.mjs +514 -0
  38. package/dist/esm/candidate.mjs.map +1 -0
  39. package/dist/esm/candidate.native.js +550 -0
  40. package/dist/esm/candidate.native.js.map +1 -0
  41. package/dist/esm/compose.mjs +328 -0
  42. package/dist/esm/compose.mjs.map +1 -0
  43. package/dist/esm/compose.native.js +332 -0
  44. package/dist/esm/compose.native.js.map +1 -0
  45. package/dist/esm/composedResolver.mjs +155 -0
  46. package/dist/esm/composedResolver.mjs.map +1 -0
  47. package/dist/esm/composedResolver.native.js +179 -0
  48. package/dist/esm/composedResolver.native.js.map +1 -0
  49. package/dist/esm/frontend.mjs +32 -0
  50. package/dist/esm/frontend.mjs.map +1 -0
  51. package/dist/esm/frontend.native.js +38 -0
  52. package/dist/esm/frontend.native.js.map +1 -0
  53. package/dist/esm/index.mjs +11 -0
  54. package/dist/esm/index.mjs.map +1 -0
  55. package/dist/esm/index.native.js +11 -0
  56. package/dist/esm/index.native.js.map +1 -0
  57. package/dist/esm/styled.mjs +10 -0
  58. package/dist/esm/styled.mjs.map +1 -0
  59. package/dist/esm/styled.native.js +10 -0
  60. package/dist/esm/styled.native.js.map +1 -0
  61. package/dist/esm/types.mjs +0 -0
  62. package/dist/esm/types.native.js +0 -0
  63. package/dist/esm/vite/state.mjs +216 -0
  64. package/dist/esm/vite/state.mjs.map +1 -0
  65. package/dist/esm/vite/state.native.js +296 -0
  66. package/dist/esm/vite/state.native.js.map +1 -0
  67. package/dist/esm/vite.mjs +116 -0
  68. package/dist/esm/vite.mjs.map +1 -0
  69. package/dist/esm/vite.native.js +154 -0
  70. package/dist/esm/vite.native.js.map +1 -0
  71. package/package.json +90 -5
  72. package/src/candidate.ts +805 -0
  73. package/src/frontend.ts +73 -0
  74. package/src/index.tsx +35 -0
  75. package/src/styled.tsx +52 -0
  76. package/src/types.ts +249 -0
  77. package/src/vite/state.ts +274 -0
  78. package/src/vite.ts +204 -0
  79. package/types/candidate.d.ts +18 -0
  80. package/types/candidate.d.ts.map +1 -0
  81. package/types/compose.d.ts +19 -0
  82. package/types/compose.d.ts.map +1 -0
  83. package/types/composedResolver.d.ts +18 -0
  84. package/types/composedResolver.d.ts.map +1 -0
  85. package/types/frontend.d.ts +19 -0
  86. package/types/frontend.d.ts.map +1 -0
  87. package/types/index.d.ts +17 -0
  88. package/types/index.d.ts.map +1 -0
  89. package/types/styled.d.ts +12 -0
  90. package/types/styled.d.ts.map +1 -0
  91. package/types/types.d.ts +169 -0
  92. package/types/types.d.ts.map +1 -0
  93. package/types/vite/state.d.ts +28 -0
  94. package/types/vite/state.d.ts.map +1 -0
  95. package/types/vite.d.ts +20 -0
  96. package/types/vite.d.ts.map +1 -0
  97. package/README.md +0 -1
@@ -0,0 +1,805 @@
1
+ import { isWeb } from '@tamagui/constants'
2
+ import { stylePropsUnitless } from '@tamagui/helpers'
3
+ import {
4
+ mergeFrontendCondition,
5
+ plainValueToPayload,
6
+ type FrontendClassPlan,
7
+ type FrontendClassPlanEntry,
8
+ type FrontendClassSink,
9
+ type StyleFrontendConfig,
10
+ } from '@tamagui/core/internal-runtime'
11
+ import { noteBoxShadow, noteTailwindTransform, tryCompose } from './compose'
12
+ import { composedResolver } from './composedResolver'
13
+ import {
14
+ borderSideSuffix,
15
+ configRevisionSymbol,
16
+ createGrammarConfigView,
17
+ getTokenCategory,
18
+ getSafeAreaEdge,
19
+ insetAxisProps,
20
+ percentUtilityProps,
21
+ radiusCornerProps,
22
+ sizeUtilityProps,
23
+ splitColorOpacitySuffix,
24
+ } from '@tamagui/style-grammar/runtime'
25
+ import {
26
+ classifyCandidate,
27
+ decodeArbitrary,
28
+ type GrammarConfigView,
29
+ type ParsedCandidate,
30
+ resolveTokenName,
31
+ } from '@tamagui/style-grammar/tooling/candidate'
32
+
33
+ /**
34
+ * Class-string tokenization and candidate adaptation. This is the Tailwind
35
+ * frontend's private half of the pipeline: everything here is about how a
36
+ * candidate is SPELLED (brackets, slashes, fractions, keywords, negatives).
37
+ *
38
+ * The semantic parser itself stays in `@tamagui/style-grammar`, shared with the
39
+ * converter and build tooling, and everything downstream of the props this module
40
+ * produces — value programs, merging, lowering, evaluation — is core's shared
41
+ * renderer.
42
+ */
43
+
44
+ const styleGrammarConfigCache = new WeakMap<
45
+ StyleFrontendConfig,
46
+ { revision: number; value: GrammarConfigView }
47
+ >()
48
+
49
+ export function getStyleGrammarConfig(config: StyleFrontendConfig): GrammarConfigView {
50
+ const cached = styleGrammarConfigCache.get(config)
51
+ const revision = (config as any)[configRevisionSymbol]?.revision || 0
52
+ if (cached && cached.revision === revision) return cached.value
53
+ const view = createGrammarConfigView(config)
54
+ styleGrammarConfigCache.set(config, { revision, value: view })
55
+ return view
56
+ }
57
+
58
+ export function isTokenValueProp(prop: string): boolean {
59
+ return getTokenCategory(prop) !== null
60
+ }
61
+
62
+ // CANONICAL arbitrary-value coercion. an arbitrary `[..]` whose inner is a UNITLESS number
63
+ // (z-[400], aspect-[1.5], leading-[1.25]) or a PX length (p-[18px], text-[14px], border-[0.5px])
64
+ // resolves to a NUMBER: React Native REQUIRES numbers for dimensional + typography props and
65
+ // silently DROPS "Npx"/"400" strings (Yoga parseCSSProperty<CSSNumber,CSSPercentage> rejects px;
66
+ // StyleSheetTypes types fontSize/lineHeight/letterSpacing as number). web accepts the number and
67
+ // re-adds px via CSS. anything carrying a real unit/function (%, rem, vh, deg, calc(), var(),
68
+ // #hex, colors) stays a STRING.
69
+ function arbitraryValue(inner: string, prop?: string): number | string {
70
+ const px = /^(-?\d*\.?\d+)px$/.exec(inner)
71
+ // a unitless prop (grid-cols-[77px]) never gets px re-added, so the length stays a string
72
+ if (px) return prop && stylePropsUnitless[prop] ? inner : Number(px[1])
73
+ if (/^-?\d*\.?\d+$/.test(inner)) return Number(inner)
74
+ return inner
75
+ }
76
+
77
+ /** the same coercion for a raw value that may still be wrapped in `[]` */
78
+ function unwrapArbitraryValue(raw: string): number | string {
79
+ return arbitraryValue(
80
+ raw.length > 1 && raw[0] === '[' && raw[raw.length - 1] === ']'
81
+ ? decodeArbitrary(raw.slice(1, -1))
82
+ : raw
83
+ )
84
+ }
85
+
86
+ // tailwind sizing keywords / fractions for width/height/min/max props.
87
+ // returns a CSS value, or null if `value` isn't a sizing keyword (falls through to normal parse).
88
+ function tailwindSizingValue(prop: string, value: string): string | null {
89
+ if (value === 'full') return '100%'
90
+ if (value === 'auto') return 'auto'
91
+ if (value === 'screen') return /[Hh]eight|[Bb]lock/.test(prop) ? '100vh' : '100vw'
92
+ if (value === 'min') return 'min-content'
93
+ if (value === 'max') return 'max-content'
94
+ if (value === 'fit') return 'fit-content'
95
+ const frac = /^(\d+)\/(\d+)$/.exec(value)
96
+ if (frac && Number(frac[2]) !== 0) {
97
+ return `${(Number(frac[1]) / Number(frac[2])) * 100}%`
98
+ }
99
+ return null
100
+ }
101
+
102
+ /**
103
+ * Prefixes that write more than one longhand from a single candidate. The registry
104
+ * already selected the token category and width-vs-color meaning; this only names
105
+ * the affected props. Values are converted per longhand so size-screen can be
106
+ * 100vw by 100vh.
107
+ */
108
+ function expansionProps(parsed: ParsedCandidate): readonly string[] | null {
109
+ const prefix = parsed.prefix
110
+ if (!prefix) return null
111
+ if (prefix === 'size') return sizeUtilityProps
112
+ if (prefix === 'translate') return ['x', 'y']
113
+ if (prefix === 'skew') return ['skewX', 'skewY']
114
+ if (prefix === 'inset-x' || prefix === 'inset-y') {
115
+ return insetAxisProps[prefix.slice('inset-'.length)]
116
+ }
117
+ if (prefix.startsWith('rounded-')) {
118
+ const props = radiusCornerProps[prefix.slice('rounded-'.length)]
119
+ if (!props || props.length === 1) return null
120
+ return props
121
+ }
122
+ const m = /^border-([trblxy])$/.exec(prefix)
123
+ if (!m) return null
124
+ const sides = borderSideSuffix[m[1]]
125
+ const suffix = parsed.entry?.prop.endsWith('Width') ? 'Width' : 'Color'
126
+ return sides.map((side) => `border${side}${suffix}`)
127
+ }
128
+
129
+ /**
130
+ * Adapt a registry-parsed candidate into the ordinary props shared rendering consumes.
131
+ * Examples:
132
+ * "hover:bg-blue5" → backgroundColor + a `hover:blue5` program
133
+ * "sm:p-4" → padding + an `sm:4` program
134
+ * "bg-[red]" → { key: "backgroundColor", value: "red" }
135
+ * "w-100" → { key: "width", value: 100 }
136
+ * "opacity-50" → { key: "opacity", value: 0.5 }
137
+ */
138
+ function tailwindClassToFlatProp(
139
+ parsed: ParsedCandidate,
140
+ grammarConfig?: GrammarConfigView
141
+ ): { key: string; value: any } | null {
142
+ if (parsed.kind !== 'dynamic' || !parsed.entry || parsed.rawValue === undefined) {
143
+ return null
144
+ }
145
+ const prop = parsed.entry.prop
146
+ const category = parsed.entry.tokenCategory
147
+ let value: any = parsed.rawValue
148
+
149
+ if (parsed.negative && value === '0' && parsed.convenience !== 'angle') {
150
+ return { key: prop, value: 0 }
151
+ }
152
+
153
+ if (parsed.convenience === 'zero') {
154
+ return { key: prop, value: 0 }
155
+ }
156
+
157
+ if (parsed.convenience === 'angle') {
158
+ return {
159
+ key: prop,
160
+ value: `${parsed.negative ? '-' : ''}${value}deg`,
161
+ }
162
+ }
163
+
164
+ // Tailwind's translate fractions and `full` are percentages, not configured
165
+ // spacing tokens. RN accepts percentage translate values, so preserve the
166
+ // exact CSS geometry for both axes and for negative candidates.
167
+ if ((prop === 'x' || prop === 'y') && parsed.valueKind !== 'token') {
168
+ const sized = tailwindSizingValue(prop, value)
169
+ if (sized != null) {
170
+ return {
171
+ key: prop,
172
+ value: parsed.negative && sized[0] !== '-' ? `-${sized}` : sized,
173
+ }
174
+ }
175
+ }
176
+
177
+ // Tailwind's numbered leading scale is the spacing scale (N * 0.25rem),
178
+ // represented as native points at the default 16px root size. Resolve it
179
+ // here instead of letting an application's same-named font token change the
180
+ // meaning of a Tailwind classname.
181
+ if (prop === 'lineHeight' && /^\d+(?:\.\d+)?$/.test(value)) {
182
+ return { key: prop, value: Number(value) * 4 }
183
+ }
184
+
185
+ // grid-cols-N → repeat(N, minmax(0, 1fr)), col-span-N → span N / span N
186
+ if (prop === 'gridTemplateColumns' && /^\d+$/.test(value)) {
187
+ return { key: prop, value: `repeat(${value}, minmax(0, 1fr))` }
188
+ }
189
+ if (prop === 'gridColumn' && /^\d+$/.test(value)) {
190
+ return { key: prop, value: `span ${value} / span ${value}` }
191
+ }
192
+ if (prop === 'gridRow' && /^\d+$/.test(value)) {
193
+ return { key: prop, value: `span ${value} / span ${value}` }
194
+ }
195
+ if (
196
+ (prop === 'gridColumnStart' ||
197
+ prop === 'gridColumnEnd' ||
198
+ prop === 'gridRowStart' ||
199
+ prop === 'gridRowEnd') &&
200
+ /^\d+$/.test(value)
201
+ ) {
202
+ return { key: prop, value: Number(value) }
203
+ }
204
+
205
+ if (prop.endsWith('Width') && parsed.prefix?.startsWith('border')) {
206
+ if (parsed.valueKind === 'token') {
207
+ value = `${parsed.negative ? '-' : ''}${value}`
208
+ if (grammarConfig && parsed.entry?.tokenCategory) {
209
+ value =
210
+ resolveTokenName(grammarConfig, parsed.entry.tokenCategory, value) ?? value
211
+ }
212
+ } else if (parsed.valueKind === 'arbitrary') {
213
+ value = unwrapArbitraryValue(value)
214
+ } else {
215
+ return null
216
+ }
217
+ return { key: prop, value }
218
+ }
219
+
220
+ if (prop === 'fontFamily') {
221
+ let famValue: string
222
+ if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
223
+ famValue = decodeArbitrary(value.slice(1, -1))
224
+ } else {
225
+ const generic: Record<string, string> = {
226
+ sans: 'sans-serif',
227
+ serif: 'serif',
228
+ mono: 'monospace',
229
+ }
230
+ famValue = parsed.valueKind === 'token' ? value : generic[value] || value
231
+ }
232
+ return {
233
+ key: 'fontFamily',
234
+ value: famValue,
235
+ }
236
+ }
237
+
238
+ if (prop === 'fontSize') {
239
+ let fsValue: any
240
+ if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
241
+ // fontSize is number-only on native: text-[14px] → 14 (arbitraryValue drops px)
242
+ fsValue = arbitraryValue(decodeArbitrary(value.slice(1, -1)))
243
+ } else {
244
+ fsValue = value
245
+ }
246
+ return {
247
+ key: 'fontSize',
248
+ value: fsValue,
249
+ }
250
+ }
251
+
252
+ if (prop === 'lineHeight') {
253
+ let lhValue: any
254
+ if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
255
+ const inner = decodeArbitrary(value.slice(1, -1))
256
+ // px length → NUMBER (native-valid: leading-[20px] → 20). a UNITLESS value is a web
257
+ // lineHeight MULTIPLIER and MUST stay a string (a number would be px-ified to "1.25px"
258
+ // on web, breaking the multiplier). RN has no unitless multiplier, so this is web-only.
259
+ lhValue = /^-?\d*\.?\d+px$/.test(inner) ? Number.parseFloat(inner) : inner
260
+ } else {
261
+ lhValue = value
262
+ }
263
+ return {
264
+ key: 'lineHeight',
265
+ value: lhValue,
266
+ }
267
+ }
268
+
269
+ if (prop === 'letterSpacing') {
270
+ let lsValue: any
271
+ if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
272
+ lsValue = arbitraryValue(decodeArbitrary(value.slice(1, -1)))
273
+ } else {
274
+ lsValue = value
275
+ }
276
+ return {
277
+ key: 'letterSpacing',
278
+ value: lsValue,
279
+ }
280
+ }
281
+
282
+ if (prop === 'boxShadow' && value[0] === '[' && value[value.length - 1] === ']') {
283
+ return {
284
+ key: 'boxShadow',
285
+ value: decodeArbitrary(value.slice(1, -1)),
286
+ }
287
+ }
288
+
289
+ // arbitrary values: p-[4px], w-[100px], rounded-[8px], min-h-[100vh], rotate-[-8deg],
290
+ // h-[calc(100%-2px)], bg-[var(--color5)], bg-[#fff]. use the bracketed value directly as
291
+ // CSS — no scaling/token resolution. tailwind encodes spaces inside [] as underscores.
292
+ if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
293
+ const inner = decodeArbitrary(value.slice(1, -1))
294
+ if (inner === '') return null
295
+ // px-length + unitless arbitraries become NUMBERS (native requires numbers, drops "Npx"
296
+ // strings); unit/function values stay strings. one canonical rule (arbitraryValue).
297
+ let resolved = arbitraryValue(inner, prop)
298
+ // rotate requires a unit-bearing string on native — a bare number from rotate-[45]
299
+ // triggers a redbox. append deg when the arbitrary resolved to a unitless number.
300
+ if (
301
+ (prop === 'rotate' ||
302
+ prop === 'rotateX' ||
303
+ prop === 'rotateY' ||
304
+ prop === 'rotateZ' ||
305
+ prop === 'skewX' ||
306
+ prop === 'skewY') &&
307
+ typeof resolved === 'number'
308
+ ) {
309
+ resolved = `${resolved}deg`
310
+ }
311
+ if (parsed.negative) {
312
+ if (typeof resolved === 'number') resolved = -resolved
313
+ else if (resolved[0] !== '-') resolved = `-${resolved}`
314
+ }
315
+ return { key: prop, value: resolved }
316
+ }
317
+
318
+ // tailwind sizing keywords / fractions (w-full → 100%, w-1/2 → 50%, w-auto, w-screen).
319
+ // handled before isValidTailwindValue since fractions/keywords aren't plain CSS values. An
320
+ // exact configured size token with the same spelling stays a token.
321
+ if (
322
+ parsed.valueKind !== 'token' &&
323
+ (category === 'size' || parsed.convenience === 'sizing-keyword')
324
+ ) {
325
+ const sized = tailwindSizingValue(prop, value)
326
+ if (sized != null) {
327
+ return {
328
+ key: prop,
329
+ value: parsed.negative && sized[0] !== '-' ? `-${sized}` : sized,
330
+ }
331
+ }
332
+ }
333
+
334
+ // color opacity modifier: bg-blue-500/50 → split value into base + /N suffix,
335
+ // re-attached after token resolution so the shared color resolver applies it via color-mix
336
+ // (web) / rgba (native). only for color props; for non-color props a "/" in the
337
+ // value is left intact (e.g. fraction sizing handled above).
338
+ let opacitySuffix = ''
339
+ if (category === 'color' && typeof value === 'string') {
340
+ const suffix = splitColorOpacitySuffix(value)
341
+ if (suffix.kind === 'invalid') {
342
+ return { key: prop, value }
343
+ }
344
+ if (suffix.kind === 'valid') {
345
+ opacitySuffix = value.slice(suffix.name.length)
346
+ value = suffix.name
347
+ }
348
+ }
349
+
350
+ // handle special value patterns
351
+ if (percentUtilityProps.has(prop) && /^\d+$/.test(value)) {
352
+ // tailwind percentage utilities: opacity-50 → 0.5, scale-95 → 0.95, scale-100 → 1
353
+ value = Number(value) / 100
354
+ } else if (/^\d+(\.\d+)?$/.test(value)) {
355
+ if (category === 'zIndex' && parsed.valueKind === 'convenience') {
356
+ value = Number(value) * (parsed.negative ? -1 : 1)
357
+ } else if (category) {
358
+ value = `${parsed.negative ? '-' : ''}${value}`
359
+ if (grammarConfig) {
360
+ value = resolveTokenName(grammarConfig, category, value) ?? value
361
+ }
362
+ } else {
363
+ value = Number(value)
364
+ }
365
+ } else if (typeof value === 'string') {
366
+ if (
367
+ category &&
368
+ !(
369
+ (category === 'space' || category === 'size' || category === 'radius') &&
370
+ getSafeAreaEdge(value)
371
+ )
372
+ ) {
373
+ value = `${parsed.negative ? '-' : ''}${value}`
374
+ }
375
+ }
376
+
377
+ // re-attach the color opacity suffix (bg-blue-500/50). the shared flat-value
378
+ // resolver parses the trailing /N after its property-scoped name lookup.
379
+ if (opacitySuffix && typeof value === 'string') {
380
+ value = `${value}${opacitySuffix}`
381
+ }
382
+
383
+ if (parsed.negative && !category) {
384
+ if (typeof value === 'number') value = -value
385
+ else if (typeof value === 'string' && value[0] !== '-') value = `-${value}`
386
+ }
387
+
388
+ return { key: prop, value }
389
+ }
390
+
391
+ // Parsing a Tailwind candidate depends only on the class string and the grammar
392
+ // config, never on the surrounding props, so the decision is cached per class.
393
+ // Without this the full parse (split + classify + resolve + border expansion) ran
394
+ // on every render of every component carrying a className — measured at ~18µs per
395
+ // call for a 4-class string, a 1.53x tax over writing the same styles as props.
396
+ // The plan is a flat [key, value][] applied in order, so the authored ordering
397
+ // between classes and ordinary props is unchanged.
398
+ //
399
+ // null = web-only candidate dropped on native. 'raw' = not claimed by the
400
+ // grammar, caller preserves the class string. An array (which may legitimately be
401
+ // empty) = claimed, apply these entries.
402
+ type TailwindPlanEntry = FrontendClassPlanEntry
403
+ type TailwindClassPlan = TailwindPlanEntry[] | null | 'raw'
404
+ type TailwindParentPlan = {
405
+ entries: TailwindPlanEntry[]
406
+ preserveRawClass: boolean
407
+ }
408
+
409
+ type CachedTailwindClassPlan = TailwindClassPlan | TailwindParentPlan
410
+
411
+ const classPlanCache = new WeakMap<object, Map<string, CachedTailwindClassPlan>>()
412
+ const nativeTarget = !isWeb || process.env.TAMAGUI_TARGET === 'native'
413
+ const cssGridProps = new Set([
414
+ 'gridTemplateColumns',
415
+ 'gridColumn',
416
+ 'gridColumnStart',
417
+ 'gridColumnEnd',
418
+ 'gridRow',
419
+ 'gridRowStart',
420
+ 'gridRowEnd',
421
+ ])
422
+ const nativeUnsupportedProps = new Set([
423
+ 'objectFit',
424
+ 'order',
425
+ 'overflowX',
426
+ 'overflowY',
427
+ 'textOverflow',
428
+ 'whiteSpace',
429
+ 'visibility',
430
+ ])
431
+ const nativeUnsupportedSizingValues = new Set(['screen', 'min', 'max', 'fit'])
432
+ const nativeUnsupportedDisplayValues = new Set(['block', 'inline', 'inline-flex'])
433
+ const nativeUnsupportedPositionValues = new Set(['fixed', 'sticky'])
434
+
435
+ function shouldGateNative(parsed: ParsedCandidate): boolean {
436
+ if (!nativeTarget) return false
437
+ const properties = parsed.properties || {}
438
+ if (
439
+ process.env.TAMAGUI_CSS_GRID !== '1' &&
440
+ ((parsed.entry && cssGridProps.has(parsed.entry.prop)) ||
441
+ properties.display === 'grid' ||
442
+ Object.keys(properties).some((prop) => cssGridProps.has(prop)))
443
+ ) {
444
+ return true
445
+ }
446
+ if (
447
+ (parsed.entry && nativeUnsupportedProps.has(parsed.entry.prop)) ||
448
+ Object.keys(properties).some((prop) => nativeUnsupportedProps.has(prop))
449
+ ) {
450
+ return true
451
+ }
452
+ if (
453
+ parsed.convenience === 'sizing-keyword' &&
454
+ nativeUnsupportedSizingValues.has(parsed.rawValue || '')
455
+ ) {
456
+ return true
457
+ }
458
+ return (
459
+ nativeUnsupportedDisplayValues.has(String(properties.display)) ||
460
+ nativeUnsupportedPositionValues.has(String(properties.position)) ||
461
+ properties.overflow === 'auto' ||
462
+ (properties.cursor !== undefined &&
463
+ properties.cursor !== 'auto' &&
464
+ properties.cursor !== 'pointer')
465
+ )
466
+ }
467
+
468
+ function getClassPlanCache(grammarConfig: object) {
469
+ let cache = classPlanCache.get(grammarConfig)
470
+ if (!cache) {
471
+ cache = new Map()
472
+ classPlanCache.set(grammarConfig, cache)
473
+ }
474
+ return cache
475
+ }
476
+
477
+ function createPlanEntry(
478
+ property: string,
479
+ value: unknown,
480
+ modifiers: readonly string[]
481
+ ): TailwindPlanEntry | null {
482
+ if (modifiers.length === 0) return [property, value]
483
+ const payload = plainValueToPayload(value, property)
484
+ if (payload === null) return null
485
+ let condition = ''
486
+ for (let index = 0; index < modifiers.length; index++) {
487
+ if (index) condition += ':'
488
+ condition += modifiers[index]
489
+ }
490
+ return [property, payload, condition, modifiers]
491
+ }
492
+
493
+ // CSS shorthands reset every longhand, including logical ones. RN stores those
494
+ // under different keys (`paddingStart` vs `paddingLeft`, `columnGap` vs `gap`),
495
+ // so `ps-2 p-4` would otherwise keep the earlier logical value. Unmodified
496
+ // shorthand plans prepend `unset` entries so both the tokenizer and the
497
+ // renderer sink clear them. Conditional classes (`hover:p-4`) skip this.
498
+ const shorthandResets: Record<string, readonly string[]> = {
499
+ padding: [
500
+ 'paddingTop',
501
+ 'paddingRight',
502
+ 'paddingBottom',
503
+ 'paddingLeft',
504
+ 'paddingStart',
505
+ 'paddingEnd',
506
+ 'paddingInlineStart',
507
+ 'paddingInlineEnd',
508
+ 'paddingBlockStart',
509
+ 'paddingBlockEnd',
510
+ 'paddingHorizontal',
511
+ 'paddingVertical',
512
+ 'paddingInline',
513
+ 'paddingBlock',
514
+ ],
515
+ margin: [
516
+ 'marginTop',
517
+ 'marginRight',
518
+ 'marginBottom',
519
+ 'marginLeft',
520
+ 'marginStart',
521
+ 'marginEnd',
522
+ 'marginInlineStart',
523
+ 'marginInlineEnd',
524
+ 'marginBlockStart',
525
+ 'marginBlockEnd',
526
+ 'marginHorizontal',
527
+ 'marginVertical',
528
+ 'marginInline',
529
+ 'marginBlock',
530
+ ],
531
+ gap: ['rowGap', 'columnGap'],
532
+ borderRadius: [
533
+ 'borderTopLeftRadius',
534
+ 'borderTopRightRadius',
535
+ 'borderBottomLeftRadius',
536
+ 'borderBottomRightRadius',
537
+ 'borderTopStartRadius',
538
+ 'borderTopEndRadius',
539
+ 'borderBottomStartRadius',
540
+ 'borderBottomEndRadius',
541
+ 'borderStartStartRadius',
542
+ 'borderStartEndRadius',
543
+ 'borderEndStartRadius',
544
+ 'borderEndEndRadius',
545
+ ],
546
+ borderWidth: [
547
+ 'borderTopWidth',
548
+ 'borderRightWidth',
549
+ 'borderBottomWidth',
550
+ 'borderLeftWidth',
551
+ 'borderStartWidth',
552
+ 'borderEndWidth',
553
+ 'borderInlineStartWidth',
554
+ 'borderInlineEndWidth',
555
+ 'borderBlockStartWidth',
556
+ 'borderBlockEndWidth',
557
+ 'borderInlineWidth',
558
+ 'borderBlockWidth',
559
+ ],
560
+ borderColor: [
561
+ 'borderTopColor',
562
+ 'borderRightColor',
563
+ 'borderBottomColor',
564
+ 'borderLeftColor',
565
+ 'borderStartColor',
566
+ 'borderEndColor',
567
+ 'borderInlineStartColor',
568
+ 'borderInlineEndColor',
569
+ 'borderBlockStartColor',
570
+ 'borderBlockEndColor',
571
+ 'borderInlineColor',
572
+ 'borderBlockColor',
573
+ ],
574
+ }
575
+
576
+ function attachShorthandResets(
577
+ entries: TailwindPlanEntry[],
578
+ modifiers: readonly string[]
579
+ ): TailwindPlanEntry[] {
580
+ // web cascade already treats a later shorthand as a reset. emitting
581
+ // `unset` there becomes a real CSS value and fails parity.
582
+ if (isWeb || modifiers.length > 0) return entries
583
+ const unsets: TailwindPlanEntry[] = []
584
+ for (let i = 0; i < entries.length; i++) {
585
+ const resets = shorthandResets[entries[i][0]]
586
+ if (!resets) continue
587
+ for (let j = 0; j < resets.length; j++) {
588
+ unsets.push([resets[j], 'unset'])
589
+ }
590
+ }
591
+ return unsets.length === 0 ? entries : unsets.concat(entries)
592
+ }
593
+
594
+ function computeClassPlan(
595
+ cls: string,
596
+ grammarConfig: GrammarConfigView
597
+ ): CachedTailwindClassPlan {
598
+ const groupMarker = /^group(?:\/([A-Za-z0-9_-]+))?$/.exec(cls)
599
+ if (groupMarker) {
600
+ return {
601
+ entries: [['group', groupMarker[1] || true]],
602
+ preserveRawClass: isWeb,
603
+ }
604
+ }
605
+
606
+ const containerMarker = /^@container(-size)?(?:\/([A-Za-z0-9_-]+))?$/.exec(cls)
607
+ if (containerMarker) {
608
+ const isSize = containerMarker[1] !== undefined
609
+ const name = containerMarker[2]
610
+ const entries: TailwindPlanEntry[] = []
611
+ entries.push(['container', name || true])
612
+ if (isSize) {
613
+ entries.push(['containerType', 'size'])
614
+ }
615
+ return { entries, preserveRawClass: isWeb }
616
+ }
617
+
618
+ const classification = classifyCandidate(cls, grammarConfig)
619
+ if (classification.kind === 'passthrough') {
620
+ return isWeb ? 'raw' : null
621
+ }
622
+ const parsed = classification.parsed
623
+ if (!nativeTarget && parsed.entry?.prop === 'perspective') return 'raw'
624
+ if (shouldGateNative(parsed)) {
625
+ return null
626
+ }
627
+ if (parsed.entry?.prop === 'flex' && parsed.rawValue) {
628
+ const fraction = /^(\d+)\/(\d+)$/.exec(parsed.rawValue)
629
+ if (fraction && Number(fraction[2]) !== 0) {
630
+ const basis = `${(Number(fraction[1]) / Number(fraction[2])) * 100}%`
631
+ const entries = [
632
+ createPlanEntry('flexGrow', 1, parsed.modifiers),
633
+ createPlanEntry('flexShrink', 1, parsed.modifiers),
634
+ createPlanEntry('flexBasis', basis, parsed.modifiers),
635
+ ]
636
+ return entries.every(Boolean) ? (entries as TailwindPlanEntry[]) : 'raw'
637
+ }
638
+ }
639
+ // named utilities first (flex-row, flex-1, hidden, …) — whole class → fixed prop(s).
640
+ // these may emit multiple props and may have no dash, so handle before the generic parse.
641
+ const util = parsed.kind === 'utility' ? parsed.properties : null
642
+ if (util) {
643
+ const entries: TailwindPlanEntry[] = []
644
+ for (const p in util) {
645
+ const value = nativeTarget && p === 'transform' && util[p] === 'none' ? [] : util[p]
646
+ const entry = createPlanEntry(p, value, parsed.modifiers)
647
+ if (!entry) return 'raw'
648
+ entries.push(entry)
649
+ }
650
+ return attachShorthandResets(entries, parsed.modifiers)
651
+ }
652
+ // Resolve only after the registry has claimed the candidate. Multi-prop expansion
653
+ // below consumes that parsed decision instead of re-parsing width vs color.
654
+ const expandedProps = expansionProps(parsed)
655
+ if (expandedProps) {
656
+ const entries: TailwindPlanEntry[] = []
657
+ for (const prop of expandedProps) {
658
+ const flatProp = tailwindClassToFlatProp(
659
+ {
660
+ ...parsed,
661
+ entry: parsed.entry ? { ...parsed.entry, prop } : parsed.entry,
662
+ },
663
+ grammarConfig
664
+ )
665
+ if (!flatProp) return 'raw'
666
+ const entry = createPlanEntry(flatProp.key, flatProp.value, parsed.modifiers)
667
+ if (!entry) return 'raw'
668
+ entries.push(entry)
669
+ }
670
+ return attachShorthandResets(entries, parsed.modifiers)
671
+ }
672
+ const flatProp = tailwindClassToFlatProp(parsed, grammarConfig)
673
+ if (flatProp) {
674
+ const entry = createPlanEntry(flatProp.key, flatProp.value, parsed.modifiers)
675
+ return entry ? attachShorthandResets([entry], parsed.modifiers) : 'raw'
676
+ }
677
+ // not claimed: caller preserves the raw class
678
+ return 'raw'
679
+ }
680
+
681
+ export function getTailwindClassPlan(
682
+ candidate: string,
683
+ config: StyleFrontendConfig
684
+ ): FrontendClassPlan {
685
+ const grammarConfig = getStyleGrammarConfig(config)
686
+ const plans = getClassPlanCache(grammarConfig)
687
+ let plan = plans.get(candidate)
688
+ if (plan === undefined) {
689
+ plan = computeClassPlan(candidate, grammarConfig)
690
+ plans.set(candidate, plan)
691
+ }
692
+ return plan
693
+ }
694
+
695
+ export function resolveTailwindCandidate(
696
+ candidate: string,
697
+ config: StyleFrontendConfig,
698
+ sink: FrontendClassSink
699
+ ): boolean | null {
700
+ const composed = tryCompose(candidate, getStyleGrammarConfig(config), sink)
701
+ if (composed !== undefined) return composed
702
+ const plan = getTailwindClassPlan(candidate, config)
703
+ if (plan === 'raw') return true
704
+ if (plan === null) return null
705
+ const parent = Array.isArray(plan) ? null : plan
706
+ const entries = parent ? parent.entries : plan
707
+ for (let index = 0; index < entries.length; index++) {
708
+ const entry = entries[index]
709
+ const transformTokenCategory =
710
+ entry[0] === 'x' || entry[0] === 'y'
711
+ ? 'space'
712
+ : entry[0] === 'perspective'
713
+ ? 'perspective'
714
+ : null
715
+ const transformValue =
716
+ transformTokenCategory && typeof entry[1] === 'string'
717
+ ? ((config as any).tokensParsed?.[transformTokenCategory]?.[entry[1]]?.val ??
718
+ entry[1])
719
+ : entry[1]
720
+ if (noteTailwindTransform(sink, [entry[0], transformValue, entry[2], entry[3]])) {
721
+ continue
722
+ }
723
+ if (entry[0] === 'boxShadow' && typeof entry[1] === 'string') {
724
+ noteBoxShadow(
725
+ sink,
726
+ (config as any).tokensParsed?.boxShadow?.[entry[1]]?.val ?? entry[1],
727
+ entry[3] || []
728
+ )
729
+ }
730
+ sink(entry)
731
+ }
732
+ return parent?.preserveRawClass || false
733
+ }
734
+
735
+ export function resolveTailwindClassName(
736
+ className: string,
737
+ config: StyleFrontendConfig
738
+ ): Record<string, any> {
739
+ const result: Record<string, any> = {}
740
+ // composed utilities collect under `__`-prefixed keys and are folded once at the
741
+ // end, the same shape and the same order the runtime walk uses
742
+ let composedProps: Record<string, any> | null = null
743
+ let rawClassName = ''
744
+ const sink: FrontendClassSink = (entry) => {
745
+ const [key, value, condition] = entry
746
+ if (key.charCodeAt(0) === 95 && key.charCodeAt(1) === 95) {
747
+ composedProps ||= {}
748
+ composedProps[key] = mergeFrontendCondition(composedProps[key], value, condition)
749
+ return
750
+ }
751
+ if (condition === undefined) {
752
+ const resets = shorthandResets[key]
753
+ if (resets) {
754
+ for (let i = 0; i < resets.length; i++) {
755
+ delete result[resets[i]]
756
+ }
757
+ }
758
+ }
759
+ setInAuthoredOrder(result, key, mergeFrontendCondition(result[key], value, condition))
760
+ }
761
+ let start = 0
762
+ for (let index = 0; index <= className.length; index++) {
763
+ if (index !== className.length && className.charCodeAt(index) > 32) continue
764
+ if (start === index) {
765
+ start = index + 1
766
+ continue
767
+ }
768
+ const candidate = className.slice(start, index)
769
+ const preserveRaw = resolveTailwindCandidate(candidate, config, sink)
770
+ if (preserveRaw) {
771
+ rawClassName = rawClassName ? `${rawClassName} ${candidate}` : candidate
772
+ }
773
+ start = index + 1
774
+ }
775
+ if (composedProps) {
776
+ const composed = composedResolver(composedProps)
777
+ for (const key in composed) {
778
+ setInAuthoredOrder(result, key, composed[key])
779
+ }
780
+ }
781
+ if (rawClassName) result.className = rawClassName
782
+ return result
783
+ }
784
+
785
+ /**
786
+ * Append a contribution at the end of the forward pass.
787
+ *
788
+ * Plain re-assignment keeps a key's FIRST insertion position, so a restated
789
+ * shorthand would stay behind a longhand authored between the two occurrences:
790
+ * `p-4 px-2 p-6` has to resolve `paddingLeft` from `p-6`, and `pt-2 p-4 pt-8` has to
791
+ * resolve `paddingTop` from `pt-8`. Deleting the key first moves it to the end,
792
+ * which is the authored order the shared per-longhand merge reads.
793
+ */
794
+ export function setInAuthoredOrder(
795
+ target: Record<string, any>,
796
+ key: string,
797
+ value: any
798
+ ): void {
799
+ if (value === 'unset') {
800
+ delete target[key]
801
+ return
802
+ }
803
+ if (key in target) delete target[key]
804
+ target[key] = value
805
+ }