@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.
- package/dist/cjs/candidate.cjs +540 -0
- package/dist/cjs/candidate.native.cjs +578 -0
- package/dist/cjs/candidate.native.js +580 -0
- package/dist/cjs/candidate.native.js.map +1 -0
- package/dist/cjs/compose.cjs +352 -0
- package/dist/cjs/compose.native.cjs +358 -0
- package/dist/cjs/compose.native.js +360 -0
- package/dist/cjs/compose.native.js.map +1 -0
- package/dist/cjs/composedResolver.cjs +175 -0
- package/dist/cjs/composedResolver.native.cjs +201 -0
- package/dist/cjs/composedResolver.native.js +203 -0
- package/dist/cjs/composedResolver.native.js.map +1 -0
- package/dist/cjs/frontend.cjs +54 -0
- package/dist/cjs/frontend.native.cjs +62 -0
- package/dist/cjs/frontend.native.js +64 -0
- package/dist/cjs/frontend.native.js.map +1 -0
- package/dist/cjs/index.cjs +38 -0
- package/dist/cjs/index.native.cjs +40 -0
- package/dist/cjs/index.native.js +42 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/cjs/styled.cjs +29 -0
- package/dist/cjs/styled.native.cjs +31 -0
- package/dist/cjs/styled.native.js +33 -0
- package/dist/cjs/styled.native.js.map +1 -0
- package/dist/cjs/types.cjs +16 -0
- package/dist/cjs/types.native.cjs +18 -0
- package/dist/cjs/types.native.js +20 -0
- package/dist/cjs/types.native.js.map +1 -0
- package/dist/cjs/vite/state.cjs +249 -0
- package/dist/cjs/vite/state.native.cjs +331 -0
- package/dist/cjs/vite/state.native.js +333 -0
- package/dist/cjs/vite/state.native.js.map +1 -0
- package/dist/cjs/vite.cjs +147 -0
- package/dist/cjs/vite.native.cjs +187 -0
- package/dist/cjs/vite.native.js +189 -0
- package/dist/cjs/vite.native.js.map +1 -0
- package/dist/esm/candidate.mjs +514 -0
- package/dist/esm/candidate.mjs.map +1 -0
- package/dist/esm/candidate.native.js +550 -0
- package/dist/esm/candidate.native.js.map +1 -0
- package/dist/esm/compose.mjs +328 -0
- package/dist/esm/compose.mjs.map +1 -0
- package/dist/esm/compose.native.js +332 -0
- package/dist/esm/compose.native.js.map +1 -0
- package/dist/esm/composedResolver.mjs +155 -0
- package/dist/esm/composedResolver.mjs.map +1 -0
- package/dist/esm/composedResolver.native.js +179 -0
- package/dist/esm/composedResolver.native.js.map +1 -0
- package/dist/esm/frontend.mjs +32 -0
- package/dist/esm/frontend.mjs.map +1 -0
- package/dist/esm/frontend.native.js +38 -0
- package/dist/esm/frontend.native.js.map +1 -0
- package/dist/esm/index.mjs +11 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +11 -0
- package/dist/esm/index.native.js.map +1 -0
- package/dist/esm/styled.mjs +10 -0
- package/dist/esm/styled.mjs.map +1 -0
- package/dist/esm/styled.native.js +10 -0
- package/dist/esm/styled.native.js.map +1 -0
- package/dist/esm/types.mjs +0 -0
- package/dist/esm/types.native.js +0 -0
- package/dist/esm/vite/state.mjs +216 -0
- package/dist/esm/vite/state.mjs.map +1 -0
- package/dist/esm/vite/state.native.js +296 -0
- package/dist/esm/vite/state.native.js.map +1 -0
- package/dist/esm/vite.mjs +116 -0
- package/dist/esm/vite.mjs.map +1 -0
- package/dist/esm/vite.native.js +154 -0
- package/dist/esm/vite.native.js.map +1 -0
- package/package.json +90 -5
- package/src/candidate.ts +805 -0
- package/src/frontend.ts +73 -0
- package/src/index.tsx +35 -0
- package/src/styled.tsx +52 -0
- package/src/types.ts +249 -0
- package/src/vite/state.ts +274 -0
- package/src/vite.ts +204 -0
- package/types/candidate.d.ts +18 -0
- package/types/candidate.d.ts.map +1 -0
- package/types/compose.d.ts +19 -0
- package/types/compose.d.ts.map +1 -0
- package/types/composedResolver.d.ts +18 -0
- package/types/composedResolver.d.ts.map +1 -0
- package/types/frontend.d.ts +19 -0
- package/types/frontend.d.ts.map +1 -0
- package/types/index.d.ts +17 -0
- package/types/index.d.ts.map +1 -0
- package/types/styled.d.ts +12 -0
- package/types/styled.d.ts.map +1 -0
- package/types/types.d.ts +169 -0
- package/types/types.d.ts.map +1 -0
- package/types/vite/state.d.ts +28 -0
- package/types/vite/state.d.ts.map +1 -0
- package/types/vite.d.ts +20 -0
- package/types/vite.d.ts.map +1 -0
- package/README.md +0 -1
package/src/candidate.ts
ADDED
|
@@ -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
|
+
}
|