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
package/src/metro/transformer.ts
CHANGED
|
@@ -17,14 +17,6 @@ interface UpstreamTransformer {
|
|
|
17
17
|
/** Env var that points at the upstream `babelTransformerPath` we override. */
|
|
18
18
|
const UPSTREAM_ENV = 'RNWIND_UPSTREAM_TRANSFORMER'
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Matches a `useCss(` call. Files that resolve classes via the `useCss`
|
|
22
|
-
* hook (no JSX `className=`) must still be scanned so the classes inside
|
|
23
|
-
* `useCss("…")` get registered — common for theme/accent hooks living in a
|
|
24
|
-
* shared package the project scan may not reach.
|
|
25
|
-
*/
|
|
26
|
-
const USE_CSS_CALL = /\buseCss\s*\(/
|
|
27
|
-
|
|
28
20
|
/** Cached upstream module — required once, reused across every transform call. */
|
|
29
21
|
let cachedUpstream: UpstreamTransformer | null = null
|
|
30
22
|
|
|
@@ -145,44 +137,53 @@ function isThemeCssEntry(filename: string): boolean {
|
|
|
145
137
|
* @returns Rewritten source text.
|
|
146
138
|
*/
|
|
147
139
|
async function rewriteSource(args: BabelTransformerArgs): Promise<string> {
|
|
148
|
-
|
|
149
|
-
|
|
140
|
+
// SCAN FIRST — exactly like Tailwind: oxide extracts class candidates from
|
|
141
|
+
// the WHOLE file content (className, cva/clsx, plain strings, anywhere),
|
|
142
|
+
// and the compiler is the only filter. No babel parse needed to scan, so
|
|
143
|
+
// the common "file with no classes" case stays cheap and the source is
|
|
144
|
+
// returned untouched. This is what makes adding a class ANYWHERE register
|
|
145
|
+
// on hot-reload, not just inside a `className=` or a known helper call.
|
|
146
|
+
const state = getRnwindState(projectRootOf(args))
|
|
147
|
+
const extension = extensionOf(args.filename)
|
|
148
|
+
const parsed = await state.parser.parseAtoms({ content: args.src, extension })
|
|
149
|
+
const hasAtoms = parsed.atoms.size > 0
|
|
150
150
|
|
|
151
151
|
const hasClassName = /classname=/i.test(args.src)
|
|
152
|
-
const hasUseCss = USE_CSS_CALL.test(args.src)
|
|
153
152
|
const hasSpread = /\{\s*\.\.\./.test(args.src)
|
|
154
153
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const wrapped = hasClassName || hasSpread ? rewriteWrapImports(ast, getWrapModules()) : false
|
|
161
|
-
|
|
162
|
-
if (!hasClassName && !hasUseCss) {
|
|
163
|
-
// Import-only file: nothing to compile. Drop any stale atom
|
|
164
|
-
// contribution (className may have just been removed) and emit the
|
|
165
|
-
// wrapped imports — or the untouched source when nothing wrapped.
|
|
166
|
-
dropFileSafely(args.filename, projectRootOf(args))
|
|
167
|
-
return wrapped ? generateModule(ast).code : args.src
|
|
154
|
+
// Nothing for rnwind to do: no Tailwind class compiled AND no host
|
|
155
|
+
// wrapping needed. Drop any stale contribution and emit the file as-is.
|
|
156
|
+
if (!hasAtoms && !hasClassName && !hasSpread) {
|
|
157
|
+
state.builder.dropFile(args.filename)
|
|
158
|
+
return args.src
|
|
168
159
|
}
|
|
169
160
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
const ast = parseUserSource(args.src)
|
|
162
|
+
if (!ast) {
|
|
163
|
+
// Can't parse to wrap/inject, but we DID scan — still record the atoms so
|
|
164
|
+
// they register (a malformed-but-recoverable file shouldn't lose styles).
|
|
165
|
+
if (hasAtoms) {
|
|
166
|
+
await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes, [])
|
|
167
|
+
await state.builder.writeSchemes()
|
|
168
|
+
} else {
|
|
169
|
+
state.builder.dropFile(args.filename)
|
|
170
|
+
}
|
|
171
|
+
return args.src
|
|
172
|
+
}
|
|
176
173
|
|
|
177
|
-
|
|
174
|
+
// Wrap host imports ONLY when className flows through a component — written
|
|
175
|
+
// (`hasClassName`) or forwarded (`{...rest}`). A `useCss`/`cva`-only file
|
|
176
|
+
// resolves manually, so its imports (e.g. skia drawing primitives) are
|
|
177
|
+
// left untouched.
|
|
178
|
+
const wrapped = hasClassName || hasSpread ? rewriteWrapImports(ast, getWrapModules()) : false
|
|
178
179
|
|
|
179
|
-
if (
|
|
180
|
+
if (!hasAtoms) {
|
|
181
|
+
// Wrap-only forwarder — no classes to record/register.
|
|
180
182
|
state.builder.dropFile(args.filename)
|
|
181
|
-
|
|
182
|
-
injectThemeSignatureImport(ast)
|
|
183
|
-
return generateModule(ast).code
|
|
183
|
+
return wrapped ? generateModule(ast).code : args.src
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename, [parsed.gradientAtoms, parsed.hapticAtoms])
|
|
186
187
|
const literals = collectClassNameLiterals(ast)
|
|
187
188
|
const { changed } = await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes, literals)
|
|
188
189
|
if (changed) await state.builder.writeSchemes()
|
|
@@ -192,21 +193,6 @@ async function rewriteSource(args: BabelTransformerArgs): Promise<string> {
|
|
|
192
193
|
return generateModule(ast).code
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
/**
|
|
196
|
-
* Drop a file's union contribution, swallowing the "state not configured"
|
|
197
|
-
* error unit tests hit when they call the transformer without
|
|
198
|
-
* `configureRnwindState`.
|
|
199
|
-
* @param filename Absolute source path.
|
|
200
|
-
* @param projectRoot Project root for state lookup.
|
|
201
|
-
*/
|
|
202
|
-
function dropFileSafely(filename: string, projectRoot: string): void {
|
|
203
|
-
try {
|
|
204
|
-
getRnwindState(projectRoot).builder.dropFile(filename)
|
|
205
|
-
} catch {
|
|
206
|
-
// State not configured (standalone/unit test). Nothing to drop.
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
196
|
/**
|
|
211
197
|
* Whether a JSX attribute names a className-style prop (`className` or
|
|
212
198
|
* any `<prefix>ClassName`).
|
|
@@ -404,19 +390,12 @@ function loadUpstream(): UpstreamTransformer | null {
|
|
|
404
390
|
*/
|
|
405
391
|
function isRewriteCandidate(args: BabelTransformerArgs): boolean {
|
|
406
392
|
if (!/\.(?:tsx|ts|jsx|js)$/i.test(args.filename)) return false
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
// forwarded className must still get its import wrapped (no literal
|
|
414
|
-
// appears in this file). A style-less `<View/>` with none is left
|
|
415
|
-
// alone so it never pays for an unused wrapper.
|
|
416
|
-
const hasClassName = /classname=/i.test(args.src)
|
|
417
|
-
const hasUseCss = USE_CSS_CALL.test(args.src)
|
|
418
|
-
const isForwarder = /\{\s*\.\.\./.test(args.src) && mentionsWrapModule(args.src)
|
|
419
|
-
if (!hasClassName && !hasUseCss && !isForwarder) return false
|
|
393
|
+
// EVERY user source file is scanned — exactly like Tailwind walks its
|
|
394
|
+
// whole content set. The compiler is the only filter (see rewriteSource);
|
|
395
|
+
// no className/helper pre-filter, because that "filter magic" missed
|
|
396
|
+
// classes living in cva/clsx/plain-string files and broke their hot-reload.
|
|
397
|
+
// (Cheap when the file has no classes: oxide finds nothing, no babel parse,
|
|
398
|
+
// source returned untouched.)
|
|
420
399
|
if (!args.filename.includes('/node_modules/')) return true
|
|
421
400
|
// node_modules in path → could be a workspace symlink; resolve it.
|
|
422
401
|
try {
|
|
@@ -427,21 +406,6 @@ function isRewriteCandidate(args: BabelTransformerArgs): boolean {
|
|
|
427
406
|
}
|
|
428
407
|
}
|
|
429
408
|
|
|
430
|
-
/**
|
|
431
|
-
* Cheap pre-parse check: does the source import from any configured
|
|
432
|
-
* wrap-module? A quoted specifier match is enough — `rewriteWrapImports`
|
|
433
|
-
* re-verifies precisely on the AST, so a false positive only costs a
|
|
434
|
-
* no-op parse.
|
|
435
|
-
* @param source Source text.
|
|
436
|
-
* @returns True when a wrap-module specifier appears in the source.
|
|
437
|
-
*/
|
|
438
|
-
function mentionsWrapModule(source: string): boolean {
|
|
439
|
-
for (const moduleName of getWrapModules().keys()) {
|
|
440
|
-
if (source.includes(`'${moduleName}'`) || source.includes(`"${moduleName}"`)) return true
|
|
441
|
-
}
|
|
442
|
-
return false
|
|
443
|
-
}
|
|
444
|
-
|
|
445
409
|
/**
|
|
446
410
|
* Fallback parse when no upstream is configured AND Metro didn't hand
|
|
447
411
|
* us an AST. Used by unit tests and standalone setups.
|
package/src/metro/with-config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, watch as watchFile } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { writeDtsFile } from './dts'
|
|
4
4
|
import { createRnwindResolver, type ResolveRequestFn } from './resolver'
|
|
@@ -15,14 +15,15 @@ const DEFAULT_CACHE_DIR = '.rnwind'
|
|
|
15
15
|
let activeCssWatcher: { cssPath: string; close: () => void } | null = null
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Watch the theme CSS for edits. On change,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
18
|
+
* Watch the theme CSS for edits. On change, rebuild state against the fresh
|
|
19
|
+
* CSS and rewrite the per-scheme files (`onThemeChange` → full rescan →
|
|
20
|
+
* `writeSchemes`). That's the entire HMR signal: every transformed source
|
|
21
|
+
* imports `rnwind/__generated/schemes`, which eager-imports
|
|
22
|
+
* `common.style.js`; the rewrite changes its bytes, Metro's content-SHA1
|
|
23
|
+
* dedup notices, and every importer is invalidated + re-bundled with the new
|
|
24
|
+
* style values. The rewritten JSX references atoms by NAME (theme-independent),
|
|
25
|
+
* so no source-file re-transform / mtime nudge is needed — the dep graph
|
|
26
|
+
* carries the signal.
|
|
26
27
|
* @param cssPath Absolute path to the theme CSS to watch.
|
|
27
28
|
* @param projectRoot Metro's project root (for `getRnwindState`).
|
|
28
29
|
*/
|
|
@@ -40,7 +41,6 @@ function watchThemeCss(cssPath: string, projectRoot: string): void {
|
|
|
40
41
|
pending = false
|
|
41
42
|
try {
|
|
42
43
|
await onThemeChange(projectRoot)
|
|
43
|
-
touchRecordedFiles(projectRoot)
|
|
44
44
|
} catch {
|
|
45
45
|
// Invalidation is best-effort — never crash the dev server.
|
|
46
46
|
}
|
|
@@ -49,25 +49,6 @@ function watchThemeCss(cssPath: string, projectRoot: string): void {
|
|
|
49
49
|
activeCssWatcher = { cssPath, close: () => watcher.close() }
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
* Bump the mtime on every file the builder has transformed. Metro's
|
|
54
|
-
* file watcher keys on `mtime`, so this is what makes it invalidate
|
|
55
|
-
* those modules and re-transform them.
|
|
56
|
-
* @param projectRoot Metro's project root.
|
|
57
|
-
*/
|
|
58
|
-
function touchRecordedFiles(projectRoot: string): void {
|
|
59
|
-
const state = getRnwindState(projectRoot)
|
|
60
|
-
const files = state.builder.recordedFiles()
|
|
61
|
-
const now = new Date()
|
|
62
|
-
for (const file of files) {
|
|
63
|
-
try {
|
|
64
|
-
if (existsSync(file)) utimesSync(file, now, now)
|
|
65
|
-
} catch {
|
|
66
|
-
// One file's stat failure shouldn't stop the others.
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
52
|
/**
|
|
72
53
|
* Where the rnwind babel transformer lives — resolved relative to this
|
|
73
54
|
* module so the path works from both the `src/` tree (tests) and the
|
|
@@ -42,22 +42,25 @@ export function useToken(cssVariable: string): string | number | undefined {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Read a color token by shorthand name — `useColor('primary')` resolves
|
|
45
|
-
* `--color-primary` for the active scheme.
|
|
46
|
-
*
|
|
45
|
+
* `--color-primary` for the active scheme. A fully-qualified name
|
|
46
|
+
* (`--color-primary`) is accepted as-is, so the call doesn't silently miss by
|
|
47
|
+
* double-prefixing into `--color---color-primary`.
|
|
48
|
+
* @param name Token suffix after `--color-`, or the full `--color-*` name.
|
|
47
49
|
* @returns Resolved color string, or undefined when the token is missing
|
|
48
50
|
* or its value isn't a string.
|
|
49
51
|
*/
|
|
50
52
|
export function useColor(name: string): string | undefined {
|
|
51
|
-
const value = useToken(`--color-${name}`)
|
|
53
|
+
const value = useToken(name.startsWith('--') ? name : `--color-${name}`)
|
|
52
54
|
return typeof value === 'string' ? value : undefined
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
58
|
* Read a spacing token by shorthand name — `useSize('4')` resolves
|
|
57
|
-
* `--spacing-4` for the active scheme.
|
|
58
|
-
*
|
|
59
|
+
* `--spacing-4` for the active scheme. A fully-qualified `--spacing-*` name is
|
|
60
|
+
* accepted as-is (no double-prefix miss).
|
|
61
|
+
* @param name Token suffix after `--spacing-`, or the full `--spacing-*` name.
|
|
59
62
|
* @returns Resolved spacing value, or undefined when the token is missing.
|
|
60
63
|
*/
|
|
61
64
|
export function useSize(name: string): number | string | undefined {
|
|
62
|
-
return useToken(`--spacing-${name}`)
|
|
65
|
+
return useToken(name.startsWith('--') ? name : `--spacing-${name}`)
|
|
63
66
|
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -280,6 +280,20 @@ function tierFor(windowWidth: number): number {
|
|
|
280
280
|
return tier
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Public breakpoint-tier for a width — the count of registered breakpoints
|
|
285
|
+
* whose threshold is reached. Used by the runtime resolver as its width
|
|
286
|
+
* cache dimension: the numeric tier is bounded AND exact, unlike the
|
|
287
|
+
* clamped `activeBreakpoint` NAME (which collapses tier-0 into the smallest
|
|
288
|
+
* breakpoint, so widths straddling that threshold would share a cache key
|
|
289
|
+
* and serve a stale style).
|
|
290
|
+
* @param windowWidth Live window width in px.
|
|
291
|
+
* @returns Tier 0..N.
|
|
292
|
+
*/
|
|
293
|
+
export function breakpointTier(windowWidth: number): number {
|
|
294
|
+
return tierFor(windowWidth)
|
|
295
|
+
}
|
|
296
|
+
|
|
283
297
|
/**
|
|
284
298
|
* Build the style array for a (hoist, scheme, state, width) tuple.
|
|
285
299
|
* Walks the atom list, applies the interact-state and breakpoint
|
package/src/runtime/resolve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getStyleVersion, lookupCss, type InteractState } from './lookup-css'
|
|
1
|
+
import { breakpointTier, getStyleVersion, lookupCss, type InteractState } from './lookup-css'
|
|
2
2
|
import type { RnwindState } from './components/rnwind-provider'
|
|
3
3
|
import { normalizeClassName } from '../core/normalize-classname'
|
|
4
4
|
import type { GradientAtomInfo, GradientDirection } from '../core/parser/gradient'
|
|
@@ -148,17 +148,19 @@ const stateSignatureCache = new WeakMap<RnwindState, string>()
|
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Cache key dimension for the reactive context — everything that can
|
|
151
|
-
* change a resolved style. Uses the breakpoint TIER (
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
151
|
+
* change a resolved style. Uses the numeric breakpoint TIER (count of
|
|
152
|
+
* thresholds reached) from `breakpointTier(windowWidth)`, NOT the
|
|
153
|
+
* `activeBreakpoint` NAME: the name clamps tier-0 into the smallest
|
|
154
|
+
* breakpoint, so widths straddling that threshold (e.g. 320 vs 700 with
|
|
155
|
+
* `sm=640`) would collide on one cache key and serve a stale style. The
|
|
156
|
+
* tier is exact AND bounded — two widths in the same tier gate every
|
|
157
|
+
* `sm:`/`md:`/… atom identically, so they resolve the same.
|
|
156
158
|
* @param state Rnwind context.
|
|
157
159
|
* @returns Compact signature string.
|
|
158
160
|
*/
|
|
159
161
|
function stateSignature(state: RnwindState): string {
|
|
160
162
|
const { insets } = state
|
|
161
|
-
return `${state.scheme}|${insets.top},${insets.right},${insets.bottom},${insets.left}|${state.fontScale}|${state.
|
|
163
|
+
return `${state.scheme}|${insets.top},${insets.right},${insets.bottom},${insets.left}|${state.fontScale}|${breakpointTier(state.windowWidth)}`
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
/**
|
package/src/runtime/wrap.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { chainFocus, chainPress } from './chain-handlers'
|
|
|
3
3
|
import { useInteract } from './hooks/use-interact'
|
|
4
4
|
import { useRnwind } from './components/rnwind-provider'
|
|
5
5
|
import type { RnwindState } from './components/rnwind-provider'
|
|
6
|
+
import type { InteractState } from './lookup-css'
|
|
6
7
|
import { resolve, type ResolvedCss } from './resolve'
|
|
7
8
|
import type { OnHaptics } from '../core/parser/haptics'
|
|
8
9
|
|
|
@@ -79,14 +80,20 @@ const CLASSNAME_SUFFIX = 'ClassName'
|
|
|
79
80
|
* handled separately by the leaf and never reaches here.
|
|
80
81
|
* @param props Mutable prop object being assembled for the host.
|
|
81
82
|
* @param state Rnwind context for resolution.
|
|
83
|
+
* @param interactState Live press/focus state so `active:`/`focus:` variants
|
|
84
|
+
* on a secondary class prop resolve too (undefined for non-interactive).
|
|
82
85
|
*/
|
|
83
|
-
function applyContainerClassNames(
|
|
86
|
+
function applyContainerClassNames(
|
|
87
|
+
props: Record<string, unknown>,
|
|
88
|
+
state: RnwindState,
|
|
89
|
+
interactState?: InteractState,
|
|
90
|
+
): void {
|
|
84
91
|
for (const key of Object.keys(props)) {
|
|
85
92
|
if (!key.endsWith(CLASSNAME_SUFFIX)) continue
|
|
86
93
|
const value = props[key]
|
|
87
94
|
if (typeof value !== 'string') continue
|
|
88
95
|
const styleKey = `${key.slice(0, -CLASSNAME_SUFFIX.length)}Style`
|
|
89
|
-
props[styleKey] = resolve(value, state, props[styleKey]).style
|
|
96
|
+
props[styleKey] = resolve(value, state, props[styleKey], interactState).style
|
|
90
97
|
delete props[key]
|
|
91
98
|
}
|
|
92
99
|
}
|
|
@@ -102,6 +109,7 @@ function applyContainerClassNames(props: Record<string, unknown>, state: RnwindS
|
|
|
102
109
|
* @param state Rnwind context — used to resolve secondary class props.
|
|
103
110
|
* @param onHaptics Dispatcher from context.
|
|
104
111
|
* @param userOnPressIn Caller-supplied onPressIn to chain after the haptic.
|
|
112
|
+
* @param interactState Live press/focus state for secondary class props.
|
|
105
113
|
* @returns The merged prop object for `createElement`.
|
|
106
114
|
*/
|
|
107
115
|
function buildProps(
|
|
@@ -110,10 +118,11 @@ function buildProps(
|
|
|
110
118
|
state: RnwindState,
|
|
111
119
|
onHaptics: OnHaptics | undefined,
|
|
112
120
|
userOnPressIn?: unknown,
|
|
121
|
+
interactState?: InteractState,
|
|
113
122
|
): Record<string, unknown> {
|
|
114
123
|
warnIfHapticsUnwired(onHaptics, resolved.haptics)
|
|
115
124
|
const props: Record<string, unknown> = { ...rest, style: resolved.style }
|
|
116
|
-
applyContainerClassNames(props, state)
|
|
125
|
+
applyContainerClassNames(props, state, interactState)
|
|
117
126
|
if (resolved.colors) {
|
|
118
127
|
props.colors = resolved.colors
|
|
119
128
|
props.start = resolved.start
|
|
@@ -136,66 +145,31 @@ function buildProps(
|
|
|
136
145
|
return props
|
|
137
146
|
}
|
|
138
147
|
|
|
139
|
-
/** Props
|
|
140
|
-
interface
|
|
141
|
-
readonly as: ComponentType<Record<string, unknown>>
|
|
148
|
+
/** Props the wrapped renderer accepts — `className` plus forwarded props. */
|
|
149
|
+
interface WrappedProps {
|
|
142
150
|
readonly className?: string
|
|
143
151
|
readonly style?: unknown
|
|
152
|
+
readonly onPressIn?: unknown
|
|
153
|
+
readonly onPressOut?: unknown
|
|
154
|
+
readonly onFocus?: unknown
|
|
155
|
+
readonly onBlur?: unknown
|
|
144
156
|
readonly [key: string]: unknown
|
|
145
157
|
}
|
|
146
158
|
|
|
147
|
-
/**
|
|
148
|
-
* Non-interactive leaf: resolve className → style (+ features) and
|
|
149
|
-
* forward. One context read, one molecule/atom resolve.
|
|
150
|
-
* @param props Leaf props.
|
|
151
|
-
* @param props.as
|
|
152
|
-
* @param props.className
|
|
153
|
-
* @param props.style
|
|
154
|
-
* @param props.onPressIn
|
|
155
|
-
* @returns The rendered `as` element.
|
|
156
|
-
*/
|
|
157
|
-
function PlainLeaf({ as: As, className, style, onPressIn, ...rest }: LeafProps): ReactElement {
|
|
158
|
-
const state = useRnwind()
|
|
159
|
-
const resolved = resolve(className, state, style)
|
|
160
|
-
useMountHaptics(resolved, state.onHaptics)
|
|
161
|
-
return createElement(As, buildProps(rest, resolved, state, state.onHaptics, onPressIn))
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Interactive leaf: tracks press/focus via `useInteract()`, feeds it into
|
|
166
|
-
* `resolve` so `active:`/`focus:` atoms apply, and chains the
|
|
167
|
-
* press/focus handlers.
|
|
168
|
-
* @param props Leaf props.
|
|
169
|
-
* @param props.as
|
|
170
|
-
* @param props.className
|
|
171
|
-
* @param props.style
|
|
172
|
-
* @param props.onPressIn
|
|
173
|
-
* @param props.onPressOut
|
|
174
|
-
* @param props.onFocus
|
|
175
|
-
* @param props.onBlur
|
|
176
|
-
* @returns The rendered `as` element with interactive wiring.
|
|
177
|
-
*/
|
|
178
|
-
function InteractiveLeaf({ as: As, className, style, onPressIn, onPressOut, onFocus, onBlur, ...rest }: LeafProps): ReactElement {
|
|
179
|
-
const state = useRnwind()
|
|
180
|
-
const interact = useInteract()
|
|
181
|
-
const resolved = resolve(className, state, style, interact.state)
|
|
182
|
-
useMountHaptics(resolved, state.onHaptics)
|
|
183
|
-
const props = buildProps(rest, resolved, state, state.onHaptics, onPressIn)
|
|
184
|
-
props.onPressIn = chainPress(props.onPressIn as Parameters<typeof chainPress>[0], interact.onPressIn)
|
|
185
|
-
props.onPressOut = chainPress(onPressOut as Parameters<typeof chainPress>[0], interact.onPressOut)
|
|
186
|
-
props.onFocus = chainFocus(onFocus as Parameters<typeof chainFocus>[0], interact.onFocus)
|
|
187
|
-
props.onBlur = chainFocus(onBlur as Parameters<typeof chainFocus>[0], interact.onBlur)
|
|
188
|
-
return createElement(As, props)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
159
|
/**
|
|
192
160
|
* Wrap a component so its `className` prop resolves to RN `style` (plus
|
|
193
161
|
* gradient / truncate props and haptic dispatch) at render — no matter
|
|
194
162
|
* how className arrived: written directly, spread through `{...rest}`, or
|
|
195
|
-
* forwarded down custom wrappers.
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
163
|
+
* forwarded down custom wrappers. `ref` (a normal prop in React 19) and all
|
|
164
|
+
* other props forward untouched.
|
|
165
|
+
*
|
|
166
|
+
* A SINGLE stable component does the work and always calls `useInteract()`,
|
|
167
|
+
* so its identity never changes when `className` flips between interactive
|
|
168
|
+
* (`active:`/`focus:`) and not — swapping the rendered component type would
|
|
169
|
+
* unmount + remount the whole host subtree (lost state, effect re-runs,
|
|
170
|
+
* visual flash). `useInteract` is cheap and shares one idle-state ref, so the
|
|
171
|
+
* always-on cost is a couple of `useState` cells; the press/focus handlers
|
|
172
|
+
* and live state only wire in when the className actually needs them.
|
|
199
173
|
* @example
|
|
200
174
|
* ```tsx
|
|
201
175
|
* const Pressable = wrap(RNPressable)
|
|
@@ -207,16 +181,38 @@ function InteractiveLeaf({ as: As, className, style, onPressIn, onPressOut, onFo
|
|
|
207
181
|
export function wrap<P>(Component: ComponentType<P>): ComponentType<P & { className?: string }> {
|
|
208
182
|
const as = Component as unknown as ComponentType<Record<string, unknown>>
|
|
209
183
|
/**
|
|
210
|
-
* The wrapped component
|
|
184
|
+
* The wrapped component. Stable identity + unconditional hooks; branches
|
|
185
|
+
* internally on whether the className carries an interactive variant.
|
|
211
186
|
* @param props Forwarded props with `className` intercepted.
|
|
212
|
-
* @param props.className
|
|
213
|
-
* @
|
|
187
|
+
* @param props.className Raw className string.
|
|
188
|
+
* @param props.style Caller-supplied style, merged under the resolved style.
|
|
189
|
+
* @param props.onPressIn Caller onPressIn — chained after haptics / interact.
|
|
190
|
+
* @param props.onPressOut Caller onPressOut — chained with interact when active.
|
|
191
|
+
* @param props.onFocus Caller onFocus — chained with interact when active.
|
|
192
|
+
* @param props.onBlur Caller onBlur — chained with interact when active.
|
|
193
|
+
* @returns The rendered `as` element.
|
|
214
194
|
*/
|
|
215
|
-
function RnwindWrapped({ className,
|
|
216
|
-
|
|
217
|
-
|
|
195
|
+
function RnwindWrapped({ className, style, onPressIn, onPressOut, onFocus, onBlur, ...rest }: WrappedProps): ReactElement {
|
|
196
|
+
const state = useRnwind()
|
|
197
|
+
const interact = useInteract()
|
|
198
|
+
const isInteractive = className !== undefined && hasInteractiveVariant(className)
|
|
199
|
+
const interactState = isInteractive ? interact.state : undefined
|
|
200
|
+
const resolved = resolve(className, state, style, interactState)
|
|
201
|
+
useMountHaptics(resolved, state.onHaptics)
|
|
202
|
+
const props = buildProps(rest, resolved, state, state.onHaptics, onPressIn, interactState)
|
|
203
|
+
if (isInteractive) {
|
|
204
|
+
props.onPressIn = chainPress(props.onPressIn as Parameters<typeof chainPress>[0], interact.onPressIn)
|
|
205
|
+
props.onPressOut = chainPress(onPressOut as Parameters<typeof chainPress>[0], interact.onPressOut)
|
|
206
|
+
props.onFocus = chainFocus(onFocus as Parameters<typeof chainFocus>[0], interact.onFocus)
|
|
207
|
+
props.onBlur = chainFocus(onBlur as Parameters<typeof chainFocus>[0], interact.onBlur)
|
|
208
|
+
} else {
|
|
209
|
+
// Forward the caller's press/focus handlers untouched (onPressIn is
|
|
210
|
+
// already set by buildProps, possibly haptic-chained).
|
|
211
|
+
if (onPressOut !== undefined) props.onPressOut = onPressOut
|
|
212
|
+
if (onFocus !== undefined) props.onFocus = onFocus
|
|
213
|
+
if (onBlur !== undefined) props.onBlur = onBlur
|
|
218
214
|
}
|
|
219
|
-
return createElement(
|
|
215
|
+
return createElement(as, props)
|
|
220
216
|
}
|
|
221
217
|
RnwindWrapped.displayName = `wrap(${displayNameOf(Component)})`
|
|
222
218
|
return RnwindWrapped as unknown as ComponentType<P & { className?: string }>
|