@tamagui/tailwind 0.0.0-bootstrap.0 → 3.0.0-beta.637.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 +385 -0
- package/dist/cjs/candidate.native.cjs +431 -0
- package/dist/cjs/candidate.native.js +433 -0
- package/dist/cjs/candidate.native.js.map +1 -0
- package/dist/cjs/frontend.cjs +80 -0
- package/dist/cjs/frontend.native.cjs +95 -0
- package/dist/cjs/frontend.native.js +97 -0
- package/dist/cjs/frontend.native.js.map +1 -0
- package/dist/cjs/index.cjs +39 -0
- package/dist/cjs/index.native.cjs +41 -0
- package/dist/cjs/index.native.js +43 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/cjs/styled.cjs +30 -0
- package/dist/cjs/styled.native.cjs +32 -0
- package/dist/cjs/styled.native.js +34 -0
- package/dist/cjs/styled.native.js.map +1 -0
- package/dist/cjs/types.cjs +17 -0
- package/dist/cjs/types.native.cjs +19 -0
- package/dist/cjs/types.native.js +21 -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 +148 -0
- package/dist/cjs/vite.native.cjs +188 -0
- package/dist/cjs/vite.native.js +190 -0
- package/dist/cjs/vite.native.js.map +1 -0
- package/dist/esm/candidate.mjs +360 -0
- package/dist/esm/candidate.mjs.map +1 -0
- package/dist/esm/candidate.native.js +404 -0
- package/dist/esm/candidate.native.js.map +1 -0
- package/dist/esm/frontend.mjs +57 -0
- package/dist/esm/frontend.mjs.map +1 -0
- package/dist/esm/frontend.native.js +70 -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 +215 -0
- package/dist/esm/vite/state.mjs.map +1 -0
- package/dist/esm/vite/state.native.js +295 -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 +88 -5
- package/src/candidate.ts +527 -0
- package/src/frontend.ts +116 -0
- package/src/index.tsx +24 -0
- package/src/styled.tsx +52 -0
- package/src/types.ts +226 -0
- package/src/vite/state.ts +274 -0
- package/src/vite.ts +204 -0
- package/types/candidate.d.ts +23 -0
- package/types/candidate.d.ts.map +1 -0
- package/types/frontend.d.ts +20 -0
- package/types/frontend.d.ts.map +1 -0
- package/types/index.d.ts +8 -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 +158 -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,527 @@
|
|
|
1
|
+
import { isWeb } from '@tamagui/constants'
|
|
2
|
+
import {
|
|
3
|
+
STYLE_FRONTEND_PASSTHROUGH_PREFIX,
|
|
4
|
+
createFrontendProgram,
|
|
5
|
+
plainValueToPayload,
|
|
6
|
+
type StyleFrontendConfig,
|
|
7
|
+
} from '@tamagui/core/internal-runtime'
|
|
8
|
+
import {
|
|
9
|
+
borderSideSuffix,
|
|
10
|
+
classifyCandidate,
|
|
11
|
+
createGrammarConfigView,
|
|
12
|
+
decodeArbitrary,
|
|
13
|
+
getTokenCategory,
|
|
14
|
+
getSafeAreaEdge,
|
|
15
|
+
percentUtilityProps,
|
|
16
|
+
radiusCornerProps,
|
|
17
|
+
splitColorOpacitySuffix,
|
|
18
|
+
type GrammarConfigView,
|
|
19
|
+
type ParsedCandidate,
|
|
20
|
+
} from '@tamagui/style-grammar/runtime'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Class-string tokenization and candidate adaptation. This is the Tailwind
|
|
24
|
+
* frontend's private half of the pipeline: everything here is about how a
|
|
25
|
+
* candidate is SPELLED (brackets, slashes, fractions, keywords, negatives).
|
|
26
|
+
*
|
|
27
|
+
* The semantic parser itself stays in `@tamagui/style-grammar`, shared with the
|
|
28
|
+
* converter and build tooling, and everything downstream of the props this module
|
|
29
|
+
* produces — value programs, merging, lowering, evaluation — is core's shared
|
|
30
|
+
* renderer.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const styleGrammarConfigCache = new WeakMap<StyleFrontendConfig, GrammarConfigView>()
|
|
34
|
+
|
|
35
|
+
export function getStyleGrammarConfig(config: StyleFrontendConfig): GrammarConfigView {
|
|
36
|
+
const cached = styleGrammarConfigCache.get(config)
|
|
37
|
+
if (cached) return cached
|
|
38
|
+
const view = createGrammarConfigView(config)
|
|
39
|
+
styleGrammarConfigCache.set(config, view)
|
|
40
|
+
return view
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isTokenValueProp(prop: string): boolean {
|
|
44
|
+
return getTokenCategory(prop) !== null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// CANONICAL arbitrary-value coercion. an arbitrary `[..]` whose inner is a UNITLESS number
|
|
48
|
+
// (z-[400], aspect-[1.5], leading-[1.25]) or a PX length (p-[18px], text-[14px], border-[0.5px])
|
|
49
|
+
// resolves to a NUMBER: React Native REQUIRES numbers for dimensional + typography props and
|
|
50
|
+
// silently DROPS "Npx"/"400" strings (Yoga parseCSSProperty<CSSNumber,CSSPercentage> rejects px;
|
|
51
|
+
// StyleSheetTypes types fontSize/lineHeight/letterSpacing as number). web accepts the number and
|
|
52
|
+
// re-adds px via CSS. anything carrying a real unit/function (%, rem, vh, deg, calc(), var(),
|
|
53
|
+
// #hex, colors) stays a STRING.
|
|
54
|
+
function arbitraryValue(inner: string): number | string {
|
|
55
|
+
const px = /^(-?\d*\.?\d+)px$/.exec(inner)
|
|
56
|
+
if (px) return Number(px[1])
|
|
57
|
+
if (/^-?\d*\.?\d+$/.test(inner)) return Number(inner)
|
|
58
|
+
return inner
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// tailwind sizing keywords / fractions for width/height/min/max props.
|
|
62
|
+
// returns a CSS value, or null if `value` isn't a sizing keyword (falls through to normal parse).
|
|
63
|
+
function tailwindSizingValue(prop: string, value: string): string | null {
|
|
64
|
+
if (value === 'full') return '100%'
|
|
65
|
+
if (value === 'auto') return 'auto'
|
|
66
|
+
if (value === 'screen') return /[Hh]eight/.test(prop) ? '100vh' : '100vw'
|
|
67
|
+
if (value === 'min') return 'min-content'
|
|
68
|
+
if (value === 'max') return 'max-content'
|
|
69
|
+
if (value === 'fit') return 'fit-content'
|
|
70
|
+
const frac = /^(\d+)\/(\d+)$/.exec(value)
|
|
71
|
+
if (frac && Number(frac[2]) !== 0) {
|
|
72
|
+
return `${(Number(frac[1]) / Number(frac[2])) * 100}%`
|
|
73
|
+
}
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// unwrap a possibly-arbitrary value ([2px] → "2px") and coerce a bare number / px length to
|
|
78
|
+
// a NUMBER (2px → 2, 0.5 → 0.5) so borderWidth/radius match tamagui's numeric props; other
|
|
79
|
+
// units (1em, calc(…)) stay strings.
|
|
80
|
+
function borderDimValue(raw: string): number | string {
|
|
81
|
+
let inner = raw
|
|
82
|
+
if (raw.length > 1 && raw[0] === '[' && raw[raw.length - 1] === ']') {
|
|
83
|
+
inner = decodeArbitrary(raw.slice(1, -1))
|
|
84
|
+
}
|
|
85
|
+
const px = /^(-?\d*\.?\d+)px$/.exec(inner)
|
|
86
|
+
if (px) return Number.parseFloat(px[1])
|
|
87
|
+
if (/^-?\d*\.?\d+$/.test(inner)) return Number(inner)
|
|
88
|
+
return inner
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Expand a registry-parsed directional border/radius into all affected props. The registry
|
|
93
|
+
* already selected the token category and width-vs-color meaning; this function only expands
|
|
94
|
+
* that semantic result across sides/corners.
|
|
95
|
+
*/
|
|
96
|
+
function expandBorderCandidate(
|
|
97
|
+
parsed: ParsedCandidate,
|
|
98
|
+
value: any
|
|
99
|
+
): Record<string, any> | null {
|
|
100
|
+
const prefix = parsed.prefix
|
|
101
|
+
if (!prefix) return null
|
|
102
|
+
if (prefix.startsWith('rounded-')) {
|
|
103
|
+
const props = radiusCornerProps[prefix.slice('rounded-'.length)]
|
|
104
|
+
if (!props || props.length === 1) return null
|
|
105
|
+
const out: Record<string, any> = {}
|
|
106
|
+
for (const p of props) out[p] = value
|
|
107
|
+
return out
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const m = /^border-([trblxy])$/.exec(prefix)
|
|
111
|
+
if (!m) return null
|
|
112
|
+
const sides = borderSideSuffix[m[1]]
|
|
113
|
+
const suffix = parsed.entry?.prop.endsWith('Width') ? 'Width' : 'Color'
|
|
114
|
+
const out: Record<string, any> = {}
|
|
115
|
+
for (const side of sides) out[`border${side}${suffix}`] = value
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Adapt a registry-parsed candidate into the ordinary props shared rendering consumes.
|
|
121
|
+
* Examples:
|
|
122
|
+
* "hover:bg-blue5" → backgroundColor + a `hover:blue5` program
|
|
123
|
+
* "sm:p-4" → padding + an `sm:4` program
|
|
124
|
+
* "bg-[red]" → { key: "backgroundColor", value: "red" }
|
|
125
|
+
* "w-100" → { key: "width", value: 100 }
|
|
126
|
+
* "opacity-50" → { key: "opacity", value: 0.5 }
|
|
127
|
+
*/
|
|
128
|
+
function tailwindClassToFlatProp(
|
|
129
|
+
parsed: ParsedCandidate
|
|
130
|
+
): { key: string; value: any } | null {
|
|
131
|
+
if (parsed.kind !== 'dynamic' || !parsed.entry || parsed.rawValue === undefined) {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
const prop = parsed.entry.prop
|
|
135
|
+
const category = parsed.entry.tokenCategory
|
|
136
|
+
let value: any = parsed.rawValue
|
|
137
|
+
|
|
138
|
+
if (prop.endsWith('Width') && parsed.prefix?.startsWith('border')) {
|
|
139
|
+
if (parsed.valueKind === 'token') {
|
|
140
|
+
value = `${parsed.negative ? '-' : ''}${value}`
|
|
141
|
+
} else if (parsed.valueKind === 'arbitrary') {
|
|
142
|
+
value = borderDimValue(value)
|
|
143
|
+
} else {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
return { key: prop, value }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (prop === 'fontFamily') {
|
|
150
|
+
let famValue: string
|
|
151
|
+
if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
|
|
152
|
+
famValue = decodeArbitrary(value.slice(1, -1))
|
|
153
|
+
} else {
|
|
154
|
+
const generic: Record<string, string> = {
|
|
155
|
+
sans: 'sans-serif',
|
|
156
|
+
serif: 'serif',
|
|
157
|
+
mono: 'monospace',
|
|
158
|
+
}
|
|
159
|
+
famValue = parsed.valueKind === 'token' ? value : generic[value] || value
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
key: 'fontFamily',
|
|
163
|
+
value: famValue,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (prop === 'fontSize') {
|
|
168
|
+
let fsValue: any
|
|
169
|
+
if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
|
|
170
|
+
// fontSize is number-only on native: text-[14px] → 14 (arbitraryValue drops px)
|
|
171
|
+
fsValue = arbitraryValue(decodeArbitrary(value.slice(1, -1)))
|
|
172
|
+
} else {
|
|
173
|
+
fsValue = value
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
key: 'fontSize',
|
|
177
|
+
value: fsValue,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (prop === 'lineHeight') {
|
|
182
|
+
let lhValue: any
|
|
183
|
+
if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
|
|
184
|
+
const inner = decodeArbitrary(value.slice(1, -1))
|
|
185
|
+
// px length → NUMBER (native-valid: leading-[20px] → 20). a UNITLESS value is a web
|
|
186
|
+
// lineHeight MULTIPLIER and MUST stay a string (a number would be px-ified to "1.25px"
|
|
187
|
+
// on web, breaking the multiplier). RN has no unitless multiplier, so this is web-only.
|
|
188
|
+
lhValue = /^-?\d*\.?\d+px$/.test(inner) ? Number.parseFloat(inner) : inner
|
|
189
|
+
} else {
|
|
190
|
+
lhValue = value
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
key: 'lineHeight',
|
|
194
|
+
value: lhValue,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (prop === 'letterSpacing') {
|
|
199
|
+
let lsValue: any
|
|
200
|
+
if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
|
|
201
|
+
lsValue = arbitraryValue(decodeArbitrary(value.slice(1, -1)))
|
|
202
|
+
} else {
|
|
203
|
+
lsValue = value
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
key: 'letterSpacing',
|
|
207
|
+
value: lsValue,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (prop === 'boxShadow' && value[0] === '[' && value[value.length - 1] === ']') {
|
|
212
|
+
return {
|
|
213
|
+
key: 'boxShadow',
|
|
214
|
+
value: decodeArbitrary(value.slice(1, -1)),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// arbitrary values: p-[4px], w-[100px], rounded-[8px], min-h-[100vh], rotate-[-8deg],
|
|
219
|
+
// h-[calc(100%-2px)], bg-[var(--color5)], bg-[#fff]. use the bracketed value directly as
|
|
220
|
+
// CSS — no scaling/token resolution. tailwind encodes spaces inside [] as underscores.
|
|
221
|
+
if (value.length > 2 && value[0] === '[' && value[value.length - 1] === ']') {
|
|
222
|
+
const inner = decodeArbitrary(value.slice(1, -1))
|
|
223
|
+
if (inner === '') return null
|
|
224
|
+
// px-length + unitless arbitraries become NUMBERS (native requires numbers, drops "Npx"
|
|
225
|
+
// strings); unit/function values stay strings. one canonical rule (arbitraryValue).
|
|
226
|
+
return { key: prop, value: arbitraryValue(inner) }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// tailwind sizing keywords / fractions (w-full → 100%, w-1/2 → 50%, w-auto, w-screen).
|
|
230
|
+
// handled before isValidTailwindValue since fractions/keywords aren't plain CSS values. An
|
|
231
|
+
// exact configured size token with the same spelling stays a token.
|
|
232
|
+
if (category === 'size' && parsed.valueKind !== 'token') {
|
|
233
|
+
const sized = tailwindSizingValue(prop, value)
|
|
234
|
+
if (sized != null) {
|
|
235
|
+
return { key: prop, value: sized }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// color opacity modifier: bg-blue-500/50 → split value into base + /N suffix,
|
|
240
|
+
// re-attached after token resolution so the shared color resolver applies it via color-mix
|
|
241
|
+
// (web) / rgba (native). only for color props; for non-color props a "/" in the
|
|
242
|
+
// value is left intact (e.g. fraction sizing handled above).
|
|
243
|
+
let opacitySuffix = ''
|
|
244
|
+
if (category === 'color' && typeof value === 'string') {
|
|
245
|
+
const suffix = splitColorOpacitySuffix(value)
|
|
246
|
+
if (suffix.kind === 'invalid') {
|
|
247
|
+
return { key: prop, value }
|
|
248
|
+
}
|
|
249
|
+
if (suffix.kind === 'valid') {
|
|
250
|
+
opacitySuffix = value.slice(suffix.name.length)
|
|
251
|
+
value = suffix.name
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// handle special value patterns
|
|
256
|
+
if (percentUtilityProps.has(prop) && /^\d+$/.test(value)) {
|
|
257
|
+
// tailwind percentage utilities: opacity-50 → 0.5, scale-95 → 0.95, scale-100 → 1
|
|
258
|
+
value = Number(value) / 100
|
|
259
|
+
} else if (/^\d+(\.\d+)?$/.test(value)) {
|
|
260
|
+
if (category === 'zIndex' && parsed.valueKind === 'convenience') {
|
|
261
|
+
value = Number(value)
|
|
262
|
+
} else if (category) {
|
|
263
|
+
value = `${parsed.negative ? '-' : ''}${value}`
|
|
264
|
+
} else {
|
|
265
|
+
value = Number(value)
|
|
266
|
+
}
|
|
267
|
+
} else if (typeof value === 'string') {
|
|
268
|
+
if (
|
|
269
|
+
category &&
|
|
270
|
+
!(
|
|
271
|
+
(category === 'space' || category === 'size' || category === 'radius') &&
|
|
272
|
+
getSafeAreaEdge(value)
|
|
273
|
+
)
|
|
274
|
+
) {
|
|
275
|
+
value = `${parsed.negative ? '-' : ''}${value}`
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// re-attach the color opacity suffix (bg-blue-500/50). the shared flat-value
|
|
280
|
+
// resolver parses the trailing /N after its property-scoped name lookup.
|
|
281
|
+
if (opacitySuffix && typeof value === 'string') {
|
|
282
|
+
value = `${value}${opacitySuffix}`
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (parsed.negative && !category) {
|
|
286
|
+
if (typeof value === 'number') value = -value
|
|
287
|
+
else if (typeof value === 'string' && value[0] !== '-') value = `-${value}`
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { key: prop, value }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Parsing a Tailwind candidate depends only on the class string and the grammar
|
|
294
|
+
// config, never on the surrounding props, so the decision is cached per class.
|
|
295
|
+
// Without this the full parse (split + classify + resolve + border expansion) ran
|
|
296
|
+
// on every render of every component carrying a className — measured at ~18µs per
|
|
297
|
+
// call for a 4-class string, a 1.53x tax over writing the same styles as props.
|
|
298
|
+
// The plan is a flat [key, value][] applied in order, so the authored ordering
|
|
299
|
+
// between classes and ordinary props is unchanged.
|
|
300
|
+
//
|
|
301
|
+
// null = web-only candidate dropped on native. 'raw' = not claimed by the
|
|
302
|
+
// grammar, caller preserves the class string. An array (which may legitimately be
|
|
303
|
+
// empty) = claimed, apply these entries.
|
|
304
|
+
type TailwindPlanEntry = [key: string | null, value: any]
|
|
305
|
+
type TailwindClassPlan = TailwindPlanEntry[] | null | 'raw'
|
|
306
|
+
type TailwindParentPlan = {
|
|
307
|
+
entries: TailwindPlanEntry[]
|
|
308
|
+
preserveRawClass: boolean
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
type CachedTailwindClassPlan = TailwindClassPlan | TailwindParentPlan
|
|
312
|
+
|
|
313
|
+
const classPlanCache = new WeakMap<object, Map<string, CachedTailwindClassPlan>>()
|
|
314
|
+
|
|
315
|
+
function getClassPlanCache(grammarConfig: object) {
|
|
316
|
+
let cache = classPlanCache.get(grammarConfig)
|
|
317
|
+
if (!cache) {
|
|
318
|
+
cache = new Map()
|
|
319
|
+
classPlanCache.set(grammarConfig, cache)
|
|
320
|
+
}
|
|
321
|
+
return cache
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function createPlanEntry(
|
|
325
|
+
property: string,
|
|
326
|
+
value: unknown,
|
|
327
|
+
modifiers: readonly string[]
|
|
328
|
+
): TailwindPlanEntry | null {
|
|
329
|
+
if (modifiers.length === 0) return [property, value]
|
|
330
|
+
const payload = plainValueToPayload(value, property)
|
|
331
|
+
if (payload === null) return null
|
|
332
|
+
return [
|
|
333
|
+
null,
|
|
334
|
+
createFrontendProgram(property, {
|
|
335
|
+
base: null,
|
|
336
|
+
clauses: [{ modifiers, payload }],
|
|
337
|
+
}),
|
|
338
|
+
]
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function computeClassPlan(
|
|
342
|
+
cls: string,
|
|
343
|
+
grammarConfig: GrammarConfigView
|
|
344
|
+
): CachedTailwindClassPlan {
|
|
345
|
+
const groupMarker = /^group(?:\/([A-Za-z0-9_-]+))?$/.exec(cls)
|
|
346
|
+
if (groupMarker) {
|
|
347
|
+
return {
|
|
348
|
+
entries: [['group', groupMarker[1] || true]],
|
|
349
|
+
preserveRawClass: isWeb,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const containerMarker = /^@container(-size)?(?:\/([A-Za-z0-9_-]+))?$/.exec(cls)
|
|
354
|
+
if (containerMarker) {
|
|
355
|
+
const isSize = containerMarker[1] !== undefined
|
|
356
|
+
const name = containerMarker[2]
|
|
357
|
+
const entries: TailwindPlanEntry[] = []
|
|
358
|
+
if (name) entries.push(['containerName', name])
|
|
359
|
+
if (isSize) {
|
|
360
|
+
entries.push(['containerType', 'size'])
|
|
361
|
+
} else if (name) {
|
|
362
|
+
entries.push(['containerType', 'inline-size'])
|
|
363
|
+
} else {
|
|
364
|
+
entries.push(['container', true])
|
|
365
|
+
}
|
|
366
|
+
return { entries, preserveRawClass: isWeb }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const classification = classifyCandidate(cls, grammarConfig)
|
|
370
|
+
if (classification.kind === 'passthrough') {
|
|
371
|
+
return isWeb ? 'raw' : null
|
|
372
|
+
}
|
|
373
|
+
const parsed = classification.parsed
|
|
374
|
+
// named utilities first (flex-row, flex-1, hidden, …) — whole class → fixed prop(s).
|
|
375
|
+
// these may emit multiple props and may have no dash, so handle before the generic parse.
|
|
376
|
+
const util = parsed.kind === 'utility' ? parsed.properties : null
|
|
377
|
+
if (util) {
|
|
378
|
+
const entries: TailwindPlanEntry[] = []
|
|
379
|
+
for (const p in util) {
|
|
380
|
+
const entry = createPlanEntry(p, util[p], parsed.modifiers)
|
|
381
|
+
if (!entry) return 'raw'
|
|
382
|
+
entries.push(entry)
|
|
383
|
+
}
|
|
384
|
+
return entries
|
|
385
|
+
}
|
|
386
|
+
// Resolve only after the registry has claimed the candidate. Directional border/radius
|
|
387
|
+
// expansion below consumes that parsed decision instead of re-parsing width vs color.
|
|
388
|
+
const flatProp = tailwindClassToFlatProp(parsed)
|
|
389
|
+
if (flatProp) {
|
|
390
|
+
const expanded = expandBorderCandidate(parsed, flatProp.value)
|
|
391
|
+
if (expanded) {
|
|
392
|
+
const entries: TailwindPlanEntry[] = []
|
|
393
|
+
for (const p in expanded) {
|
|
394
|
+
const entry = createPlanEntry(p, expanded[p], parsed.modifiers)
|
|
395
|
+
if (!entry) return 'raw'
|
|
396
|
+
entries.push(entry)
|
|
397
|
+
}
|
|
398
|
+
return entries
|
|
399
|
+
}
|
|
400
|
+
const entry = createPlanEntry(flatProp.key, flatProp.value, parsed.modifiers)
|
|
401
|
+
return entry ? [entry] : 'raw'
|
|
402
|
+
}
|
|
403
|
+
// not claimed: caller preserves the raw class
|
|
404
|
+
return 'raw'
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const warnedNativePassthroughCandidates = new Set<string>()
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Append a contribution at the end of the forward pass.
|
|
411
|
+
*
|
|
412
|
+
* Plain re-assignment keeps a key's FIRST insertion position, so a restated
|
|
413
|
+
* shorthand would stay behind a longhand authored between the two occurrences:
|
|
414
|
+
* `p-4 px-2 p-6` has to resolve `paddingLeft` from `p-6`, and `pt-2 p-4 pt-8` has to
|
|
415
|
+
* resolve `paddingTop` from `pt-8`. Deleting the key first moves it to the end,
|
|
416
|
+
* which is the authored order the shared per-longhand merge reads.
|
|
417
|
+
*/
|
|
418
|
+
export function setInAuthoredOrder(
|
|
419
|
+
target: Record<string, any>,
|
|
420
|
+
key: string,
|
|
421
|
+
value: any
|
|
422
|
+
): void {
|
|
423
|
+
if (key in target) delete target[key]
|
|
424
|
+
target[key] = value
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Tokenize a className into ordinary props and internal value-program contributions,
|
|
429
|
+
* once per class per config.
|
|
430
|
+
* User-defined tokens drive resolution; Tailwind's color/spacing scales are never
|
|
431
|
+
* hardcoded. Classes the grammar does not claim stay in `className` verbatim, in
|
|
432
|
+
* author order, so official Tailwind CSS still applies them on web.
|
|
433
|
+
*/
|
|
434
|
+
export function preprocessTailwindClassName(
|
|
435
|
+
props: Record<string, any>,
|
|
436
|
+
config: StyleFrontendConfig,
|
|
437
|
+
preservePassthroughPosition = false
|
|
438
|
+
): Record<string, any> {
|
|
439
|
+
const className = props.className
|
|
440
|
+
if (!className || typeof className !== 'string') {
|
|
441
|
+
return props
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const classes = className.split(/\s+/).filter(Boolean)
|
|
445
|
+
const result: Record<string, any> = {}
|
|
446
|
+
const grammarConfig = getStyleGrammarConfig(config)
|
|
447
|
+
const plans = getClassPlanCache(grammarConfig)
|
|
448
|
+
const classPlans = classes.map((cls) => {
|
|
449
|
+
let plan = plans.get(cls)
|
|
450
|
+
if (plan === undefined) {
|
|
451
|
+
plan = computeClassPlan(cls, grammarConfig)
|
|
452
|
+
plans.set(cls, plan)
|
|
453
|
+
}
|
|
454
|
+
return plan
|
|
455
|
+
})
|
|
456
|
+
const hasOwnedStyleCandidate =
|
|
457
|
+
preservePassthroughPosition && classPlans.some((plan) => Array.isArray(plan))
|
|
458
|
+
let frontendProgramIndex = 0
|
|
459
|
+
let passthroughIndex = 0
|
|
460
|
+
const regularClasses: string[] = []
|
|
461
|
+
|
|
462
|
+
const preserveRawClass = (cls: string) => {
|
|
463
|
+
if (hasOwnedStyleCandidate) {
|
|
464
|
+
result[`${STYLE_FRONTEND_PASSTHROUGH_PREFIX}${passthroughIndex++}`] = cls
|
|
465
|
+
} else {
|
|
466
|
+
regularClasses.push(cls)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const applyEntry = ([key, value]: TailwindPlanEntry) => {
|
|
471
|
+
if (key === null) {
|
|
472
|
+
result[`__tamagui_frontend_program_${frontendProgramIndex++}`] = value
|
|
473
|
+
} else {
|
|
474
|
+
setInAuthoredOrder(result, key, value)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Expand the className exactly where it was authored. Claimed classes and
|
|
479
|
+
// ordinary props therefore share one forward pass: whichever contribution is
|
|
480
|
+
// encountered later wins. Classes within the string are likewise applied in
|
|
481
|
+
// their own left-to-right order.
|
|
482
|
+
for (const key in props) {
|
|
483
|
+
if (key !== 'className') {
|
|
484
|
+
setInAuthoredOrder(result, key, props[key])
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
for (let classIndex = 0; classIndex < classes.length; classIndex++) {
|
|
488
|
+
const cls = classes[classIndex]
|
|
489
|
+
const plan = classPlans[classIndex]
|
|
490
|
+
if (plan === null) {
|
|
491
|
+
// web-only candidate on native: dropped, warned once
|
|
492
|
+
if (
|
|
493
|
+
process.env.NODE_ENV !== 'production' &&
|
|
494
|
+
!warnedNativePassthroughCandidates.has(cls)
|
|
495
|
+
) {
|
|
496
|
+
warnedNativePassthroughCandidates.add(cls)
|
|
497
|
+
console.warn(
|
|
498
|
+
`[tamagui] Tailwind candidate "${cls}" is web-only and was dropped on native. Use a Tamagui grammar candidate or a native style prop for cross-platform output.`
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
if (plan === 'raw') {
|
|
504
|
+
// not claimed by the grammar: preserve the class as-is
|
|
505
|
+
preserveRawClass(cls)
|
|
506
|
+
continue
|
|
507
|
+
}
|
|
508
|
+
if (!Array.isArray(plan)) {
|
|
509
|
+
if (plan.preserveRawClass) {
|
|
510
|
+
preserveRawClass(cls)
|
|
511
|
+
}
|
|
512
|
+
for (let i = 0; i < plan.entries.length; i++) {
|
|
513
|
+
applyEntry(plan.entries[i])
|
|
514
|
+
}
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
517
|
+
for (let i = 0; i < plan.length; i++) {
|
|
518
|
+
applyEntry(plan[i])
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (regularClasses.length > 0) {
|
|
522
|
+
result.className = regularClasses.join(' ')
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return result
|
|
527
|
+
}
|
package/src/frontend.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
STYLE_FRONTEND_PREPROCESSED,
|
|
3
|
+
type FrontendStaticConfig,
|
|
4
|
+
type StyleFrontend,
|
|
5
|
+
type StyleFrontendConfig,
|
|
6
|
+
} from '@tamagui/core/internal-runtime'
|
|
7
|
+
import { preprocessTailwindClassName } from './candidate'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Tailwind frontend descriptor.
|
|
11
|
+
*
|
|
12
|
+
* `preprocessProps` is the single Tailwind pass: it tokenizes `className` once and
|
|
13
|
+
* emits ordinary props plus internal program contributions in authored order.
|
|
14
|
+
* Everything after this point — value programs,
|
|
15
|
+
* per-longhand forward merging, web lowering, native evaluation — is shared.
|
|
16
|
+
*
|
|
17
|
+
* Owned candidates are never string-merged: each contributes at its authored
|
|
18
|
+
* position and the shared resolver's last-contribution-wins rule decides. That is
|
|
19
|
+
* why `tailwind-merge` is not a dependency here.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse a static class string (a `styled()` base, a string variant value, a string
|
|
24
|
+
* compound-variant style) into ordinary style props.
|
|
25
|
+
*/
|
|
26
|
+
export function parseStaticStyle(
|
|
27
|
+
input: string,
|
|
28
|
+
config: StyleFrontendConfig
|
|
29
|
+
): Record<string, any> {
|
|
30
|
+
return preprocessTailwindClassName({ className: input }, config)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const normalizedStaticConfigCache = new WeakMap<
|
|
34
|
+
FrontendStaticConfig,
|
|
35
|
+
WeakMap<StyleFrontendConfig, FrontendStaticConfig>
|
|
36
|
+
>()
|
|
37
|
+
const normalizedStaticConfigs = new WeakSet<FrontendStaticConfig>()
|
|
38
|
+
|
|
39
|
+
function normalizeTailwindStaticConfig<Config extends FrontendStaticConfig>(
|
|
40
|
+
staticConfig: Config,
|
|
41
|
+
config: StyleFrontendConfig
|
|
42
|
+
): Config {
|
|
43
|
+
if (normalizedStaticConfigs.has(staticConfig)) {
|
|
44
|
+
return staticConfig
|
|
45
|
+
}
|
|
46
|
+
let configCache = normalizedStaticConfigCache.get(staticConfig)
|
|
47
|
+
const cached = configCache?.get(config)
|
|
48
|
+
if (cached) return cached as Config
|
|
49
|
+
|
|
50
|
+
let variants = staticConfig.variants
|
|
51
|
+
if (variants) {
|
|
52
|
+
variants = Object.fromEntries(
|
|
53
|
+
Object.entries(variants).map(([variantName, definition]) => [
|
|
54
|
+
variantName,
|
|
55
|
+
typeof definition === 'object' && definition
|
|
56
|
+
? Object.fromEntries(
|
|
57
|
+
Object.entries(definition).map(([matcher, value]) => [
|
|
58
|
+
matcher,
|
|
59
|
+
typeof value === 'string' ? parseStaticStyle(value, config) : value,
|
|
60
|
+
])
|
|
61
|
+
)
|
|
62
|
+
: definition,
|
|
63
|
+
])
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const compoundVariants = staticConfig.compoundVariants?.map((compoundVariant) => ({
|
|
68
|
+
...compoundVariant,
|
|
69
|
+
style:
|
|
70
|
+
typeof compoundVariant.style === 'string'
|
|
71
|
+
? parseStaticStyle(compoundVariant.style, config)
|
|
72
|
+
: compoundVariant.style,
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
// A class base is the one class string with no authored position, so its unclaimed
|
|
76
|
+
// classes cannot ride the forward pass the way a call-site className does. Partition
|
|
77
|
+
// them out: `baseStyle` holds styles only, and the raw remainder goes to
|
|
78
|
+
// `passthroughClassName` for the renderer to prepend.
|
|
79
|
+
let baseStyle = staticConfig.baseStyle
|
|
80
|
+
let passthroughClassName = staticConfig.passthroughClassName
|
|
81
|
+
if (staticConfig.baseClassName) {
|
|
82
|
+
const { className, ...styles } = parseStaticStyle(staticConfig.baseClassName, config)
|
|
83
|
+
baseStyle = styles
|
|
84
|
+
passthroughClassName = className
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const normalized = {
|
|
88
|
+
...staticConfig,
|
|
89
|
+
baseStyle,
|
|
90
|
+
passthroughClassName,
|
|
91
|
+
variants,
|
|
92
|
+
compoundVariants,
|
|
93
|
+
} as Config
|
|
94
|
+
normalizedStaticConfigs.add(normalized)
|
|
95
|
+
configCache ||= new WeakMap()
|
|
96
|
+
configCache.set(config, normalized)
|
|
97
|
+
normalizedStaticConfigCache.set(staticConfig, configCache)
|
|
98
|
+
return normalized
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const tailwindStyleFrontend: StyleFrontend = {
|
|
102
|
+
preprocessProps(props, config) {
|
|
103
|
+
const transformed =
|
|
104
|
+
typeof props.className === 'string'
|
|
105
|
+
? preprocessTailwindClassName(props, config, true)
|
|
106
|
+
: props
|
|
107
|
+
// only a fresh object gets the marker; marking the caller's props would leak the
|
|
108
|
+
// symbol onto props the frontend never actually rewrote
|
|
109
|
+
if (transformed !== props) {
|
|
110
|
+
;(transformed as any)[STYLE_FRONTEND_PREPROCESSED] = true
|
|
111
|
+
}
|
|
112
|
+
return transformed
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
normalizeStaticConfig: normalizeTailwindStaticConfig,
|
|
116
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind authoring for Tamagui.
|
|
3
|
+
*
|
|
4
|
+
* Components here render through the same runtime as `@tamagui/core` — same
|
|
5
|
+
* config, tokens, themes, media, events, refs, accessibility, animations, and
|
|
6
|
+
* normalized style output. Only the authoring syntax differs, and it is selected
|
|
7
|
+
* by which package a component was imported from, never by a global setting.
|
|
8
|
+
*
|
|
9
|
+
* This entry is runtime-only. The official Tailwind scanner and build integration
|
|
10
|
+
* live behind `@tamagui/tailwind/vite` and are never reachable from here.
|
|
11
|
+
*/
|
|
12
|
+
import { createFrontendViews } from '@tamagui/core/internal-runtime'
|
|
13
|
+
import { tailwindStyleFrontend } from './frontend'
|
|
14
|
+
import type { TailwindText, TailwindView } from './types'
|
|
15
|
+
|
|
16
|
+
const frontendViews = createFrontendViews(tailwindStyleFrontend)
|
|
17
|
+
|
|
18
|
+
export const View = frontendViews.View as unknown as TailwindView
|
|
19
|
+
export const Text = frontendViews.Text as unknown as TailwindText
|
|
20
|
+
|
|
21
|
+
export { styled } from './styled'
|
|
22
|
+
export { parseStaticStyle, tailwindStyleFrontend } from './frontend'
|
|
23
|
+
export { preprocessTailwindClassName } from './candidate'
|
|
24
|
+
export type * from './types'
|