rnwind 0.0.8 → 0.0.10
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/lib/cjs/core/parser/color.cjs +33 -1
- package/lib/cjs/core/parser/color.cjs.map +1 -1
- package/lib/cjs/core/parser/color.d.ts +10 -0
- package/lib/cjs/core/parser/declaration.cjs +121 -9
- package/lib/cjs/core/parser/declaration.cjs.map +1 -1
- package/lib/cjs/core/parser/gradient.cjs +46 -12
- package/lib/cjs/core/parser/gradient.cjs.map +1 -1
- package/lib/cjs/core/parser/gradient.d.ts +2 -1
- package/lib/cjs/core/parser/keyframes.cjs +27 -12
- package/lib/cjs/core/parser/keyframes.cjs.map +1 -1
- package/lib/cjs/core/parser/keyframes.d.ts +11 -0
- package/lib/cjs/core/parser/layout-dispatcher.cjs +33 -10
- package/lib/cjs/core/parser/layout-dispatcher.cjs.map +1 -1
- package/lib/cjs/core/parser/length.cjs +17 -1
- package/lib/cjs/core/parser/length.cjs.map +1 -1
- package/lib/cjs/core/parser/safe-area.cjs +24 -3
- package/lib/cjs/core/parser/safe-area.cjs.map +1 -1
- package/lib/cjs/core/parser/theme-vars.cjs +58 -8
- package/lib/cjs/core/parser/theme-vars.cjs.map +1 -1
- package/lib/cjs/core/parser/tokens.cjs +77 -9
- package/lib/cjs/core/parser/tokens.cjs.map +1 -1
- package/lib/cjs/core/parser/tokens.d.ts +9 -0
- package/lib/cjs/core/parser/transform.cjs +18 -9
- package/lib/cjs/core/parser/transform.cjs.map +1 -1
- package/lib/cjs/core/parser/tw-parser.cjs +136 -34
- package/lib/cjs/core/parser/tw-parser.cjs.map +1 -1
- package/lib/cjs/core/parser/tw-parser.d.ts +20 -0
- package/lib/cjs/core/parser/typography-dispatcher.cjs +19 -1
- package/lib/cjs/core/parser/typography-dispatcher.cjs.map +1 -1
- package/lib/cjs/core/parser/typography.cjs +15 -18
- package/lib/cjs/core/parser/typography.cjs.map +1 -1
- package/lib/cjs/core/parser/typography.d.ts +5 -5
- package/lib/cjs/core/style-builder/build-style.cjs +12 -3
- package/lib/cjs/core/style-builder/build-style.cjs.map +1 -1
- package/lib/cjs/core/style-builder/build-style.d.ts +3 -1
- package/lib/cjs/core/style-builder/union-builder.cjs +9 -11
- package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -1
- package/lib/cjs/core/style-builder/union-builder.d.ts +7 -8
- package/lib/cjs/metro/dts.cjs +6 -1
- package/lib/cjs/metro/dts.cjs.map +1 -1
- package/lib/cjs/metro/transformer.cjs +42 -77
- package/lib/cjs/metro/transformer.cjs.map +1 -1
- package/lib/cjs/metro/with-config.cjs +9 -29
- package/lib/cjs/metro/with-config.cjs.map +1 -1
- package/lib/cjs/runtime/hooks/use-scheme.cjs +17 -11
- package/lib/cjs/runtime/hooks/use-scheme.cjs.map +1 -1
- package/lib/cjs/runtime/hooks/use-scheme.d.ts +7 -4
- package/lib/cjs/runtime/index.cjs +2 -1
- package/lib/cjs/runtime/index.cjs.map +1 -1
- package/lib/cjs/runtime/index.d.ts +2 -2
- package/lib/cjs/runtime/lookup-css.cjs +41 -0
- package/lib/cjs/runtime/lookup-css.cjs.map +1 -1
- package/lib/cjs/runtime/lookup-css.d.ts +29 -0
- package/lib/cjs/runtime/resolve.cjs +8 -6
- package/lib/cjs/runtime/resolve.cjs.map +1 -1
- package/lib/cjs/runtime/wrap.cjs +50 -57
- package/lib/cjs/runtime/wrap.cjs.map +1 -1
- package/lib/cjs/runtime/wrap.d.ts +10 -4
- package/lib/cjs/testing/index.cjs +1 -1
- package/lib/cjs/testing/index.cjs.map +1 -1
- package/lib/esm/core/parser/color.d.ts +10 -0
- package/lib/esm/core/parser/color.mjs +34 -3
- package/lib/esm/core/parser/color.mjs.map +1 -1
- package/lib/esm/core/parser/declaration.mjs +122 -10
- package/lib/esm/core/parser/declaration.mjs.map +1 -1
- package/lib/esm/core/parser/gradient.d.ts +2 -1
- package/lib/esm/core/parser/gradient.mjs +45 -11
- package/lib/esm/core/parser/gradient.mjs.map +1 -1
- package/lib/esm/core/parser/keyframes.d.ts +11 -0
- package/lib/esm/core/parser/keyframes.mjs +27 -12
- package/lib/esm/core/parser/keyframes.mjs.map +1 -1
- package/lib/esm/core/parser/layout-dispatcher.mjs +33 -10
- package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -1
- package/lib/esm/core/parser/length.mjs +17 -1
- package/lib/esm/core/parser/length.mjs.map +1 -1
- package/lib/esm/core/parser/safe-area.mjs +24 -3
- package/lib/esm/core/parser/safe-area.mjs.map +1 -1
- package/lib/esm/core/parser/theme-vars.mjs +58 -8
- package/lib/esm/core/parser/theme-vars.mjs.map +1 -1
- package/lib/esm/core/parser/tokens.d.ts +9 -0
- package/lib/esm/core/parser/tokens.mjs +77 -10
- package/lib/esm/core/parser/tokens.mjs.map +1 -1
- package/lib/esm/core/parser/transform.mjs +18 -9
- package/lib/esm/core/parser/transform.mjs.map +1 -1
- package/lib/esm/core/parser/tw-parser.d.ts +20 -0
- package/lib/esm/core/parser/tw-parser.mjs +138 -36
- package/lib/esm/core/parser/tw-parser.mjs.map +1 -1
- package/lib/esm/core/parser/typography-dispatcher.mjs +19 -1
- package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -1
- package/lib/esm/core/parser/typography.d.ts +5 -5
- package/lib/esm/core/parser/typography.mjs +15 -18
- package/lib/esm/core/parser/typography.mjs.map +1 -1
- package/lib/esm/core/style-builder/build-style.d.ts +3 -1
- package/lib/esm/core/style-builder/build-style.mjs +12 -3
- package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
- package/lib/esm/core/style-builder/union-builder.d.ts +7 -8
- package/lib/esm/core/style-builder/union-builder.mjs +9 -11
- package/lib/esm/core/style-builder/union-builder.mjs.map +1 -1
- package/lib/esm/metro/dts.mjs +6 -1
- package/lib/esm/metro/dts.mjs.map +1 -1
- package/lib/esm/metro/transformer.mjs +42 -77
- package/lib/esm/metro/transformer.mjs.map +1 -1
- package/lib/esm/metro/with-config.mjs +10 -30
- package/lib/esm/metro/with-config.mjs.map +1 -1
- package/lib/esm/runtime/hooks/use-scheme.d.ts +7 -4
- package/lib/esm/runtime/hooks/use-scheme.mjs +17 -11
- package/lib/esm/runtime/hooks/use-scheme.mjs.map +1 -1
- package/lib/esm/runtime/index.d.ts +2 -2
- package/lib/esm/runtime/index.mjs +2 -2
- package/lib/esm/runtime/index.mjs.map +1 -1
- package/lib/esm/runtime/lookup-css.d.ts +29 -0
- package/lib/esm/runtime/lookup-css.mjs +39 -1
- package/lib/esm/runtime/lookup-css.mjs.map +1 -1
- package/lib/esm/runtime/resolve.mjs +9 -7
- package/lib/esm/runtime/resolve.mjs.map +1 -1
- package/lib/esm/runtime/wrap.d.ts +10 -4
- package/lib/esm/runtime/wrap.mjs +50 -57
- package/lib/esm/runtime/wrap.mjs.map +1 -1
- package/lib/esm/testing/index.mjs +2 -2
- package/lib/esm/testing/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/parser/color.ts +32 -1
- package/src/core/parser/declaration.ts +119 -10
- package/src/core/parser/gradient.ts +48 -11
- package/src/core/parser/keyframes.ts +31 -3
- package/src/core/parser/layout-dispatcher.ts +32 -9
- package/src/core/parser/length.ts +18 -1
- package/src/core/parser/safe-area.ts +23 -2
- package/src/core/parser/theme-vars.ts +75 -8
- package/src/core/parser/tokens.ts +76 -9
- package/src/core/parser/transform.ts +19 -8
- package/src/core/parser/tw-parser.ts +148 -31
- package/src/core/parser/typography-dispatcher.ts +20 -1
- package/src/core/parser/typography.ts +15 -15
- package/src/core/style-builder/build-style.ts +12 -1
- package/src/core/style-builder/union-builder.ts +10 -12
- package/src/metro/dts.ts +6 -1
- package/src/metro/transformer.ts +42 -78
- package/src/metro/with-config.ts +10 -29
- package/src/runtime/hooks/use-scheme.ts +17 -10
- package/src/runtime/index.ts +2 -1
- package/src/runtime/lookup-css.ts +42 -0
- package/src/runtime/resolve.ts +9 -7
- package/src/runtime/wrap.tsx +57 -61
- package/src/testing/index.ts +3 -0
|
@@ -321,28 +321,47 @@ const COLOR_MIX_PCT_TAIL = /(-?\d+(?:\.\d+)?)%$/
|
|
|
321
321
|
function applyAlphaToCssColor(color: string, multiplier: number): string | null {
|
|
322
322
|
const trimmed = color.trim()
|
|
323
323
|
if (trimmed === 'transparent') return 'rgba(0, 0, 0, 0)'
|
|
324
|
+
// Nested color-mix — Tailwind's `shadow-<token>/<opacity>` emits
|
|
325
|
+
// `color-mix(… color-mix(… <token> N%, transparent) <alpha>, transparent)`.
|
|
326
|
+
// Resolve the inner mix to a concrete color first, then apply this alpha.
|
|
327
|
+
if (trimmed.toLowerCase().startsWith('color-mix(')) {
|
|
328
|
+
const inner = evaluateColorMixWithTransparent(trimmed)
|
|
329
|
+
if (inner !== null) return applyAlphaToCssColor(inner, multiplier)
|
|
330
|
+
}
|
|
324
331
|
return alphaFromHex(trimmed, multiplier) ?? alphaFromRgbFunction(trimmed, multiplier) ?? alphaFromCulori(trimmed, multiplier)
|
|
325
332
|
}
|
|
326
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Round a composed alpha to 4 decimals — `0.2 * 1` round-trips through f32 as
|
|
336
|
+
* `0.20000000298…`; the rounded form keeps generated rgba strings compact.
|
|
337
|
+
* @param alpha Raw alpha product.
|
|
338
|
+
* @returns Rounded alpha.
|
|
339
|
+
*/
|
|
340
|
+
function roundAlpha(alpha: number): number {
|
|
341
|
+
return Math.round(alpha * 10_000) / 10_000
|
|
342
|
+
}
|
|
343
|
+
|
|
327
344
|
/**
|
|
328
345
|
* Apply the alpha multiplier to a hex literal, expanding 3/4/6/8-digit forms.
|
|
329
|
-
* @param text
|
|
330
|
-
* @param multiplier
|
|
346
|
+
* @param text Candidate hex color string.
|
|
347
|
+
* @param multiplier Alpha multiplier (0…1).
|
|
348
|
+
* @returns `rgba(…)` string, or null when `text` is not a hex literal.
|
|
331
349
|
*/
|
|
332
350
|
function alphaFromHex(text: string, multiplier: number): string | null {
|
|
333
351
|
const hexMatch = /^#([0-9a-fA-F]{3,8})$/.exec(text)
|
|
334
352
|
if (!hexMatch) return null
|
|
335
353
|
const expanded = expandHex(hexMatch[1]!)
|
|
336
354
|
if (!expanded) return null
|
|
337
|
-
return `rgba(${expanded.r}, ${expanded.g}, ${expanded.b}, ${expanded.alpha * multiplier})`
|
|
355
|
+
return `rgba(${expanded.r}, ${expanded.g}, ${expanded.b}, ${roundAlpha(expanded.alpha * multiplier)})`
|
|
338
356
|
}
|
|
339
357
|
|
|
340
358
|
/**
|
|
341
359
|
* Apply alpha to an `rgb(…)` / `rgba(…)` literal. Walks the channels by
|
|
342
360
|
* hand instead of a multi-capture regex (the linter flags the regex
|
|
343
361
|
* form as backtracking-prone).
|
|
344
|
-
* @param text
|
|
345
|
-
* @param multiplier
|
|
362
|
+
* @param text Candidate `rgb(…)` / `rgba(…)` color string.
|
|
363
|
+
* @param multiplier Alpha multiplier (0…1).
|
|
364
|
+
* @returns `rgba(…)` string, or null when `text` is not an rgb function.
|
|
346
365
|
*/
|
|
347
366
|
function alphaFromRgbFunction(text: string, multiplier: number): string | null {
|
|
348
367
|
if (!text.startsWith('rgb(') && !text.startsWith('rgba(')) return null
|
|
@@ -354,7 +373,7 @@ function alphaFromRgbFunction(text: string, multiplier: number): string | null {
|
|
|
354
373
|
const b = Math.round(Number(channels[2]))
|
|
355
374
|
const baseAlpha = channels.length === 4 ? Number(channels[3]) : 1
|
|
356
375
|
if (![r, g, b, baseAlpha].every((value) => Number.isFinite(value))) return null
|
|
357
|
-
return `rgba(${r}, ${g}, ${b}, ${baseAlpha * multiplier})`
|
|
376
|
+
return `rgba(${r}, ${g}, ${b}, ${roundAlpha(baseAlpha * multiplier)})`
|
|
358
377
|
}
|
|
359
378
|
|
|
360
379
|
/**
|
|
@@ -363,8 +382,9 @@ function alphaFromRgbFunction(text: string, multiplier: number): string | null {
|
|
|
363
382
|
* `color-mix(in oklab, oklch(...) 50%, transparent)` resolve when
|
|
364
383
|
* Tailwind emits the source color in a wide-gamut space (every
|
|
365
384
|
* built-in `bg-red-500` / `shadow-red-500` does, in v4).
|
|
366
|
-
* @param text
|
|
367
|
-
* @param multiplier
|
|
385
|
+
* @param text Candidate wide-gamut / named CSS color string.
|
|
386
|
+
* @param multiplier Alpha multiplier (0…1).
|
|
387
|
+
* @returns `rgba(…)` string, or null when culori can't parse `text`.
|
|
368
388
|
*/
|
|
369
389
|
function alphaFromCulori(text: string, multiplier: number): string | null {
|
|
370
390
|
try {
|
|
@@ -375,7 +395,7 @@ function alphaFromCulori(text: string, multiplier: number): string | null {
|
|
|
375
395
|
const g = Math.round(Math.max(0, Math.min(1, parsed.g!)) * 255)
|
|
376
396
|
const b = Math.round(Math.max(0, Math.min(1, parsed.b!)) * 255)
|
|
377
397
|
const baseAlpha = typeof parsed.alpha === 'number' ? parsed.alpha : 1
|
|
378
|
-
return `rgba(${r}, ${g}, ${b}, ${baseAlpha * multiplier})`
|
|
398
|
+
return `rgba(${r}, ${g}, ${b}, ${roundAlpha(baseAlpha * multiplier)})`
|
|
379
399
|
} catch {
|
|
380
400
|
// culori threw on an unrecognised CSS form — fall through.
|
|
381
401
|
return null
|
|
@@ -450,6 +470,53 @@ export function coerceFontFamily(value: string): string {
|
|
|
450
470
|
return unquoteCssString(first)
|
|
451
471
|
}
|
|
452
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Generic CSS font-family keywords — NOT real React Native typefaces. A
|
|
475
|
+
* `font-family` stack made only of these (e.g. the default `font-sans`:
|
|
476
|
+
* `ui-sans-serif, system-ui, sans-serif`) should fall back to RN's system
|
|
477
|
+
* font rather than emit a bogus `fontFamily`.
|
|
478
|
+
*/
|
|
479
|
+
const GENERIC_FONT_FAMILIES: ReadonlySet<string> = new Set([
|
|
480
|
+
'ui-sans-serif',
|
|
481
|
+
'ui-serif',
|
|
482
|
+
'ui-monospace',
|
|
483
|
+
'ui-rounded',
|
|
484
|
+
'system-ui',
|
|
485
|
+
'sans-serif',
|
|
486
|
+
'serif',
|
|
487
|
+
'monospace',
|
|
488
|
+
'cursive',
|
|
489
|
+
'fantasy',
|
|
490
|
+
'math',
|
|
491
|
+
'emoji',
|
|
492
|
+
'fangsong',
|
|
493
|
+
'-apple-system',
|
|
494
|
+
'blinkmacsystemfont',
|
|
495
|
+
// Emoji / symbol fonts that Tailwind appends to the default sans stack —
|
|
496
|
+
// never the intended text typeface.
|
|
497
|
+
'apple color emoji',
|
|
498
|
+
'segoe ui emoji',
|
|
499
|
+
'segoe ui symbol',
|
|
500
|
+
'noto color emoji',
|
|
501
|
+
])
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Pick the first CONCRETE typeface from a typed `font-family` LIST (a CSS
|
|
505
|
+
* fallback stack). RN takes one family, and generic keywords aren't real
|
|
506
|
+
* faces, so skip them. Returns undefined when the whole stack is generic
|
|
507
|
+
* (→ caller emits nothing → system font).
|
|
508
|
+
* @param families Typed `font-family` value — an array of family-name strings.
|
|
509
|
+
* @returns First concrete family name, or undefined.
|
|
510
|
+
*/
|
|
511
|
+
export function firstConcreteFontFamily(families: readonly unknown[]): string | undefined {
|
|
512
|
+
for (const entry of families) {
|
|
513
|
+
if (typeof entry !== 'string') continue
|
|
514
|
+
const bare = coerceFontFamily(entry)
|
|
515
|
+
if (bare.length > 0 && !GENERIC_FONT_FAMILIES.has(bare.toLowerCase())) return bare
|
|
516
|
+
}
|
|
517
|
+
return undefined
|
|
518
|
+
}
|
|
519
|
+
|
|
453
520
|
/**
|
|
454
521
|
* Substitute every `var(--name [, fallback])` reference in `text` with
|
|
455
522
|
* the value from `table` (or the fallback clause when the name misses).
|
|
@@ -103,37 +103,48 @@ function angleToString(angle: Angle): string {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Convert a `NumberOrPercentage` to a plain number. Percentages become
|
|
106
|
-
* their fractional equivalent (e.g. `50%` → `0.5`).
|
|
106
|
+
* their fractional equivalent (e.g. `50%` → `0.5`). Rounded so a literal
|
|
107
|
+
* like `scale-[1.7]` doesn't carry lightningcss's f32 noise
|
|
108
|
+
* (`1.7000000476837158`) into the RN `transform` array.
|
|
107
109
|
* @param value Typed value.
|
|
108
110
|
* @returns Plain number.
|
|
109
111
|
*/
|
|
110
112
|
function numberOrPercentageToNumber(value: NumberOrPercentage): number {
|
|
111
|
-
|
|
112
|
-
return value.value
|
|
113
|
+
return roundNumber(value.value)
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
116
117
|
* Convert a length-or-percentage used by translate into the shape RN
|
|
117
118
|
* accepts (`number` for px, `string` for `%`). Percentages stay as
|
|
118
|
-
* strings so RN layout can resolve them against the element size.
|
|
119
|
+
* strings so RN layout can resolve them against the element size. Pixel
|
|
120
|
+
* values are rounded to shed f32 noise (`3.3px` → `3.299999952…`).
|
|
119
121
|
* @param value Typed length or percentage.
|
|
120
122
|
* @returns RN-style translate value.
|
|
121
123
|
*/
|
|
122
124
|
function lengthOrPercentToNumber(value: DimensionPercent | { type: 'value'; value: LengthValue }): number | string {
|
|
123
|
-
if (value.type === 'dimension') return lengthToPx(value.value)
|
|
124
|
-
if (value.type === 'value') return lengthToPx(value.value)
|
|
125
|
+
if (value.type === 'dimension') return roundNumber(lengthToPx(value.value))
|
|
126
|
+
if (value.type === 'value') return roundNumber(lengthToPx(value.value))
|
|
125
127
|
if (value.type === 'percentage') return `${formatNumber(value.value * 100)}%`
|
|
126
128
|
return 0
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Round a number to 4 decimals — sheds lightningcss's f32 representation
|
|
133
|
+
* noise while staying well below subpixel / sub-percent precision.
|
|
134
|
+
* @param value Raw number.
|
|
135
|
+
* @returns Rounded number.
|
|
136
|
+
*/
|
|
137
|
+
function roundNumber(value: number): number {
|
|
138
|
+
return Math.round(value * 10_000) / 10_000
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
/**
|
|
130
142
|
* Render a number without trailing IEEE noise.
|
|
131
143
|
* @param value Number to format.
|
|
132
144
|
* @returns Compact string form.
|
|
133
145
|
*/
|
|
134
146
|
function formatNumber(value: number): string {
|
|
135
|
-
|
|
136
|
-
return String(rounded)
|
|
147
|
+
return String(roundNumber(value))
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
/**
|
|
@@ -5,7 +5,7 @@ import { Features, transform, type TransformOptions } from 'lightningcss'
|
|
|
5
5
|
import { declarationToRnEntries } from './declaration'
|
|
6
6
|
import { detectGradientAtom, type GradientAtomInfo } from './gradient'
|
|
7
7
|
import { detectHapticAtom, type HapticRequest } from './haptics'
|
|
8
|
-
import {
|
|
8
|
+
import { keyframeSelectorOffsets, keyframesName, pickAnimationName } from './keyframes'
|
|
9
9
|
import { serializeInitialValue } from './property'
|
|
10
10
|
import { classNameFromSelector } from './selector'
|
|
11
11
|
import {
|
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
extractThemeVars,
|
|
17
17
|
type ThemeSchemeTable,
|
|
18
18
|
} from './theme-vars'
|
|
19
|
-
import { serializeTokens } from './tokens'
|
|
19
|
+
import { coerceUnparsedValue, serializeTokens, substituteThemeVars } from './tokens'
|
|
20
|
+
import { normalizeColorString } from './color'
|
|
20
21
|
import type { RNStyle } from './types'
|
|
22
|
+
import type { ThemeTable, ThemeTables } from '../types'
|
|
21
23
|
import type { Declaration as LcDeclaration, TokenOrValue } from 'lightningcss'
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -152,6 +154,14 @@ export interface ParsedOutput {
|
|
|
152
154
|
* provider's `windowWidth`.
|
|
153
155
|
*/
|
|
154
156
|
breakpoints: ReadonlyMap<string, number>
|
|
157
|
+
/**
|
|
158
|
+
* Per-scheme user theme token tables (`--color-*`, `--spacing-*`, …) with
|
|
159
|
+
* `--color-*` values lowered to sRGB. The style-builder emits these as
|
|
160
|
+
* `registerThemeTokens({...})` in the manifest so `useColor` / `useToken` /
|
|
161
|
+
* `useSize` resolve out of the box, without the user threading a `tables`
|
|
162
|
+
* prop on the provider. Keyed by scheme (`base` + each declared variant).
|
|
163
|
+
*/
|
|
164
|
+
themeTokens: ThemeTables
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
/**
|
|
@@ -259,6 +269,33 @@ export class TailwindParser {
|
|
|
259
269
|
return merged
|
|
260
270
|
}
|
|
261
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Build the per-scheme theme token tables for `registerThemeTokens` —
|
|
274
|
+
* the data source for `useColor` / `useToken` / `useSize`. Emits one table
|
|
275
|
+
* per scheme the user wrote tokens under (`base` + each variant block /
|
|
276
|
+
* `.dark` override). `--color-*` values are lowered to sRGB (matching the
|
|
277
|
+
* className path) using `resolver` so a wide-gamut or `var()`-referencing
|
|
278
|
+
* token resolves to an RN-renderable string; other tokens pass through.
|
|
279
|
+
* @param resolver Full compiled `:root` table, for resolving `var()` refs.
|
|
280
|
+
* @returns Scheme → (token name → value) map.
|
|
281
|
+
*/
|
|
282
|
+
private buildThemeTokens(resolver: ReadonlyMap<string, string>): ThemeTables {
|
|
283
|
+
const out: ThemeTables = {}
|
|
284
|
+
for (const scheme of this.themeSchemes.keys()) {
|
|
285
|
+
const userTable = this.themeSchemes.get(scheme)
|
|
286
|
+
if (!userTable || userTable.size === 0) continue
|
|
287
|
+
|
|
288
|
+
const schemeResolver = new Map(resolver)
|
|
289
|
+
for (const [k, v] of this.effectiveVars(scheme)) schemeResolver.set(k, v)
|
|
290
|
+
const table: ThemeTable = {}
|
|
291
|
+
for (const [name, raw] of userTable) {
|
|
292
|
+
table[name] = name.startsWith('--color-') ? lowerColorToken(raw, schemeResolver) : raw
|
|
293
|
+
}
|
|
294
|
+
out[scheme] = table
|
|
295
|
+
}
|
|
296
|
+
return out
|
|
297
|
+
}
|
|
298
|
+
|
|
262
299
|
/**
|
|
263
300
|
* Build the Tailwind compiler on first use and cache it. The theme CSS
|
|
264
301
|
* gets a `theme(inline)` modifier on its `@import 'tailwindcss'` so
|
|
@@ -398,7 +435,8 @@ export class TailwindParser {
|
|
|
398
435
|
// surface their role + resolved colour so the transformer
|
|
399
436
|
// can rewrite `<LinearGradient className="...">` into
|
|
400
437
|
// `colors={...}` / `start={...}` / `end={...}` props.
|
|
401
|
-
const
|
|
438
|
+
const gradientTable = schemeTables.get(BASE_SCHEME) ?? schemeTables.get(schemes[0] ?? BASE_SCHEME)
|
|
439
|
+
const gradient = detectGradientAtom(rule.value.declarations.declarations, gradientTable)
|
|
402
440
|
if (gradient) gradientAtoms.set(className, gradient)
|
|
403
441
|
// Haptics may live on the rule directly OR inside a
|
|
404
442
|
// nested pseudo (e.g. `&:active` for `active:haptic-*`).
|
|
@@ -415,14 +453,16 @@ export class TailwindParser {
|
|
|
415
453
|
const steps: KeyframeStep[] = []
|
|
416
454
|
const baseTable = schemeTables.get(BASE_SCHEME) ?? schemeTables.get(schemes[0] ?? BASE_SCHEME)
|
|
417
455
|
for (const frame of rule.value.keyframes) {
|
|
418
|
-
const
|
|
419
|
-
if (
|
|
456
|
+
const offsets = keyframeSelectorOffsets(frame.selectors)
|
|
457
|
+
if (offsets.length === 0) continue
|
|
420
458
|
const style: RNStyle = {}
|
|
421
459
|
const frameDecls = frame.declarations.declarations ?? []
|
|
422
460
|
for (const decl of frameDecls) {
|
|
423
461
|
for (const [key, value] of declarationToRnEntries(decl, baseTable)) style[key] = value
|
|
424
462
|
}
|
|
425
|
-
|
|
463
|
+
// One frame can carry several offsets (`0%, 100% { … }`); emit a
|
|
464
|
+
// step for each so the terminal frame isn't lost.
|
|
465
|
+
for (const offset of offsets) steps.push({ offset, style })
|
|
426
466
|
}
|
|
427
467
|
keyframes.set(name, { name, steps })
|
|
428
468
|
},
|
|
@@ -442,10 +482,25 @@ export class TailwindParser {
|
|
|
442
482
|
if (!referencedKeyframes.has(name)) keyframes.delete(name)
|
|
443
483
|
}
|
|
444
484
|
|
|
445
|
-
|
|
485
|
+
const themeTokens = this.buildThemeTokens(compiledTheme)
|
|
486
|
+
return { atoms, keyframes, propertyDefaults, gradientAtoms, hapticAtoms, candidates: [...candidates], schemes, breakpoints, themeTokens }
|
|
446
487
|
}
|
|
447
488
|
}
|
|
448
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Lower a `--color-*` token value to an RN-renderable sRGB string, matching
|
|
492
|
+
* the className path: resolve any `var()` ref via `resolver`, then lower a
|
|
493
|
+
* wide-gamut form (`oklch(…)`, `lab(…)`, `color(p3 …)`) to sRGB. Hex / rgb /
|
|
494
|
+
* named colors pass through unchanged.
|
|
495
|
+
* @param raw Raw token value.
|
|
496
|
+
* @param resolver Var name → value table for resolving `var()` references.
|
|
497
|
+
* @returns RN-safe color string.
|
|
498
|
+
*/
|
|
499
|
+
function lowerColorToken(raw: string, resolver: ReadonlyMap<string, string>): string {
|
|
500
|
+
const substituted = substituteThemeVars(raw, resolver)
|
|
501
|
+
return normalizeColorString(substituted) ?? substituted
|
|
502
|
+
}
|
|
503
|
+
|
|
449
504
|
/**
|
|
450
505
|
* Wrap an error from `@tailwindcss/node`'s compiler or `lightningcss`'s
|
|
451
506
|
* transform with a `rnwind:` prefix so the user sees a clear "this came
|
|
@@ -493,6 +548,7 @@ function emptyOutput(): ParsedOutput {
|
|
|
493
548
|
candidates: [],
|
|
494
549
|
schemes: [BASE_SCHEME],
|
|
495
550
|
breakpoints: new Map(),
|
|
551
|
+
themeTokens: {},
|
|
496
552
|
}
|
|
497
553
|
}
|
|
498
554
|
|
|
@@ -582,8 +638,8 @@ function processStyleRule(
|
|
|
582
638
|
if (animationRef) ctx.referencedKeyframes.add(animationRef)
|
|
583
639
|
}
|
|
584
640
|
applyComposedTransform(bucket, ctx.schemes, ruleLocalVars)
|
|
585
|
-
applyComposedShadow(bucket, ctx.schemes, ruleLocalVars)
|
|
586
|
-
applyComposedRing(bucket, ctx.schemes, ruleLocalVars)
|
|
641
|
+
applyComposedShadow(bucket, ctx.schemes, ruleLocalVars, ruleSchemeTables)
|
|
642
|
+
applyComposedRing(bucket, ctx.schemes, ruleLocalVars, ruleSchemeTables)
|
|
587
643
|
// Phase 2: nested rules — three orthogonal flavours, dispatched on
|
|
588
644
|
// the lightningcss node `type`:
|
|
589
645
|
// - `media`: Tailwind v4 responsive variants (`sm:`, `md:`, …) wrap
|
|
@@ -729,7 +785,7 @@ function applyMediaRule(
|
|
|
729
785
|
const nestedLocalVars = new Map(ruleLocalVars)
|
|
730
786
|
for (const [k, v] of collectRuleLocalVars(decls)) nestedLocalVars.set(k, v)
|
|
731
787
|
applyComposedTransformToScheme(schemeBucket, nestedLocalVars)
|
|
732
|
-
applyComposedShadowToScheme(schemeBucket, nestedLocalVars)
|
|
788
|
+
applyComposedShadowToScheme(schemeBucket, nestedLocalVars, table)
|
|
733
789
|
bucket[scheme] = schemeBucket
|
|
734
790
|
}
|
|
735
791
|
}
|
|
@@ -769,7 +825,7 @@ function applyInteractiveNestedRule(
|
|
|
769
825
|
const nestedLocalVars = new Map(ruleLocalVars)
|
|
770
826
|
for (const [k, v] of collectRuleLocalVars(decls)) nestedLocalVars.set(k, v)
|
|
771
827
|
applyComposedTransformToScheme(schemeBucket, nestedLocalVars)
|
|
772
|
-
applyComposedShadowToScheme(schemeBucket, nestedLocalVars)
|
|
828
|
+
applyComposedShadowToScheme(schemeBucket, nestedLocalVars, table)
|
|
773
829
|
bucket[scheme] = schemeBucket
|
|
774
830
|
}
|
|
775
831
|
}
|
|
@@ -832,7 +888,7 @@ function applyNestedSchemeRule(
|
|
|
832
888
|
const nestedLocalVars = new Map(ruleLocalVars)
|
|
833
889
|
for (const [k, v] of collectRuleLocalVars(innerDecls)) nestedLocalVars.set(k, v)
|
|
834
890
|
applyComposedTransformToScheme(schemeBucket, nestedLocalVars)
|
|
835
|
-
applyComposedShadowToScheme(schemeBucket, nestedLocalVars)
|
|
891
|
+
applyComposedShadowToScheme(schemeBucket, nestedLocalVars, table)
|
|
836
892
|
bucket[targetScheme] = schemeBucket
|
|
837
893
|
}
|
|
838
894
|
|
|
@@ -948,12 +1004,17 @@ function applyComposedTransformToScheme(style: RNStyle, ruleLocalVars: ReadonlyM
|
|
|
948
1004
|
* prop.
|
|
949
1005
|
* @param style Scheme-specific style map.
|
|
950
1006
|
* @param ruleLocalVars Combined outer+nested `--tw-*` vars.
|
|
1007
|
+
* @param table Per-scheme var table for resolving `var(--color-x)` in colors.
|
|
951
1008
|
*/
|
|
952
|
-
function applyComposedShadowToScheme(
|
|
1009
|
+
function applyComposedShadowToScheme(
|
|
1010
|
+
style: RNStyle,
|
|
1011
|
+
ruleLocalVars: ReadonlyMap<string, string>,
|
|
1012
|
+
table?: ReadonlyMap<string, string>,
|
|
1013
|
+
): void {
|
|
953
1014
|
const rawShadow = ruleLocalVars.get('--tw-shadow')
|
|
954
1015
|
const rawShadowColor = ruleLocalVars.get('--tw-shadow-color')
|
|
955
1016
|
if (!rawShadow && rawShadowColor) {
|
|
956
|
-
const color = resolveCustomColorString(rawShadowColor)
|
|
1017
|
+
const color = resolveCustomColorString(rawShadowColor, table)
|
|
957
1018
|
if (!color) return
|
|
958
1019
|
delete style.boxShadow
|
|
959
1020
|
style.shadowColor = color
|
|
@@ -980,11 +1041,13 @@ function applyComposedShadowToScheme(style: RNStyle, ruleLocalVars: ReadonlyMap<
|
|
|
980
1041
|
* @param bucket Per-scheme style map for the atom.
|
|
981
1042
|
* @param schemes Scheme names active for this parse.
|
|
982
1043
|
* @param ruleLocalVars Rule-local `--tw-*` vars.
|
|
1044
|
+
* @param schemeTables Per-scheme var tables for resolving `var(--color-x)`.
|
|
983
1045
|
*/
|
|
984
1046
|
function applyComposedShadow(
|
|
985
1047
|
bucket: Record<string, RNStyle>,
|
|
986
1048
|
schemes: readonly string[],
|
|
987
1049
|
ruleLocalVars: ReadonlyMap<string, string>,
|
|
1050
|
+
schemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>,
|
|
988
1051
|
): void {
|
|
989
1052
|
const rawShadow = ruleLocalVars.get('--tw-shadow')
|
|
990
1053
|
const rawShadowColor = ruleLocalVars.get('--tw-shadow-color')
|
|
@@ -994,9 +1057,10 @@ function applyComposedShadow(
|
|
|
994
1057
|
// where setting `--tw-shadow-color` swaps in a solid color). Offset /
|
|
995
1058
|
// blur / elevation come from the partner size utility's atom.
|
|
996
1059
|
if (!rawShadow && rawShadowColor) {
|
|
997
|
-
const color = resolveCustomColorString(rawShadowColor)
|
|
998
|
-
if (!color) return
|
|
999
1060
|
for (const scheme of schemes) {
|
|
1061
|
+
// Resolve per scheme — a custom token may differ between light/dark.
|
|
1062
|
+
const color = resolveCustomColorString(rawShadowColor, schemeTables.get(scheme))
|
|
1063
|
+
if (!color) continue
|
|
1000
1064
|
const style = bucket[scheme] ?? {}
|
|
1001
1065
|
delete style.boxShadow
|
|
1002
1066
|
style.shadowColor = color
|
|
@@ -1028,35 +1092,64 @@ function applyComposedShadow(
|
|
|
1028
1092
|
* @param bucket Per-scheme style map for the atom.
|
|
1029
1093
|
* @param schemes Scheme names active for this parse.
|
|
1030
1094
|
* @param ruleLocalVars Rule-local `--tw-*` vars.
|
|
1095
|
+
* @param schemeTables Per-scheme var tables for resolving `var(--color-x)`.
|
|
1031
1096
|
*/
|
|
1032
1097
|
function applyComposedRing(
|
|
1033
1098
|
bucket: Record<string, RNStyle>,
|
|
1034
1099
|
schemes: readonly string[],
|
|
1035
1100
|
ruleLocalVars: ReadonlyMap<string, string>,
|
|
1101
|
+
schemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>,
|
|
1036
1102
|
): void {
|
|
1037
1103
|
const ringColor = ruleLocalVars.get('--tw-ring-color')
|
|
1038
1104
|
if (!ringColor) return
|
|
1039
|
-
const color = resolveCustomColorString(ringColor)
|
|
1040
|
-
if (!color) return
|
|
1041
1105
|
for (const scheme of schemes) {
|
|
1106
|
+
// Resolve per scheme — a custom token may differ between light/dark.
|
|
1107
|
+
const color = resolveCustomColorString(ringColor, schemeTables.get(scheme))
|
|
1108
|
+
if (!color) continue
|
|
1042
1109
|
const style = bucket[scheme] ?? {}
|
|
1043
1110
|
if (!('borderColor' in style)) style.borderColor = color
|
|
1044
1111
|
bucket[scheme] = style
|
|
1045
1112
|
}
|
|
1046
1113
|
}
|
|
1047
1114
|
|
|
1115
|
+
/**
|
|
1116
|
+
* Tailwind composable shadow/inset-shadow alpha defaults. Their `100%` lives
|
|
1117
|
+
* in an `@property` initial-value (not the rule's local vars), so after the
|
|
1118
|
+
* `@supports` color-mix is unwrapped, `var(--tw-shadow-alpha)` is left dangling
|
|
1119
|
+
* and the shadow color fails to resolve. Seed the default; a `/<opacity>`
|
|
1120
|
+
* modifier still wins because the in-rule table value overrides it.
|
|
1121
|
+
*/
|
|
1122
|
+
const COMPOSABLE_ALPHA_DEFAULTS: ReadonlyMap<string, string> = new Map([
|
|
1123
|
+
['--tw-shadow-alpha', '100%'],
|
|
1124
|
+
['--tw-inset-shadow-alpha', '100%'],
|
|
1125
|
+
])
|
|
1126
|
+
|
|
1048
1127
|
/**
|
|
1049
1128
|
* Resolve a CSS color string (`oklch(0.971 0.013 17.38)`, `#ff0000`,
|
|
1050
1129
|
* `rgb(0 0 0 / 0.1)`) to the hex string RN's `shadowColor` accepts.
|
|
1051
1130
|
* Wraps culori's parser via {@link parseCssColorToHex}.
|
|
1052
|
-
*
|
|
1131
|
+
*
|
|
1132
|
+
* Custom `@theme` color tokens arrive as `var(--color-x)` (only the default
|
|
1133
|
+
* palette is `theme(inline)`-d), so `table` is substituted FIRST — without it
|
|
1134
|
+
* `shadow-<token>` / `ring-<token>` silently drop the color (culori can't
|
|
1135
|
+
* parse a bare `var()`). The table is per-scheme so a token that differs
|
|
1136
|
+
* between light/dark resolves to the right value for each.
|
|
1137
|
+
* @param raw Raw color text from a `--tw-shadow-color` / `--tw-ring-color` prop.
|
|
1138
|
+
* @param table Per-scheme var table for resolving `var(--color-x)` references.
|
|
1053
1139
|
* @returns `#rrggbb` string, or null when culori can't parse it.
|
|
1054
1140
|
*/
|
|
1055
|
-
function resolveCustomColorString(raw: string): string | null {
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1141
|
+
function resolveCustomColorString(raw: string, table?: ReadonlyMap<string, string>): string | null {
|
|
1142
|
+
const seeded = new Map([...COMPOSABLE_ALPHA_DEFAULTS, ...(table ?? [])])
|
|
1143
|
+
const substituted = substituteThemeVars(raw, seeded)
|
|
1144
|
+
// `coerceUnparsedValue` collapses Tailwind's opacity shape
|
|
1145
|
+
// `color-mix(in oklab, <color> <pct>%, transparent)` (emitted by
|
|
1146
|
+
// `shadow-<token>` / `ring-<token>`) to a flat rgba/hex and unwraps
|
|
1147
|
+
// `var(…, fallback)`. Modern spaces (`oklch(…)`) then lower via
|
|
1148
|
+
// `normalizeColorString`; anything still un-RN-safe falls to culori.
|
|
1149
|
+
const coerced = coerceUnparsedValue(unwrapVariableFallback(substituted).trim())
|
|
1150
|
+
if (typeof coerced !== 'string' || coerced.length === 0 || coerced.startsWith('var(')) return null
|
|
1151
|
+
if (coerced.startsWith('#') || coerced.startsWith('rgb') || coerced.startsWith('hsl')) return coerced
|
|
1152
|
+
return normalizeColorString(coerced) ?? parseCssColorToHex(coerced)
|
|
1060
1153
|
}
|
|
1061
1154
|
|
|
1062
1155
|
/**
|
|
@@ -1168,6 +1261,11 @@ function parseShadowColor(expr: string): { color: string; opacity: number } {
|
|
|
1168
1261
|
const rgba = parseRgbaExpression(working)
|
|
1169
1262
|
if (rgba) return rgba
|
|
1170
1263
|
if (working.startsWith('#')) return { color: working, opacity: 1 }
|
|
1264
|
+
// Named (`red`) / modern (`hsl(…)`, `oklch(…)`) colors — culori → sRGB hex.
|
|
1265
|
+
// Without this they fell to the default black at 0.1 alpha, silently losing
|
|
1266
|
+
// the user's `shadow-[0_2px_4px_red]` color.
|
|
1267
|
+
const hex = formatHexSafe(working)
|
|
1268
|
+
if (hex) return { color: hex, opacity: 1 }
|
|
1171
1269
|
return { color: '#000', opacity: 0.1 }
|
|
1172
1270
|
}
|
|
1173
1271
|
|
|
@@ -1379,8 +1477,8 @@ function resolveLengthExpression(text: string): number | string | null {
|
|
|
1379
1477
|
const evaluated = evaluateLengthExpr(trimmed)
|
|
1380
1478
|
if (!evaluated) return null
|
|
1381
1479
|
if (evaluated.unit === '%') return `${stripTrailingZeros(evaluated.value)}%`
|
|
1382
|
-
if (evaluated.unit === 'rem') return evaluated.value * 16
|
|
1383
|
-
return evaluated.value
|
|
1480
|
+
if (evaluated.unit === 'rem') return roundTransformValue(evaluated.value * 16)
|
|
1481
|
+
return roundTransformValue(evaluated.value)
|
|
1384
1482
|
}
|
|
1385
1483
|
|
|
1386
1484
|
/** Evaluated length + its unit. `''` means px or bare number. */
|
|
@@ -1607,17 +1705,36 @@ function parseArithmeticFactor(tokens: readonly string[], cursor: { index: numbe
|
|
|
1607
1705
|
}
|
|
1608
1706
|
|
|
1609
1707
|
/**
|
|
1610
|
-
* Resolve a scale factor expressed as a percentage (`150%`)
|
|
1708
|
+
* Resolve a scale factor expressed as a percentage (`150%`), number (`1.5`),
|
|
1709
|
+
* or a `calc()` expression. Tailwind emits NEGATIVE scale utilities as a calc
|
|
1710
|
+
* (`-scale-x-100` → `calc(100% * -1)`), so a plain percent/number regex
|
|
1711
|
+
* silently dropped them — `-scale-*` (the horizontal-flip idiom) rendered
|
|
1712
|
+
* nothing. Fall back to the shared arithmetic evaluator, reading `%` as a
|
|
1713
|
+
* fraction (`100%` → 1) and rounding off f32 noise.
|
|
1611
1714
|
* @param text Raw value.
|
|
1612
|
-
* @returns Scale number (e.g. 1.5 for 150%), or null.
|
|
1715
|
+
* @returns Scale number (e.g. 1.5 for 150%, -1 for `calc(100% * -1)`), or null.
|
|
1613
1716
|
*/
|
|
1614
1717
|
function resolveNumberOrPercent(text: string): number | null {
|
|
1615
1718
|
const trimmed = text.trim()
|
|
1616
1719
|
const percent = /^(-?\d+(?:\.\d+)?)%$/.exec(trimmed)
|
|
1617
|
-
if (percent) return Number(percent[1]) / 100
|
|
1720
|
+
if (percent) return roundTransformValue(Number(percent[1]) / 100)
|
|
1618
1721
|
const bare = /^-?\d+(?:\.\d+)?$/.exec(trimmed)
|
|
1619
|
-
if (bare) return Number(trimmed)
|
|
1620
|
-
|
|
1722
|
+
if (bare) return roundTransformValue(Number(trimmed))
|
|
1723
|
+
const evaluated = evaluateLengthExpr(trimmed)
|
|
1724
|
+
if (!evaluated || evaluated.unit === 'rem') return null
|
|
1725
|
+
return roundTransformValue(evaluated.unit === '%' ? evaluated.value / 100 : evaluated.value)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* Round a composed-transform numeric value to 4 decimals. lightningcss
|
|
1730
|
+
* serializes arbitrary literals (`scale-x-[0.333]`) back as noisy f32 text
|
|
1731
|
+
* (`0.3330000042915344`), and the resolvers `Number()` that verbatim — round
|
|
1732
|
+
* so the RN `transform` array stays clean.
|
|
1733
|
+
* @param value Raw number.
|
|
1734
|
+
* @returns Rounded number.
|
|
1735
|
+
*/
|
|
1736
|
+
function roundTransformValue(value: number): number {
|
|
1737
|
+
return Math.round(value * 10_000) / 10_000
|
|
1621
1738
|
}
|
|
1622
1739
|
|
|
1623
1740
|
/**
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { Declaration as LcDeclaration } from 'lightningcss'
|
|
2
2
|
import { lineHeightToEntries } from './typography'
|
|
3
|
+
import { firstConcreteFontFamily } from './tokens'
|
|
3
4
|
import type { RNEntry } from './types'
|
|
4
5
|
|
|
6
|
+
/** RN-supported `textDecorationStyle` values (`wavy` has no RN equivalent). */
|
|
7
|
+
const RN_DECORATION_STYLES: ReadonlySet<string> = new Set(['solid', 'double', 'dotted', 'dashed'])
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Build the RN `textDecorationLine` entry — string identity for the
|
|
7
11
|
* single-line cases, joined-string for the array shape.
|
|
@@ -45,7 +49,9 @@ function letterSpacingToEntries(value: LcDeclaration['value']): readonly RNEntry
|
|
|
45
49
|
if (inner?.type !== 'value' || !inner.value) return []
|
|
46
50
|
const { unit, value: px } = inner.value
|
|
47
51
|
if (typeof px !== 'number') return []
|
|
48
|
-
|
|
52
|
+
const resolved = unit === 'px' ? px : px * 16
|
|
53
|
+
// Round off lightningcss f32 noise (`0.1em` → `1.600000023841858`).
|
|
54
|
+
return [['letterSpacing', Math.round(resolved * 10_000) / 10_000]]
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
/**
|
|
@@ -81,6 +87,19 @@ export function dispatchTypographyDeclaration(decl: LcDeclaration): readonly RNE
|
|
|
81
87
|
case 'text-decoration-line': {
|
|
82
88
|
return textDecorationLineToEntries(decl.value)
|
|
83
89
|
}
|
|
90
|
+
case 'text-decoration-style': {
|
|
91
|
+
// RN <Text> supports textDecorationStyle (solid/double/dotted/dashed).
|
|
92
|
+
const style = String(decl.value)
|
|
93
|
+
return RN_DECORATION_STYLES.has(style) ? [['textDecorationStyle', style]] : []
|
|
94
|
+
}
|
|
95
|
+
case 'font-family': {
|
|
96
|
+
// Typed `font-family` is a fallback LIST (`font-sans`, `font-mono`,
|
|
97
|
+
// `font-[Inter]`). RN takes one concrete typeface; an all-generic
|
|
98
|
+
// stack (default `font-sans`) emits nothing → system font. The themed
|
|
99
|
+
// `var(--font-*)` path goes through `coerceFontFamily` in declaration.ts.
|
|
100
|
+
const family = firstConcreteFontFamily(decl.value as readonly unknown[])
|
|
101
|
+
return family === undefined ? [] : [['fontFamily', family]]
|
|
102
|
+
}
|
|
84
103
|
case 'aspect-ratio': {
|
|
85
104
|
return aspectRatioToEntries(decl.value)
|
|
86
105
|
}
|
|
@@ -3,25 +3,25 @@ import { dimensionPercentageToNumber } from './length'
|
|
|
3
3
|
import type { RNEntry } from './types'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
6
|
+
* Display values React Native's `display` style prop actually accepts.
|
|
7
|
+
* Everything else (`block`, `inline`, `inline-block`, `grid`, `table`, …)
|
|
8
|
+
* has no RN analog — RN lays out as flex by default, and emitting an invalid
|
|
9
|
+
* value triggers a dev warning + silent drop. So we drop them outright.
|
|
10
|
+
*/
|
|
11
|
+
const RN_DISPLAY_VALUES: ReadonlySet<string> = new Set(['none', 'flex', 'contents'])
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Expand lightningcss's `Display` typed value to an RN `{display}` entry,
|
|
15
|
+
* keeping only the values RN supports (`none` / `flex` / `contents`).
|
|
16
|
+
* - `keyword` variant emits only when the keyword is RN-valid.
|
|
17
|
+
* - `pair` variant (the modern CSS model) collapses `flex` inside to
|
|
18
|
+
* `'flex'`; `flow` (`block`/`inline`) and `grid` have no RN analog → drop.
|
|
11
19
|
* @param value Typed display value.
|
|
12
20
|
* @returns RN entries (zero or one).
|
|
13
21
|
*/
|
|
14
22
|
export function displayToEntries(value: Display): readonly RNEntry[] {
|
|
15
|
-
if (value.type === 'keyword') return [['display', value.value]]
|
|
16
|
-
if (value.type === 'pair')
|
|
17
|
-
const inside = value.inside.type
|
|
18
|
-
// `flow` is the default inside mode — maps to `block` / `inline` /
|
|
19
|
-
// `inline-block` based on the outer; RN only distinguishes `block`-ish
|
|
20
|
-
// from `flex`, so collapse the `flow` family to the `outside` keyword.
|
|
21
|
-
if (inside === 'flow') return [['display', value.outside]]
|
|
22
|
-
if (inside === 'flex') return [['display', 'flex']]
|
|
23
|
-
if (inside === 'grid') return [['display', 'grid']]
|
|
24
|
-
}
|
|
23
|
+
if (value.type === 'keyword') return RN_DISPLAY_VALUES.has(value.value) ? [['display', value.value]] : []
|
|
24
|
+
if (value.type === 'pair' && value.inside.type === 'flex') return [['display', 'flex']]
|
|
25
25
|
return []
|
|
26
26
|
}
|
|
27
27
|
|