rnwind 0.0.8 → 0.0.9
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 +93 -33
- package/lib/cjs/core/parser/tw-parser.cjs.map +1 -1
- 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/union-builder.cjs +0 -10
- package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -1
- package/lib/cjs/core/style-builder/union-builder.d.ts +0 -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 +9 -6
- 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 +1 -1
- package/lib/cjs/runtime/index.cjs.map +1 -1
- package/lib/cjs/runtime/index.d.ts +1 -1
- package/lib/cjs/runtime/lookup-css.cjs +14 -0
- package/lib/cjs/runtime/lookup-css.cjs.map +1 -1
- package/lib/cjs/runtime/lookup-css.d.ts +11 -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/esm/core/parser/color.d.ts +10 -0
- package/lib/esm/core/parser/color.mjs +33 -2
- 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.mjs +95 -35
- 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/union-builder.d.ts +0 -8
- package/lib/esm/core/style-builder/union-builder.mjs +0 -10
- 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 +9 -6
- package/lib/esm/runtime/hooks/use-scheme.mjs.map +1 -1
- package/lib/esm/runtime/index.d.ts +1 -1
- package/lib/esm/runtime/index.mjs +1 -1
- package/lib/esm/runtime/index.mjs.map +1 -1
- package/lib/esm/runtime/lookup-css.d.ts +11 -0
- package/lib/esm/runtime/lookup-css.mjs +14 -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/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 +95 -30
- package/src/core/parser/typography-dispatcher.ts +20 -1
- package/src/core/parser/typography.ts +15 -15
- package/src/core/style-builder/union-builder.ts +0 -11
- 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 +9 -6
- package/src/runtime/index.ts +1 -1
- package/src/runtime/lookup-css.ts +14 -0
- package/src/runtime/resolve.ts +9 -7
- package/src/runtime/wrap.tsx +57 -61
|
@@ -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,7 +16,8 @@ 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'
|
|
21
22
|
import type { Declaration as LcDeclaration, TokenOrValue } from 'lightningcss'
|
|
22
23
|
|
|
@@ -398,7 +399,8 @@ export class TailwindParser {
|
|
|
398
399
|
// surface their role + resolved colour so the transformer
|
|
399
400
|
// can rewrite `<LinearGradient className="...">` into
|
|
400
401
|
// `colors={...}` / `start={...}` / `end={...}` props.
|
|
401
|
-
const
|
|
402
|
+
const gradientTable = schemeTables.get(BASE_SCHEME) ?? schemeTables.get(schemes[0] ?? BASE_SCHEME)
|
|
403
|
+
const gradient = detectGradientAtom(rule.value.declarations.declarations, gradientTable)
|
|
402
404
|
if (gradient) gradientAtoms.set(className, gradient)
|
|
403
405
|
// Haptics may live on the rule directly OR inside a
|
|
404
406
|
// nested pseudo (e.g. `&:active` for `active:haptic-*`).
|
|
@@ -415,14 +417,16 @@ export class TailwindParser {
|
|
|
415
417
|
const steps: KeyframeStep[] = []
|
|
416
418
|
const baseTable = schemeTables.get(BASE_SCHEME) ?? schemeTables.get(schemes[0] ?? BASE_SCHEME)
|
|
417
419
|
for (const frame of rule.value.keyframes) {
|
|
418
|
-
const
|
|
419
|
-
if (
|
|
420
|
+
const offsets = keyframeSelectorOffsets(frame.selectors)
|
|
421
|
+
if (offsets.length === 0) continue
|
|
420
422
|
const style: RNStyle = {}
|
|
421
423
|
const frameDecls = frame.declarations.declarations ?? []
|
|
422
424
|
for (const decl of frameDecls) {
|
|
423
425
|
for (const [key, value] of declarationToRnEntries(decl, baseTable)) style[key] = value
|
|
424
426
|
}
|
|
425
|
-
|
|
427
|
+
// One frame can carry several offsets (`0%, 100% { … }`); emit a
|
|
428
|
+
// step for each so the terminal frame isn't lost.
|
|
429
|
+
for (const offset of offsets) steps.push({ offset, style })
|
|
426
430
|
}
|
|
427
431
|
keyframes.set(name, { name, steps })
|
|
428
432
|
},
|
|
@@ -582,8 +586,8 @@ function processStyleRule(
|
|
|
582
586
|
if (animationRef) ctx.referencedKeyframes.add(animationRef)
|
|
583
587
|
}
|
|
584
588
|
applyComposedTransform(bucket, ctx.schemes, ruleLocalVars)
|
|
585
|
-
applyComposedShadow(bucket, ctx.schemes, ruleLocalVars)
|
|
586
|
-
applyComposedRing(bucket, ctx.schemes, ruleLocalVars)
|
|
589
|
+
applyComposedShadow(bucket, ctx.schemes, ruleLocalVars, ruleSchemeTables)
|
|
590
|
+
applyComposedRing(bucket, ctx.schemes, ruleLocalVars, ruleSchemeTables)
|
|
587
591
|
// Phase 2: nested rules — three orthogonal flavours, dispatched on
|
|
588
592
|
// the lightningcss node `type`:
|
|
589
593
|
// - `media`: Tailwind v4 responsive variants (`sm:`, `md:`, …) wrap
|
|
@@ -729,7 +733,7 @@ function applyMediaRule(
|
|
|
729
733
|
const nestedLocalVars = new Map(ruleLocalVars)
|
|
730
734
|
for (const [k, v] of collectRuleLocalVars(decls)) nestedLocalVars.set(k, v)
|
|
731
735
|
applyComposedTransformToScheme(schemeBucket, nestedLocalVars)
|
|
732
|
-
applyComposedShadowToScheme(schemeBucket, nestedLocalVars)
|
|
736
|
+
applyComposedShadowToScheme(schemeBucket, nestedLocalVars, table)
|
|
733
737
|
bucket[scheme] = schemeBucket
|
|
734
738
|
}
|
|
735
739
|
}
|
|
@@ -769,7 +773,7 @@ function applyInteractiveNestedRule(
|
|
|
769
773
|
const nestedLocalVars = new Map(ruleLocalVars)
|
|
770
774
|
for (const [k, v] of collectRuleLocalVars(decls)) nestedLocalVars.set(k, v)
|
|
771
775
|
applyComposedTransformToScheme(schemeBucket, nestedLocalVars)
|
|
772
|
-
applyComposedShadowToScheme(schemeBucket, nestedLocalVars)
|
|
776
|
+
applyComposedShadowToScheme(schemeBucket, nestedLocalVars, table)
|
|
773
777
|
bucket[scheme] = schemeBucket
|
|
774
778
|
}
|
|
775
779
|
}
|
|
@@ -832,7 +836,7 @@ function applyNestedSchemeRule(
|
|
|
832
836
|
const nestedLocalVars = new Map(ruleLocalVars)
|
|
833
837
|
for (const [k, v] of collectRuleLocalVars(innerDecls)) nestedLocalVars.set(k, v)
|
|
834
838
|
applyComposedTransformToScheme(schemeBucket, nestedLocalVars)
|
|
835
|
-
applyComposedShadowToScheme(schemeBucket, nestedLocalVars)
|
|
839
|
+
applyComposedShadowToScheme(schemeBucket, nestedLocalVars, table)
|
|
836
840
|
bucket[targetScheme] = schemeBucket
|
|
837
841
|
}
|
|
838
842
|
|
|
@@ -948,12 +952,17 @@ function applyComposedTransformToScheme(style: RNStyle, ruleLocalVars: ReadonlyM
|
|
|
948
952
|
* prop.
|
|
949
953
|
* @param style Scheme-specific style map.
|
|
950
954
|
* @param ruleLocalVars Combined outer+nested `--tw-*` vars.
|
|
955
|
+
* @param table Per-scheme var table for resolving `var(--color-x)` in colors.
|
|
951
956
|
*/
|
|
952
|
-
function applyComposedShadowToScheme(
|
|
957
|
+
function applyComposedShadowToScheme(
|
|
958
|
+
style: RNStyle,
|
|
959
|
+
ruleLocalVars: ReadonlyMap<string, string>,
|
|
960
|
+
table?: ReadonlyMap<string, string>,
|
|
961
|
+
): void {
|
|
953
962
|
const rawShadow = ruleLocalVars.get('--tw-shadow')
|
|
954
963
|
const rawShadowColor = ruleLocalVars.get('--tw-shadow-color')
|
|
955
964
|
if (!rawShadow && rawShadowColor) {
|
|
956
|
-
const color = resolveCustomColorString(rawShadowColor)
|
|
965
|
+
const color = resolveCustomColorString(rawShadowColor, table)
|
|
957
966
|
if (!color) return
|
|
958
967
|
delete style.boxShadow
|
|
959
968
|
style.shadowColor = color
|
|
@@ -980,11 +989,13 @@ function applyComposedShadowToScheme(style: RNStyle, ruleLocalVars: ReadonlyMap<
|
|
|
980
989
|
* @param bucket Per-scheme style map for the atom.
|
|
981
990
|
* @param schemes Scheme names active for this parse.
|
|
982
991
|
* @param ruleLocalVars Rule-local `--tw-*` vars.
|
|
992
|
+
* @param schemeTables Per-scheme var tables for resolving `var(--color-x)`.
|
|
983
993
|
*/
|
|
984
994
|
function applyComposedShadow(
|
|
985
995
|
bucket: Record<string, RNStyle>,
|
|
986
996
|
schemes: readonly string[],
|
|
987
997
|
ruleLocalVars: ReadonlyMap<string, string>,
|
|
998
|
+
schemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>,
|
|
988
999
|
): void {
|
|
989
1000
|
const rawShadow = ruleLocalVars.get('--tw-shadow')
|
|
990
1001
|
const rawShadowColor = ruleLocalVars.get('--tw-shadow-color')
|
|
@@ -994,9 +1005,10 @@ function applyComposedShadow(
|
|
|
994
1005
|
// where setting `--tw-shadow-color` swaps in a solid color). Offset /
|
|
995
1006
|
// blur / elevation come from the partner size utility's atom.
|
|
996
1007
|
if (!rawShadow && rawShadowColor) {
|
|
997
|
-
const color = resolveCustomColorString(rawShadowColor)
|
|
998
|
-
if (!color) return
|
|
999
1008
|
for (const scheme of schemes) {
|
|
1009
|
+
// Resolve per scheme — a custom token may differ between light/dark.
|
|
1010
|
+
const color = resolveCustomColorString(rawShadowColor, schemeTables.get(scheme))
|
|
1011
|
+
if (!color) continue
|
|
1000
1012
|
const style = bucket[scheme] ?? {}
|
|
1001
1013
|
delete style.boxShadow
|
|
1002
1014
|
style.shadowColor = color
|
|
@@ -1028,35 +1040,64 @@ function applyComposedShadow(
|
|
|
1028
1040
|
* @param bucket Per-scheme style map for the atom.
|
|
1029
1041
|
* @param schemes Scheme names active for this parse.
|
|
1030
1042
|
* @param ruleLocalVars Rule-local `--tw-*` vars.
|
|
1043
|
+
* @param schemeTables Per-scheme var tables for resolving `var(--color-x)`.
|
|
1031
1044
|
*/
|
|
1032
1045
|
function applyComposedRing(
|
|
1033
1046
|
bucket: Record<string, RNStyle>,
|
|
1034
1047
|
schemes: readonly string[],
|
|
1035
1048
|
ruleLocalVars: ReadonlyMap<string, string>,
|
|
1049
|
+
schemeTables: ReadonlyMap<string, ReadonlyMap<string, string>>,
|
|
1036
1050
|
): void {
|
|
1037
1051
|
const ringColor = ruleLocalVars.get('--tw-ring-color')
|
|
1038
1052
|
if (!ringColor) return
|
|
1039
|
-
const color = resolveCustomColorString(ringColor)
|
|
1040
|
-
if (!color) return
|
|
1041
1053
|
for (const scheme of schemes) {
|
|
1054
|
+
// Resolve per scheme — a custom token may differ between light/dark.
|
|
1055
|
+
const color = resolveCustomColorString(ringColor, schemeTables.get(scheme))
|
|
1056
|
+
if (!color) continue
|
|
1042
1057
|
const style = bucket[scheme] ?? {}
|
|
1043
1058
|
if (!('borderColor' in style)) style.borderColor = color
|
|
1044
1059
|
bucket[scheme] = style
|
|
1045
1060
|
}
|
|
1046
1061
|
}
|
|
1047
1062
|
|
|
1063
|
+
/**
|
|
1064
|
+
* Tailwind composable shadow/inset-shadow alpha defaults. Their `100%` lives
|
|
1065
|
+
* in an `@property` initial-value (not the rule's local vars), so after the
|
|
1066
|
+
* `@supports` color-mix is unwrapped, `var(--tw-shadow-alpha)` is left dangling
|
|
1067
|
+
* and the shadow color fails to resolve. Seed the default; a `/<opacity>`
|
|
1068
|
+
* modifier still wins because the in-rule table value overrides it.
|
|
1069
|
+
*/
|
|
1070
|
+
const COMPOSABLE_ALPHA_DEFAULTS: ReadonlyMap<string, string> = new Map([
|
|
1071
|
+
['--tw-shadow-alpha', '100%'],
|
|
1072
|
+
['--tw-inset-shadow-alpha', '100%'],
|
|
1073
|
+
])
|
|
1074
|
+
|
|
1048
1075
|
/**
|
|
1049
1076
|
* Resolve a CSS color string (`oklch(0.971 0.013 17.38)`, `#ff0000`,
|
|
1050
1077
|
* `rgb(0 0 0 / 0.1)`) to the hex string RN's `shadowColor` accepts.
|
|
1051
1078
|
* Wraps culori's parser via {@link parseCssColorToHex}.
|
|
1052
|
-
*
|
|
1079
|
+
*
|
|
1080
|
+
* Custom `@theme` color tokens arrive as `var(--color-x)` (only the default
|
|
1081
|
+
* palette is `theme(inline)`-d), so `table` is substituted FIRST — without it
|
|
1082
|
+
* `shadow-<token>` / `ring-<token>` silently drop the color (culori can't
|
|
1083
|
+
* parse a bare `var()`). The table is per-scheme so a token that differs
|
|
1084
|
+
* between light/dark resolves to the right value for each.
|
|
1085
|
+
* @param raw Raw color text from a `--tw-shadow-color` / `--tw-ring-color` prop.
|
|
1086
|
+
* @param table Per-scheme var table for resolving `var(--color-x)` references.
|
|
1053
1087
|
* @returns `#rrggbb` string, or null when culori can't parse it.
|
|
1054
1088
|
*/
|
|
1055
|
-
function resolveCustomColorString(raw: string): string | null {
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1089
|
+
function resolveCustomColorString(raw: string, table?: ReadonlyMap<string, string>): string | null {
|
|
1090
|
+
const seeded = new Map([...COMPOSABLE_ALPHA_DEFAULTS, ...(table ?? [])])
|
|
1091
|
+
const substituted = substituteThemeVars(raw, seeded)
|
|
1092
|
+
// `coerceUnparsedValue` collapses Tailwind's opacity shape
|
|
1093
|
+
// `color-mix(in oklab, <color> <pct>%, transparent)` (emitted by
|
|
1094
|
+
// `shadow-<token>` / `ring-<token>`) to a flat rgba/hex and unwraps
|
|
1095
|
+
// `var(…, fallback)`. Modern spaces (`oklch(…)`) then lower via
|
|
1096
|
+
// `normalizeColorString`; anything still un-RN-safe falls to culori.
|
|
1097
|
+
const coerced = coerceUnparsedValue(unwrapVariableFallback(substituted).trim())
|
|
1098
|
+
if (typeof coerced !== 'string' || coerced.length === 0 || coerced.startsWith('var(')) return null
|
|
1099
|
+
if (coerced.startsWith('#') || coerced.startsWith('rgb') || coerced.startsWith('hsl')) return coerced
|
|
1100
|
+
return normalizeColorString(coerced) ?? parseCssColorToHex(coerced)
|
|
1060
1101
|
}
|
|
1061
1102
|
|
|
1062
1103
|
/**
|
|
@@ -1168,6 +1209,11 @@ function parseShadowColor(expr: string): { color: string; opacity: number } {
|
|
|
1168
1209
|
const rgba = parseRgbaExpression(working)
|
|
1169
1210
|
if (rgba) return rgba
|
|
1170
1211
|
if (working.startsWith('#')) return { color: working, opacity: 1 }
|
|
1212
|
+
// Named (`red`) / modern (`hsl(…)`, `oklch(…)`) colors — culori → sRGB hex.
|
|
1213
|
+
// Without this they fell to the default black at 0.1 alpha, silently losing
|
|
1214
|
+
// the user's `shadow-[0_2px_4px_red]` color.
|
|
1215
|
+
const hex = formatHexSafe(working)
|
|
1216
|
+
if (hex) return { color: hex, opacity: 1 }
|
|
1171
1217
|
return { color: '#000', opacity: 0.1 }
|
|
1172
1218
|
}
|
|
1173
1219
|
|
|
@@ -1379,8 +1425,8 @@ function resolveLengthExpression(text: string): number | string | null {
|
|
|
1379
1425
|
const evaluated = evaluateLengthExpr(trimmed)
|
|
1380
1426
|
if (!evaluated) return null
|
|
1381
1427
|
if (evaluated.unit === '%') return `${stripTrailingZeros(evaluated.value)}%`
|
|
1382
|
-
if (evaluated.unit === 'rem') return evaluated.value * 16
|
|
1383
|
-
return evaluated.value
|
|
1428
|
+
if (evaluated.unit === 'rem') return roundTransformValue(evaluated.value * 16)
|
|
1429
|
+
return roundTransformValue(evaluated.value)
|
|
1384
1430
|
}
|
|
1385
1431
|
|
|
1386
1432
|
/** Evaluated length + its unit. `''` means px or bare number. */
|
|
@@ -1607,17 +1653,36 @@ function parseArithmeticFactor(tokens: readonly string[], cursor: { index: numbe
|
|
|
1607
1653
|
}
|
|
1608
1654
|
|
|
1609
1655
|
/**
|
|
1610
|
-
* Resolve a scale factor expressed as a percentage (`150%`)
|
|
1656
|
+
* Resolve a scale factor expressed as a percentage (`150%`), number (`1.5`),
|
|
1657
|
+
* or a `calc()` expression. Tailwind emits NEGATIVE scale utilities as a calc
|
|
1658
|
+
* (`-scale-x-100` → `calc(100% * -1)`), so a plain percent/number regex
|
|
1659
|
+
* silently dropped them — `-scale-*` (the horizontal-flip idiom) rendered
|
|
1660
|
+
* nothing. Fall back to the shared arithmetic evaluator, reading `%` as a
|
|
1661
|
+
* fraction (`100%` → 1) and rounding off f32 noise.
|
|
1611
1662
|
* @param text Raw value.
|
|
1612
|
-
* @returns Scale number (e.g. 1.5 for 150%), or null.
|
|
1663
|
+
* @returns Scale number (e.g. 1.5 for 150%, -1 for `calc(100% * -1)`), or null.
|
|
1613
1664
|
*/
|
|
1614
1665
|
function resolveNumberOrPercent(text: string): number | null {
|
|
1615
1666
|
const trimmed = text.trim()
|
|
1616
1667
|
const percent = /^(-?\d+(?:\.\d+)?)%$/.exec(trimmed)
|
|
1617
|
-
if (percent) return Number(percent[1]) / 100
|
|
1668
|
+
if (percent) return roundTransformValue(Number(percent[1]) / 100)
|
|
1618
1669
|
const bare = /^-?\d+(?:\.\d+)?$/.exec(trimmed)
|
|
1619
|
-
if (bare) return Number(trimmed)
|
|
1620
|
-
|
|
1670
|
+
if (bare) return roundTransformValue(Number(trimmed))
|
|
1671
|
+
const evaluated = evaluateLengthExpr(trimmed)
|
|
1672
|
+
if (!evaluated || evaluated.unit === 'rem') return null
|
|
1673
|
+
return roundTransformValue(evaluated.unit === '%' ? evaluated.value / 100 : evaluated.value)
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Round a composed-transform numeric value to 4 decimals. lightningcss
|
|
1678
|
+
* serializes arbitrary literals (`scale-x-[0.333]`) back as noisy f32 text
|
|
1679
|
+
* (`0.3330000042915344`), and the resolvers `Number()` that verbatim — round
|
|
1680
|
+
* so the RN `transform` array stays clean.
|
|
1681
|
+
* @param value Raw number.
|
|
1682
|
+
* @returns Rounded number.
|
|
1683
|
+
*/
|
|
1684
|
+
function roundTransformValue(value: number): number {
|
|
1685
|
+
return Math.round(value * 10_000) / 10_000
|
|
1621
1686
|
}
|
|
1622
1687
|
|
|
1623
1688
|
/**
|
|
@@ -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
|
|
|
@@ -143,17 +143,6 @@ class UnionBuilder {
|
|
|
143
143
|
return path.join(this.cacheDir, MANIFEST_BASENAME)
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
/**
|
|
147
|
-
* Snapshot of every source file the builder has recorded atoms for
|
|
148
|
-
* this worker session. Used by `withRnwindConfig`'s CSS watcher to
|
|
149
|
-
* touch `mtime` on each and nudge Metro into re-transforming them
|
|
150
|
-
* with the new theme values.
|
|
151
|
-
* @returns Absolute source paths.
|
|
152
|
-
*/
|
|
153
|
-
public recordedFiles(): readonly string[] {
|
|
154
|
-
return [...this.fileAtomSets.keys()]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
146
|
/** Cumulative cache-miss count — exposed for tests to assert cache behaviour. */
|
|
158
147
|
public get serializedMisses(): number {
|
|
159
148
|
return this.serializedMissesCount
|
package/src/metro/dts.ts
CHANGED
|
@@ -100,7 +100,12 @@ export function writeDtsFile(targetPath: string, schemes: readonly string[]): vo
|
|
|
100
100
|
lines.push('}', '')
|
|
101
101
|
if (schemes.length > 0) {
|
|
102
102
|
lines.push(`declare module 'rnwind' {`, ` export interface RnwindConfig {`)
|
|
103
|
-
|
|
103
|
+
// Escape backslash / single-quote so a scheme name with a quote (only
|
|
104
|
+
// reachable via the public `writeDtsFile`, since CSS idents can't contain
|
|
105
|
+
// one) can't emit invalid TS that breaks the whole file and drops the
|
|
106
|
+
// `className` augmentation project-wide. Single-quoted to match the rest
|
|
107
|
+
// of the generated declaration.
|
|
108
|
+
const schemeLiterals = schemes.map((s) => `'${s.replaceAll('\\', '\\\\').replaceAll("'", String.raw`\'`)}'`).join(', ')
|
|
104
109
|
lines.push(` themes: readonly [${schemeLiterals}]`, ` }`, '}', '')
|
|
105
110
|
}
|
|
106
111
|
// The `export {}` is mandatory — without at least one top-level
|