@tamagui/tailwind 3.0.0-beta.924.1 → 3.0.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.
- package/dist/cjs/candidate.cjs +312 -137
- package/dist/cjs/candidate.native.cjs +342 -153
- package/dist/cjs/candidate.native.js +342 -153
- package/dist/cjs/candidate.native.js.map +1 -1
- package/dist/cjs/compose.cjs +383 -0
- package/dist/cjs/compose.native.cjs +389 -0
- package/dist/cjs/compose.native.js +391 -0
- package/dist/cjs/compose.native.js.map +1 -0
- package/dist/cjs/composedResolver.cjs +226 -0
- package/dist/cjs/composedResolver.native.cjs +255 -0
- package/dist/cjs/composedResolver.native.js +257 -0
- package/dist/cjs/composedResolver.native.js.map +1 -0
- package/dist/cjs/frontend.cjs +4 -11
- package/dist/cjs/frontend.native.cjs +5 -16
- package/dist/cjs/frontend.native.js +5 -16
- package/dist/cjs/frontend.native.js.map +1 -1
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.native.cjs +2 -0
- package/dist/cjs/index.native.js +2 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/candidate.mjs +314 -140
- package/dist/esm/candidate.mjs.map +1 -1
- package/dist/esm/candidate.native.js +344 -156
- package/dist/esm/candidate.native.js.map +1 -1
- package/dist/esm/compose.mjs +359 -0
- package/dist/esm/compose.mjs.map +1 -0
- package/dist/esm/compose.native.js +363 -0
- package/dist/esm/compose.native.js.map +1 -0
- package/dist/esm/composedResolver.mjs +206 -0
- package/dist/esm/composedResolver.mjs.map +1 -0
- package/dist/esm/composedResolver.native.js +233 -0
- package/dist/esm/composedResolver.native.js.map +1 -0
- package/dist/esm/frontend.mjs +4 -11
- package/dist/esm/frontend.mjs.map +1 -1
- package/dist/esm/frontend.native.js +5 -16
- package/dist/esm/frontend.native.js.map +1 -1
- package/dist/esm/index.mjs +3 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +3 -2
- package/dist/esm/index.native.js.map +1 -1
- package/package.json +18 -15
- package/src/candidate.ts +396 -60
- package/src/frontend.ts +3 -16
- package/src/index.tsx +14 -2
- package/src/types.ts +32 -4
- package/types/candidate.d.ts +2 -1
- package/types/candidate.d.ts.map +1 -1
- package/types/compose.d.ts +19 -0
- package/types/compose.d.ts.map +1 -0
- package/types/composedResolver.d.ts +19 -0
- package/types/composedResolver.d.ts.map +1 -0
- package/types/frontend.d.ts.map +1 -1
- package/types/index.d.ts +11 -1
- package/types/index.d.ts.map +1 -1
- package/types/types.d.ts +18 -3
- package/types/types.d.ts.map +1 -1
package/src/candidate.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { isWeb } from '@tamagui/constants'
|
|
2
|
+
import { stylePropsUnitless } from '@tamagui/helpers'
|
|
2
3
|
import {
|
|
4
|
+
mergeFrontendCondition,
|
|
3
5
|
plainValueToPayload,
|
|
4
6
|
type FrontendClassPlan,
|
|
5
7
|
type FrontendClassPlanEntry,
|
|
8
|
+
type FrontendClassSink,
|
|
6
9
|
type StyleFrontendConfig,
|
|
7
10
|
} from '@tamagui/core/internal-runtime'
|
|
11
|
+
import { noteBoxShadow, noteTailwindTransform, tryCompose } from './compose'
|
|
12
|
+
import { composedResolver } from './composedResolver'
|
|
8
13
|
import {
|
|
9
14
|
borderSideSuffix,
|
|
10
15
|
configRevisionSymbol,
|
|
@@ -20,8 +25,12 @@ import {
|
|
|
20
25
|
import {
|
|
21
26
|
classifyCandidate,
|
|
22
27
|
decodeArbitrary,
|
|
28
|
+
hasTokenName,
|
|
23
29
|
type GrammarConfigView,
|
|
24
30
|
type ParsedCandidate,
|
|
31
|
+
resolveTokenName,
|
|
32
|
+
tailwindLeadingRatios,
|
|
33
|
+
tailwindTrackingEms,
|
|
25
34
|
} from '@tamagui/style-grammar/tooling/candidate'
|
|
26
35
|
|
|
27
36
|
/**
|
|
@@ -54,25 +63,36 @@ export function isTokenValueProp(prop: string): boolean {
|
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
// CANONICAL arbitrary-value coercion. an arbitrary `[..]` whose inner is a UNITLESS number
|
|
57
|
-
// (z-[400], aspect-[1.5]
|
|
66
|
+
// (z-[400], aspect-[1.5]) or a PX length (p-[18px], text-[14px], border-[0.5px])
|
|
58
67
|
// resolves to a NUMBER: React Native REQUIRES numbers for dimensional + typography props and
|
|
59
68
|
// silently DROPS "Npx"/"400" strings (Yoga parseCSSProperty<CSSNumber,CSSPercentage> rejects px;
|
|
60
69
|
// StyleSheetTypes types fontSize/lineHeight/letterSpacing as number). web accepts the number and
|
|
61
70
|
// re-adds px via CSS. anything carrying a real unit/function (%, rem, vh, deg, calc(), var(),
|
|
62
71
|
// #hex, colors) stays a STRING.
|
|
63
|
-
function arbitraryValue(inner: string): number | string {
|
|
72
|
+
function arbitraryValue(inner: string, prop?: string): number | string {
|
|
64
73
|
const px = /^(-?\d*\.?\d+)px$/.exec(inner)
|
|
65
|
-
|
|
74
|
+
// a unitless prop (grid-cols-[77px]) never gets px re-added, so the length stays a string
|
|
75
|
+
if (px) return prop && stylePropsUnitless[prop] ? inner : Number(px[1])
|
|
66
76
|
if (/^-?\d*\.?\d+$/.test(inner)) return Number(inner)
|
|
67
77
|
return inner
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
/** the same coercion for a raw value that may still be wrapped in `[]` */
|
|
81
|
+
function unwrapArbitraryValue(raw: string): number | string {
|
|
82
|
+
return arbitraryValue(
|
|
83
|
+
raw.length > 1 && raw[0] === '[' && raw[raw.length - 1] === ']'
|
|
84
|
+
? decodeArbitrary(raw.slice(1, -1))
|
|
85
|
+
: raw
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
// tailwind sizing keywords / fractions for width/height/min/max props.
|
|
71
90
|
// returns a CSS value, or null if `value` isn't a sizing keyword (falls through to normal parse).
|
|
72
91
|
function tailwindSizingValue(prop: string, value: string): string | null {
|
|
73
92
|
if (value === 'full') return '100%'
|
|
74
93
|
if (value === 'auto') return 'auto'
|
|
75
|
-
if (value === 'screen') return /[Hh]eight/.test(prop) ? '100vh' : '100vw'
|
|
94
|
+
if (value === 'screen') return /[Hh]eight|[Bb]lock/.test(prop) ? '100vh' : '100vw'
|
|
95
|
+
if (/^[dls]v[hw]$/.test(value)) return `100${value}`
|
|
76
96
|
if (value === 'min') return 'min-content'
|
|
77
97
|
if (value === 'max') return 'max-content'
|
|
78
98
|
if (value === 'fit') return 'fit-content'
|
|
@@ -83,20 +103,6 @@ function tailwindSizingValue(prop: string, value: string): string | null {
|
|
|
83
103
|
return null
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
// unwrap a possibly-arbitrary value ([2px] → "2px") and coerce a bare number / px length to
|
|
87
|
-
// a NUMBER (2px → 2, 0.5 → 0.5) so borderWidth/radius match tamagui's numeric props; other
|
|
88
|
-
// units (1em, calc(…)) stay strings.
|
|
89
|
-
function borderDimValue(raw: string): number | string {
|
|
90
|
-
let inner = raw
|
|
91
|
-
if (raw.length > 1 && raw[0] === '[' && raw[raw.length - 1] === ']') {
|
|
92
|
-
inner = decodeArbitrary(raw.slice(1, -1))
|
|
93
|
-
}
|
|
94
|
-
const px = /^(-?\d*\.?\d+)px$/.exec(inner)
|
|
95
|
-
if (px) return Number.parseFloat(px[1])
|
|
96
|
-
if (/^-?\d*\.?\d+$/.test(inner)) return Number(inner)
|
|
97
|
-
return inner
|
|
98
|
-
}
|
|
99
|
-
|
|
100
106
|
/**
|
|
101
107
|
* Prefixes that write more than one longhand from a single candidate. The registry
|
|
102
108
|
* already selected the token category and width-vs-color meaning; this only names
|
|
@@ -107,6 +113,8 @@ function expansionProps(parsed: ParsedCandidate): readonly string[] | null {
|
|
|
107
113
|
const prefix = parsed.prefix
|
|
108
114
|
if (!prefix) return null
|
|
109
115
|
if (prefix === 'size') return sizeUtilityProps
|
|
116
|
+
if (prefix === 'translate') return ['x', 'y']
|
|
117
|
+
if (prefix === 'skew') return ['skewX', 'skewY']
|
|
110
118
|
if (prefix === 'inset-x' || prefix === 'inset-y') {
|
|
111
119
|
return insetAxisProps[prefix.slice('inset-'.length)]
|
|
112
120
|
}
|
|
@@ -125,14 +133,15 @@ function expansionProps(parsed: ParsedCandidate): readonly string[] | null {
|
|
|
125
133
|
/**
|
|
126
134
|
* Adapt a registry-parsed candidate into the ordinary props shared rendering consumes.
|
|
127
135
|
* Examples:
|
|
128
|
-
* "hover:bg-
|
|
136
|
+
* "hover:bg-blue-5" → backgroundColor + a `hover:blue-5` program
|
|
129
137
|
* "sm:p-4" → padding + an `sm:4` program
|
|
130
138
|
* "bg-[red]" → { key: "backgroundColor", value: "red" }
|
|
131
139
|
* "w-100" → { key: "width", value: 100 }
|
|
132
140
|
* "opacity-50" → { key: "opacity", value: 0.5 }
|
|
133
141
|
*/
|
|
134
142
|
function tailwindClassToFlatProp(
|
|
135
|
-
parsed: ParsedCandidate
|
|
143
|
+
parsed: ParsedCandidate,
|
|
144
|
+
grammarConfig?: GrammarConfigView
|
|
136
145
|
): { key: string; value: any } | null {
|
|
137
146
|
if (parsed.kind !== 'dynamic' || !parsed.entry || parsed.rawValue === undefined) {
|
|
138
147
|
return null
|
|
@@ -141,11 +150,83 @@ function tailwindClassToFlatProp(
|
|
|
141
150
|
const category = parsed.entry.tokenCategory
|
|
142
151
|
let value: any = parsed.rawValue
|
|
143
152
|
|
|
153
|
+
if (parsed.negative && value === '0' && parsed.convenience !== 'angle') {
|
|
154
|
+
return { key: prop, value: 0 }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (parsed.convenience === 'zero') {
|
|
158
|
+
return { key: prop, value: 0 }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (parsed.convenience === 'angle') {
|
|
162
|
+
return {
|
|
163
|
+
key: prop,
|
|
164
|
+
value: `${parsed.negative ? '-' : ''}${value}deg`,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Tailwind's translate fractions and `full` are percentages, not configured
|
|
169
|
+
// spacing tokens. RN accepts percentage translate values, so preserve the
|
|
170
|
+
// exact CSS geometry for both axes and for negative candidates.
|
|
171
|
+
if ((prop === 'x' || prop === 'y') && parsed.valueKind !== 'token') {
|
|
172
|
+
const sized = tailwindSizingValue(prop, value)
|
|
173
|
+
if (sized != null) {
|
|
174
|
+
return {
|
|
175
|
+
key: prop,
|
|
176
|
+
value: parsed.negative && sized[0] !== '-' ? `-${sized}` : sized,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// named leading (leading-tight) is a unitless ratio, which v3 line height reads as such
|
|
182
|
+
if (parsed.convenience === 'leading-keyword') {
|
|
183
|
+
return { key: prop, value: tailwindLeadingRatios[value] }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// named tracking (tracking-wide) is em of the font size, which core lowers per host
|
|
187
|
+
if (parsed.convenience === 'tracking-keyword') {
|
|
188
|
+
const em = tailwindTrackingEms[value] * (parsed.negative ? -1 : 1)
|
|
189
|
+
return { key: prop, value: `${em}em` }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Tailwind's numbered leading scale is the spacing scale (N * 0.25rem),
|
|
193
|
+
// represented as native points at the default 16px root size. Resolve it
|
|
194
|
+
// here instead of letting an application's same-named font token change the
|
|
195
|
+
// meaning of a Tailwind classname. Keep the unit until core lowers it for the
|
|
196
|
+
// target host, because a bare number is a V3 line-height ratio.
|
|
197
|
+
if (prop === 'lineHeight' && /^\d+(?:\.\d+)?$/.test(value)) {
|
|
198
|
+
return { key: prop, value: `${Number(value) * 4}px` }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// grid-cols-N → repeat(N, minmax(0, 1fr)), col-span-N → span N / span N
|
|
202
|
+
if (prop === 'gridTemplateColumns' && /^\d+$/.test(value)) {
|
|
203
|
+
return { key: prop, value: `repeat(${value}, minmax(0, 1fr))` }
|
|
204
|
+
}
|
|
205
|
+
if (prop === 'gridColumn' && /^\d+$/.test(value)) {
|
|
206
|
+
return { key: prop, value: `span ${value} / span ${value}` }
|
|
207
|
+
}
|
|
208
|
+
if (prop === 'gridRow' && /^\d+$/.test(value)) {
|
|
209
|
+
return { key: prop, value: `span ${value} / span ${value}` }
|
|
210
|
+
}
|
|
211
|
+
if (
|
|
212
|
+
(prop === 'gridColumnStart' ||
|
|
213
|
+
prop === 'gridColumnEnd' ||
|
|
214
|
+
prop === 'gridRowStart' ||
|
|
215
|
+
prop === 'gridRowEnd') &&
|
|
216
|
+
/^\d+$/.test(value)
|
|
217
|
+
) {
|
|
218
|
+
return { key: prop, value: Number(value) }
|
|
219
|
+
}
|
|
220
|
+
|
|
144
221
|
if (prop.endsWith('Width') && parsed.prefix?.startsWith('border')) {
|
|
145
222
|
if (parsed.valueKind === 'token') {
|
|
146
223
|
value = `${parsed.negative ? '-' : ''}${value}`
|
|
224
|
+
if (grammarConfig && parsed.entry?.tokenCategory) {
|
|
225
|
+
value =
|
|
226
|
+
resolveTokenName(grammarConfig, parsed.entry.tokenCategory, value) ?? value
|
|
227
|
+
}
|
|
147
228
|
} else if (parsed.valueKind === 'arbitrary') {
|
|
148
|
-
value =
|
|
229
|
+
value = unwrapArbitraryValue(value)
|
|
149
230
|
} else {
|
|
150
231
|
return null
|
|
151
232
|
}
|
|
@@ -188,10 +269,9 @@ function tailwindClassToFlatProp(
|
|
|
188
269
|
let lhValue: any
|
|
189
270
|
if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
|
|
190
271
|
const inner = decodeArbitrary(value.slice(1, -1))
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
lhValue = /^-?\d*\.?\d+px$/.test(inner) ? Number.parseFloat(inner) : inner
|
|
272
|
+
// preserve both forms until core's target lowering: px is absolute and a
|
|
273
|
+
// unitless numeric string is a ratio on web and native.
|
|
274
|
+
lhValue = inner
|
|
195
275
|
} else {
|
|
196
276
|
lhValue = value
|
|
197
277
|
}
|
|
@@ -222,23 +302,47 @@ function tailwindClassToFlatProp(
|
|
|
222
302
|
}
|
|
223
303
|
|
|
224
304
|
// arbitrary values: p-[4px], w-[100px], rounded-[8px], min-h-[100vh], rotate-[-8deg],
|
|
225
|
-
// h-[calc(100%-2px)], bg-[var(--
|
|
305
|
+
// h-[calc(100%-2px)], bg-[var(--color-5)], bg-[#fff]. use the bracketed value directly as
|
|
226
306
|
// CSS — no scaling/token resolution. tailwind encodes spaces inside [] as underscores.
|
|
227
307
|
if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
|
|
228
308
|
const inner = decodeArbitrary(value.slice(1, -1))
|
|
229
309
|
if (inner === '') return null
|
|
230
310
|
// px-length + unitless arbitraries become NUMBERS (native requires numbers, drops "Npx"
|
|
231
311
|
// strings); unit/function values stay strings. one canonical rule (arbitraryValue).
|
|
232
|
-
|
|
312
|
+
let resolved = arbitraryValue(inner, prop)
|
|
313
|
+
// rotate requires a unit-bearing string on native — a bare number from rotate-[45]
|
|
314
|
+
// triggers a redbox. append deg when the arbitrary resolved to a unitless number.
|
|
315
|
+
if (
|
|
316
|
+
(prop === 'rotate' ||
|
|
317
|
+
prop === 'rotateX' ||
|
|
318
|
+
prop === 'rotateY' ||
|
|
319
|
+
prop === 'rotateZ' ||
|
|
320
|
+
prop === 'skewX' ||
|
|
321
|
+
prop === 'skewY') &&
|
|
322
|
+
typeof resolved === 'number'
|
|
323
|
+
) {
|
|
324
|
+
resolved = `${resolved}deg`
|
|
325
|
+
}
|
|
326
|
+
if (parsed.negative) {
|
|
327
|
+
if (typeof resolved === 'number') resolved = -resolved
|
|
328
|
+
else if (resolved[0] !== '-') resolved = `-${resolved}`
|
|
329
|
+
}
|
|
330
|
+
return { key: prop, value: resolved }
|
|
233
331
|
}
|
|
234
332
|
|
|
235
333
|
// tailwind sizing keywords / fractions (w-full → 100%, w-1/2 → 50%, w-auto, w-screen).
|
|
236
334
|
// handled before isValidTailwindValue since fractions/keywords aren't plain CSS values. An
|
|
237
335
|
// exact configured size token with the same spelling stays a token.
|
|
238
|
-
if (
|
|
336
|
+
if (
|
|
337
|
+
parsed.valueKind !== 'token' &&
|
|
338
|
+
(category === 'size' || parsed.convenience === 'sizing-keyword')
|
|
339
|
+
) {
|
|
239
340
|
const sized = tailwindSizingValue(prop, value)
|
|
240
341
|
if (sized != null) {
|
|
241
|
-
return {
|
|
342
|
+
return {
|
|
343
|
+
key: prop,
|
|
344
|
+
value: parsed.negative && sized[0] !== '-' ? `-${sized}` : sized,
|
|
345
|
+
}
|
|
242
346
|
}
|
|
243
347
|
}
|
|
244
348
|
|
|
@@ -264,9 +368,12 @@ function tailwindClassToFlatProp(
|
|
|
264
368
|
value = Number(value) / 100
|
|
265
369
|
} else if (/^\d+(\.\d+)?$/.test(value)) {
|
|
266
370
|
if (category === 'zIndex' && parsed.valueKind === 'convenience') {
|
|
267
|
-
value = Number(value)
|
|
371
|
+
value = Number(value) * (parsed.negative ? -1 : 1)
|
|
268
372
|
} else if (category) {
|
|
269
373
|
value = `${parsed.negative ? '-' : ''}${value}`
|
|
374
|
+
if (grammarConfig) {
|
|
375
|
+
value = resolveTokenName(grammarConfig, category, value) ?? value
|
|
376
|
+
}
|
|
270
377
|
} else {
|
|
271
378
|
value = Number(value)
|
|
272
379
|
}
|
|
@@ -317,6 +424,71 @@ type TailwindParentPlan = {
|
|
|
317
424
|
type CachedTailwindClassPlan = TailwindClassPlan | TailwindParentPlan
|
|
318
425
|
|
|
319
426
|
const classPlanCache = new WeakMap<object, Map<string, CachedTailwindClassPlan>>()
|
|
427
|
+
const nativeTarget = !isWeb || process.env.TAMAGUI_TARGET === 'native'
|
|
428
|
+
const cssGridProps = new Set([
|
|
429
|
+
'gridTemplateColumns',
|
|
430
|
+
'gridColumn',
|
|
431
|
+
'gridColumnStart',
|
|
432
|
+
'gridColumnEnd',
|
|
433
|
+
'gridRow',
|
|
434
|
+
'gridRowStart',
|
|
435
|
+
'gridRowEnd',
|
|
436
|
+
])
|
|
437
|
+
const nativeUnsupportedProps = new Set([
|
|
438
|
+
'objectFit',
|
|
439
|
+
'order',
|
|
440
|
+
'overflowX',
|
|
441
|
+
'overflowY',
|
|
442
|
+
'textOverflow',
|
|
443
|
+
'whiteSpace',
|
|
444
|
+
'visibility',
|
|
445
|
+
])
|
|
446
|
+
const nativeUnsupportedSizingValues = new Set(['min', 'max', 'fit'])
|
|
447
|
+
// values outside react native's style unions, which native would drop
|
|
448
|
+
const nativeUnsupportedValues: Record<string, Set<string>> = {
|
|
449
|
+
display: new Set(['block', 'inline', 'inline-flex']),
|
|
450
|
+
position: new Set(['fixed', 'sticky']),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function shouldGateNative(parsed: ParsedCandidate): boolean {
|
|
454
|
+
if (!nativeTarget) return false
|
|
455
|
+
const properties = parsed.properties || {}
|
|
456
|
+
if (
|
|
457
|
+
process.env.TAMAGUI_CSS_GRID !== '1' &&
|
|
458
|
+
((parsed.entry && cssGridProps.has(parsed.entry.prop)) ||
|
|
459
|
+
properties.display === 'grid' ||
|
|
460
|
+
Object.keys(properties).some((prop) => cssGridProps.has(prop)))
|
|
461
|
+
) {
|
|
462
|
+
return true
|
|
463
|
+
}
|
|
464
|
+
if (
|
|
465
|
+
(parsed.entry && nativeUnsupportedProps.has(parsed.entry.prop)) ||
|
|
466
|
+
Object.keys(properties).some((prop) => nativeUnsupportedProps.has(prop))
|
|
467
|
+
) {
|
|
468
|
+
return true
|
|
469
|
+
}
|
|
470
|
+
if (
|
|
471
|
+
parsed.convenience === 'sizing-keyword' &&
|
|
472
|
+
nativeUnsupportedSizingValues.has(parsed.rawValue || '')
|
|
473
|
+
) {
|
|
474
|
+
return true
|
|
475
|
+
}
|
|
476
|
+
const entryProp = parsed.entry?.prop
|
|
477
|
+
if (
|
|
478
|
+
(entryProp && nativeUnsupportedValues[entryProp]?.has(parsed.rawValue || '')) ||
|
|
479
|
+
Object.keys(properties).some((prop) =>
|
|
480
|
+
nativeUnsupportedValues[prop]?.has(String(properties[prop]))
|
|
481
|
+
)
|
|
482
|
+
) {
|
|
483
|
+
return true
|
|
484
|
+
}
|
|
485
|
+
return (
|
|
486
|
+
properties.overflow === 'auto' ||
|
|
487
|
+
(properties.cursor !== undefined &&
|
|
488
|
+
properties.cursor !== 'auto' &&
|
|
489
|
+
properties.cursor !== 'pointer')
|
|
490
|
+
)
|
|
491
|
+
}
|
|
320
492
|
|
|
321
493
|
function getClassPlanCache(grammarConfig: object) {
|
|
322
494
|
let cache = classPlanCache.get(grammarConfig)
|
|
@@ -343,6 +515,89 @@ function createPlanEntry(
|
|
|
343
515
|
return [property, payload, condition, modifiers]
|
|
344
516
|
}
|
|
345
517
|
|
|
518
|
+
// CSS shorthands reset every longhand, including logical ones. RN stores those
|
|
519
|
+
// under different keys (`paddingStart` vs `paddingLeft`, `columnGap` vs `gap`),
|
|
520
|
+
// so `ps-2 p-4` would otherwise keep the earlier logical value. Unmodified
|
|
521
|
+
// shorthand plans prepend `unset` entries so both the tokenizer and the
|
|
522
|
+
// renderer sink clear them. Conditional classes (`hover:p-4`) skip this.
|
|
523
|
+
const shorthandResets: Record<string, readonly string[]> = {
|
|
524
|
+
padding: [
|
|
525
|
+
'paddingTop',
|
|
526
|
+
'paddingRight',
|
|
527
|
+
'paddingBottom',
|
|
528
|
+
'paddingLeft',
|
|
529
|
+
'paddingStart',
|
|
530
|
+
'paddingEnd',
|
|
531
|
+
'paddingInlineStart',
|
|
532
|
+
'paddingInlineEnd',
|
|
533
|
+
'paddingBlockStart',
|
|
534
|
+
'paddingBlockEnd',
|
|
535
|
+
'paddingHorizontal',
|
|
536
|
+
'paddingVertical',
|
|
537
|
+
'paddingInline',
|
|
538
|
+
'paddingBlock',
|
|
539
|
+
],
|
|
540
|
+
margin: [
|
|
541
|
+
'marginTop',
|
|
542
|
+
'marginRight',
|
|
543
|
+
'marginBottom',
|
|
544
|
+
'marginLeft',
|
|
545
|
+
'marginStart',
|
|
546
|
+
'marginEnd',
|
|
547
|
+
'marginInlineStart',
|
|
548
|
+
'marginInlineEnd',
|
|
549
|
+
'marginBlockStart',
|
|
550
|
+
'marginBlockEnd',
|
|
551
|
+
'marginHorizontal',
|
|
552
|
+
'marginVertical',
|
|
553
|
+
'marginInline',
|
|
554
|
+
'marginBlock',
|
|
555
|
+
],
|
|
556
|
+
gap: ['rowGap', 'columnGap'],
|
|
557
|
+
borderRadius: [
|
|
558
|
+
'borderTopLeftRadius',
|
|
559
|
+
'borderTopRightRadius',
|
|
560
|
+
'borderBottomLeftRadius',
|
|
561
|
+
'borderBottomRightRadius',
|
|
562
|
+
'borderTopStartRadius',
|
|
563
|
+
'borderTopEndRadius',
|
|
564
|
+
'borderBottomStartRadius',
|
|
565
|
+
'borderBottomEndRadius',
|
|
566
|
+
'borderStartStartRadius',
|
|
567
|
+
'borderStartEndRadius',
|
|
568
|
+
'borderEndStartRadius',
|
|
569
|
+
'borderEndEndRadius',
|
|
570
|
+
],
|
|
571
|
+
borderWidth: [
|
|
572
|
+
'borderTopWidth',
|
|
573
|
+
'borderRightWidth',
|
|
574
|
+
'borderBottomWidth',
|
|
575
|
+
'borderLeftWidth',
|
|
576
|
+
'borderStartWidth',
|
|
577
|
+
'borderEndWidth',
|
|
578
|
+
'borderInlineStartWidth',
|
|
579
|
+
'borderInlineEndWidth',
|
|
580
|
+
'borderBlockStartWidth',
|
|
581
|
+
'borderBlockEndWidth',
|
|
582
|
+
'borderInlineWidth',
|
|
583
|
+
'borderBlockWidth',
|
|
584
|
+
],
|
|
585
|
+
borderColor: [
|
|
586
|
+
'borderTopColor',
|
|
587
|
+
'borderRightColor',
|
|
588
|
+
'borderBottomColor',
|
|
589
|
+
'borderLeftColor',
|
|
590
|
+
'borderStartColor',
|
|
591
|
+
'borderEndColor',
|
|
592
|
+
'borderInlineStartColor',
|
|
593
|
+
'borderInlineEndColor',
|
|
594
|
+
'borderBlockStartColor',
|
|
595
|
+
'borderBlockEndColor',
|
|
596
|
+
'borderInlineColor',
|
|
597
|
+
'borderBlockColor',
|
|
598
|
+
],
|
|
599
|
+
}
|
|
600
|
+
|
|
346
601
|
function computeClassPlan(
|
|
347
602
|
cls: string,
|
|
348
603
|
grammarConfig: GrammarConfigView
|
|
@@ -372,13 +627,30 @@ function computeClassPlan(
|
|
|
372
627
|
return isWeb ? 'raw' : null
|
|
373
628
|
}
|
|
374
629
|
const parsed = classification.parsed
|
|
630
|
+
if (!nativeTarget && parsed.entry?.prop === 'perspective') return 'raw'
|
|
631
|
+
if (shouldGateNative(parsed)) {
|
|
632
|
+
return null
|
|
633
|
+
}
|
|
634
|
+
if (parsed.entry?.prop === 'flex' && parsed.rawValue) {
|
|
635
|
+
const fraction = /^(\d+)\/(\d+)$/.exec(parsed.rawValue)
|
|
636
|
+
if (fraction && Number(fraction[2]) !== 0) {
|
|
637
|
+
const basis = `${(Number(fraction[1]) / Number(fraction[2])) * 100}%`
|
|
638
|
+
const entries = [
|
|
639
|
+
createPlanEntry('flexGrow', 1, parsed.modifiers),
|
|
640
|
+
createPlanEntry('flexShrink', 1, parsed.modifiers),
|
|
641
|
+
createPlanEntry('flexBasis', basis, parsed.modifiers),
|
|
642
|
+
]
|
|
643
|
+
return entries.every(Boolean) ? (entries as TailwindPlanEntry[]) : 'raw'
|
|
644
|
+
}
|
|
645
|
+
}
|
|
375
646
|
// named utilities first (flex-row, flex-1, hidden, …) — whole class → fixed prop(s).
|
|
376
647
|
// these may emit multiple props and may have no dash, so handle before the generic parse.
|
|
377
648
|
const util = parsed.kind === 'utility' ? parsed.properties : null
|
|
378
649
|
if (util) {
|
|
379
650
|
const entries: TailwindPlanEntry[] = []
|
|
380
651
|
for (const p in util) {
|
|
381
|
-
const
|
|
652
|
+
const value = nativeTarget && p === 'transform' && util[p] === 'none' ? [] : util[p]
|
|
653
|
+
const entry = createPlanEntry(p, value, parsed.modifiers)
|
|
382
654
|
if (!entry) return 'raw'
|
|
383
655
|
entries.push(entry)
|
|
384
656
|
}
|
|
@@ -390,10 +662,13 @@ function computeClassPlan(
|
|
|
390
662
|
if (expandedProps) {
|
|
391
663
|
const entries: TailwindPlanEntry[] = []
|
|
392
664
|
for (const prop of expandedProps) {
|
|
393
|
-
const flatProp = tailwindClassToFlatProp(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
665
|
+
const flatProp = tailwindClassToFlatProp(
|
|
666
|
+
{
|
|
667
|
+
...parsed,
|
|
668
|
+
entry: parsed.entry ? { ...parsed.entry, prop } : parsed.entry,
|
|
669
|
+
},
|
|
670
|
+
grammarConfig
|
|
671
|
+
)
|
|
397
672
|
if (!flatProp) return 'raw'
|
|
398
673
|
const entry = createPlanEntry(flatProp.key, flatProp.value, parsed.modifiers)
|
|
399
674
|
if (!entry) return 'raw'
|
|
@@ -401,7 +676,7 @@ function computeClassPlan(
|
|
|
401
676
|
}
|
|
402
677
|
return entries
|
|
403
678
|
}
|
|
404
|
-
const flatProp = tailwindClassToFlatProp(parsed)
|
|
679
|
+
const flatProp = tailwindClassToFlatProp(parsed, grammarConfig)
|
|
405
680
|
if (flatProp) {
|
|
406
681
|
const entry = createPlanEntry(flatProp.key, flatProp.value, parsed.modifiers)
|
|
407
682
|
return entry ? [entry] : 'raw'
|
|
@@ -424,12 +699,85 @@ export function getTailwindClassPlan(
|
|
|
424
699
|
return plan
|
|
425
700
|
}
|
|
426
701
|
|
|
702
|
+
export function resolveTailwindCandidate(
|
|
703
|
+
candidate: string,
|
|
704
|
+
config: StyleFrontendConfig,
|
|
705
|
+
sink: FrontendClassSink
|
|
706
|
+
): boolean | null {
|
|
707
|
+
const composed = tryCompose(candidate, getStyleGrammarConfig(config), sink)
|
|
708
|
+
if (composed !== undefined) return composed
|
|
709
|
+
const plan = getTailwindClassPlan(candidate, config)
|
|
710
|
+
if (plan === 'raw') return true
|
|
711
|
+
if (plan === null) return null
|
|
712
|
+
const parent = Array.isArray(plan) ? null : plan
|
|
713
|
+
const entries = parent ? parent.entries : plan
|
|
714
|
+
for (let index = 0; index < entries.length; index++) {
|
|
715
|
+
const entry = entries[index]
|
|
716
|
+
const transformTokenCategory =
|
|
717
|
+
entry[0] === 'x' || entry[0] === 'y'
|
|
718
|
+
? 'space'
|
|
719
|
+
: entry[0] === 'perspective'
|
|
720
|
+
? 'perspective'
|
|
721
|
+
: null
|
|
722
|
+
const transformValue =
|
|
723
|
+
transformTokenCategory && typeof entry[1] === 'string'
|
|
724
|
+
? ((config as any).tokensParsed?.[transformTokenCategory]?.[entry[1]]?.val ??
|
|
725
|
+
entry[1])
|
|
726
|
+
: entry[1]
|
|
727
|
+
if (noteTailwindTransform(sink, [entry[0], transformValue, entry[2], entry[3]])) {
|
|
728
|
+
continue
|
|
729
|
+
}
|
|
730
|
+
// a text size carries its paired line height as a default that any leading
|
|
731
|
+
// utility overrides whatever the class order, as tailwind's --tw-leading does
|
|
732
|
+
if (entry[0] === 'lineHeight') {
|
|
733
|
+
sink(['__leading', entry[1], entry[2], entry[3]])
|
|
734
|
+
continue
|
|
735
|
+
}
|
|
736
|
+
if (
|
|
737
|
+
entry[0] === 'fontSize' &&
|
|
738
|
+
typeof entry[1] === 'string' &&
|
|
739
|
+
hasTokenName(getStyleGrammarConfig(config), 'lineHeight', entry[1])
|
|
740
|
+
) {
|
|
741
|
+
sink(['__textLeading', entry[1], entry[2], entry[3]])
|
|
742
|
+
}
|
|
743
|
+
if (entry[0] === 'boxShadow' && typeof entry[1] === 'string') {
|
|
744
|
+
noteBoxShadow(
|
|
745
|
+
sink,
|
|
746
|
+
(config as any).tokensParsed?.boxShadow?.[entry[1]]?.val ?? entry[1],
|
|
747
|
+
entry[3] || []
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
sink(entry)
|
|
751
|
+
}
|
|
752
|
+
return parent?.preserveRawClass || false
|
|
753
|
+
}
|
|
754
|
+
|
|
427
755
|
export function resolveTailwindClassName(
|
|
428
756
|
className: string,
|
|
429
757
|
config: StyleFrontendConfig
|
|
430
758
|
): Record<string, any> {
|
|
431
759
|
const result: Record<string, any> = {}
|
|
760
|
+
// composed utilities collect under `__`-prefixed keys and are folded once at the
|
|
761
|
+
// end, the same shape and the same order the runtime walk uses
|
|
762
|
+
let composedProps: Record<string, any> | null = null
|
|
432
763
|
let rawClassName = ''
|
|
764
|
+
const sink: FrontendClassSink = (entry) => {
|
|
765
|
+
const [key, value, condition] = entry
|
|
766
|
+
if (key.charCodeAt(0) === 95 && key.charCodeAt(1) === 95) {
|
|
767
|
+
composedProps ||= {}
|
|
768
|
+
composedProps[key] = mergeFrontendCondition(composedProps[key], value, condition)
|
|
769
|
+
return
|
|
770
|
+
}
|
|
771
|
+
if (condition === undefined) {
|
|
772
|
+
const resets = shorthandResets[key]
|
|
773
|
+
if (resets) {
|
|
774
|
+
for (let i = 0; i < resets.length; i++) {
|
|
775
|
+
delete result[resets[i]]
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
setInAuthoredOrder(result, key, mergeFrontendCondition(result[key], value, condition))
|
|
780
|
+
}
|
|
433
781
|
let start = 0
|
|
434
782
|
for (let index = 0; index <= className.length; index++) {
|
|
435
783
|
if (index !== className.length && className.charCodeAt(index) > 32) continue
|
|
@@ -438,34 +786,18 @@ export function resolveTailwindClassName(
|
|
|
438
786
|
continue
|
|
439
787
|
}
|
|
440
788
|
const candidate = className.slice(start, index)
|
|
441
|
-
const
|
|
442
|
-
if (
|
|
789
|
+
const preserveRaw = resolveTailwindCandidate(candidate, config, sink)
|
|
790
|
+
if (preserveRaw) {
|
|
443
791
|
rawClassName = rawClassName ? `${rawClassName} ${candidate}` : candidate
|
|
444
|
-
} else if (plan) {
|
|
445
|
-
const parentPlan = plan as TailwindParentPlan
|
|
446
|
-
if (!Array.isArray(plan) && parentPlan.preserveRawClass) {
|
|
447
|
-
rawClassName = rawClassName ? `${rawClassName} ${candidate}` : candidate
|
|
448
|
-
}
|
|
449
|
-
const entries = Array.isArray(plan) ? plan : parentPlan.entries
|
|
450
|
-
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
|
|
451
|
-
const [key, value, condition] = entries[entryIndex]
|
|
452
|
-
const previous = result[key]
|
|
453
|
-
let next = value
|
|
454
|
-
if (condition !== undefined) {
|
|
455
|
-
next =
|
|
456
|
-
previous && typeof previous === 'object' && !Array.isArray(previous)
|
|
457
|
-
? { ...previous, [condition]: value }
|
|
458
|
-
: previous === undefined
|
|
459
|
-
? { [condition]: value }
|
|
460
|
-
: { default: previous, [condition]: value }
|
|
461
|
-
} else if (previous && typeof previous === 'object' && !Array.isArray(previous)) {
|
|
462
|
-
next = { ...previous, default: value }
|
|
463
|
-
}
|
|
464
|
-
setInAuthoredOrder(result, key, next)
|
|
465
|
-
}
|
|
466
792
|
}
|
|
467
793
|
start = index + 1
|
|
468
794
|
}
|
|
795
|
+
if (composedProps) {
|
|
796
|
+
const composed = composedResolver(composedProps)
|
|
797
|
+
for (const key in composed) {
|
|
798
|
+
setInAuthoredOrder(result, key, composed[key])
|
|
799
|
+
}
|
|
800
|
+
}
|
|
469
801
|
if (rawClassName) result.className = rawClassName
|
|
470
802
|
return result
|
|
471
803
|
}
|
|
@@ -484,6 +816,10 @@ export function setInAuthoredOrder(
|
|
|
484
816
|
key: string,
|
|
485
817
|
value: any
|
|
486
818
|
): void {
|
|
819
|
+
if (value === 'unset') {
|
|
820
|
+
delete target[key]
|
|
821
|
+
return
|
|
822
|
+
}
|
|
487
823
|
if (key in target) delete target[key]
|
|
488
824
|
target[key] = value
|
|
489
825
|
}
|
package/src/frontend.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type FrontendStaticConfig,
|
|
3
3
|
type FrontendStaticConfigNormalization,
|
|
4
|
-
type FrontendClassSink,
|
|
5
4
|
type StyleFrontend,
|
|
6
5
|
type StyleFrontendConfig,
|
|
7
6
|
} from '@tamagui/core/internal-runtime'
|
|
8
|
-
import {
|
|
7
|
+
import { resolveTailwindCandidate, resolveTailwindClassName } from './candidate'
|
|
8
|
+
import { composedResolver } from './composedResolver'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* The Tailwind frontend descriptor.
|
|
@@ -66,21 +66,8 @@ function normalizeTailwindStaticConfig(
|
|
|
66
66
|
return { baseStyle, passthroughClassName, variants }
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function resolveTailwindCandidate(
|
|
70
|
-
candidate: string,
|
|
71
|
-
config: StyleFrontendConfig,
|
|
72
|
-
sink: FrontendClassSink
|
|
73
|
-
): boolean | null {
|
|
74
|
-
const plan = getTailwindClassPlan(candidate, config)
|
|
75
|
-
if (plan === 'raw') return true
|
|
76
|
-
if (plan === null) return null
|
|
77
|
-
const parent = Array.isArray(plan) ? null : plan
|
|
78
|
-
const entries = parent ? parent.entries : plan
|
|
79
|
-
for (let index = 0; index < entries.length; index++) sink(entries[index])
|
|
80
|
-
return parent?.preserveRawClass || false
|
|
81
|
-
}
|
|
82
|
-
|
|
83
69
|
export const tailwindStyleFrontend: StyleFrontend = {
|
|
84
70
|
resolveClassName: resolveTailwindCandidate,
|
|
85
71
|
normalizeStaticConfig: normalizeTailwindStaticConfig,
|
|
72
|
+
compose: composedResolver,
|
|
86
73
|
}
|