rnwind 0.0.5 → 0.0.7

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.
@@ -435,6 +435,21 @@ function unquoteCssString(text: string): string {
435
435
  return inner
436
436
  }
437
437
 
438
+ /**
439
+ * Coerce a CSS `font-family` value into the SINGLE typeface name RN wants.
440
+ * CSS font-family is a fallback LIST (`"Montserrat", sans-serif`), but RN's
441
+ * `fontFamily` takes one family — so take the first entry and strip its
442
+ * quotes. This is what lets the standard Tailwind convention
443
+ * (`--font-display: "Name", sans-serif`) work out of the box: `font-display`
444
+ * resolves to `{ fontFamily: 'Name' }`, not the raw quoted list.
445
+ * @param value Resolved `font-family` value (possibly a quoted list).
446
+ * @returns Bare first-family name.
447
+ */
448
+ export function coerceFontFamily(value: string): string {
449
+ const first = value.split(',')[0]?.trim() ?? value
450
+ return unquoteCssString(first)
451
+ }
452
+
438
453
  /**
439
454
  * Substitute every `var(--name [, fallback])` reference in `text` with
440
455
  * the value from `table` (or the fallback clause when the name misses).
@@ -43,6 +43,33 @@ const resolvedCache = new Map<string, ResolvedCss>()
43
43
  /** Version the cache was last valid for (`getStyleVersion()` + {@link registryVersion}). */
44
44
  let cachedFor = -1
45
45
 
46
+ /**
47
+ * Hard ceiling on the resolved cache. The cache is pure memoisation, so
48
+ * eviction only costs a re-resolve (sub-µs) — never correctness. Build
49
+ * molecules are NOT in here; they live permanently in `molecules`.
50
+ */
51
+ const MAX_RESOLVED_CACHE = 2048
52
+
53
+ /**
54
+ * Store a resolved result, bulk-evicting the OLDEST half when the cache
55
+ * hits {@link MAX_RESOLVED_CACHE}. `Map` preserves insertion order, so the
56
+ * first keys are the oldest. Bulk eviction keeps the hot (cache-hit) path
57
+ * free of per-access LRU bookkeeping at the cost of an occasional small
58
+ * recompute burst under sustained pressure (web / long sessions).
59
+ * @param key Cache key.
60
+ * @param value Resolved result to store.
61
+ */
62
+ function cacheResolved(key: string, value: ResolvedCss): void {
63
+ if (resolvedCache.size >= MAX_RESOLVED_CACHE) {
64
+ let drop = resolvedCache.size >> 1
65
+ for (const oldKey of resolvedCache.keys()) {
66
+ resolvedCache.delete(oldKey)
67
+ if (--drop <= 0) break
68
+ }
69
+ }
70
+ resolvedCache.set(key, value)
71
+ }
72
+
46
73
  /** A unit-square gradient endpoint. */
47
74
  interface GradientPoint {
48
75
  readonly x: number
@@ -121,13 +148,17 @@ const stateSignatureCache = new WeakMap<RnwindState, string>()
121
148
 
122
149
  /**
123
150
  * Cache key dimension for the reactive context — everything that can
124
- * change a resolved style.
151
+ * change a resolved style. Uses the breakpoint TIER (`activeBreakpoint`),
152
+ * NOT the raw `windowWidth`: two widths in the same tier gate `md:*` atoms
153
+ * identically, so they resolve the same. This collapses the window axis
154
+ * from "every pixel" to ~6 values, bounding the cache on resizable
155
+ * surfaces (web / desktop) without changing any result.
125
156
  * @param state Rnwind context.
126
157
  * @returns Compact signature string.
127
158
  */
128
159
  function stateSignature(state: RnwindState): string {
129
160
  const { insets } = state
130
- return `${state.scheme}|${insets.top},${insets.right},${insets.bottom},${insets.left}|${state.fontScale}|${state.windowWidth}`
161
+ return `${state.scheme}|${insets.top},${insets.right},${insets.bottom},${insets.left}|${state.fontScale}|${state.activeBreakpoint}`
131
162
  }
132
163
 
133
164
  /**
@@ -303,6 +334,22 @@ function attachFeatures(base: ResolvedCss, tokens: readonly string[]): ResolvedC
303
334
  return result
304
335
  }
305
336
 
337
+ /**
338
+ * Flatten the per-atom style array into ONE object — a *runtime molecule*.
339
+ * RN flattens a style array left-to-right (later wins), which is exactly
340
+ * `Object.assign` semantics, so the merged object renders identically
341
+ * while giving dynamic (cva / clsx) classNames the same single-object
342
+ * shape a build-time molecule has. The caller caches the result per
343
+ * `(className · state)`, so the merge runs once per unique context.
344
+ * @param array Per-atom style array from `lookupCss`.
345
+ * @returns Single merged style object.
346
+ */
347
+ function mergeStyleArray(array: readonly unknown[]): Record<string, unknown> {
348
+ const out: Record<string, unknown> = {}
349
+ for (const entry of array) if (entry && typeof entry === 'object') Object.assign(out, entry)
350
+ return out
351
+ }
352
+
306
353
  /**
307
354
  * Compose a resolved style with a caller-supplied inline style (user wins).
308
355
  * @param style
@@ -349,7 +396,7 @@ export function resolve(
349
396
  const normalized = normalizeClassName(className)
350
397
  if (normalized.length === 0) {
351
398
  const empty: ResolvedCss = { style: EMPTY }
352
- resolvedCache.set(key, empty)
399
+ cacheResolved(key, empty)
353
400
  return userStyle === undefined || userStyle === null ? empty : { style: [userStyle] }
354
401
  }
355
402
  // Molecules are static pre-merges; anything carrying `active:`/`focus:`
@@ -357,11 +404,16 @@ export function resolve(
357
404
  const tokens = normalized.split(' ')
358
405
  const molecule = interactState ? undefined : molecules[state.scheme]?.[normalized] ?? molecules[COMMON_SCHEME]?.[normalized]
359
406
  // Feature-only tokens (gradient / haptic / truncate) carry no style — keep
360
- // them out of the atom lookup so they don't warn as "unknown class".
407
+ // them out of the atom lookup so they don't warn as "unknown class". The
408
+ // atom array is merged into ONE object (a runtime molecule) so dynamic
409
+ // (cva / clsx) classNames get the same single-object shape as a build
410
+ // molecule; the cache below pins the context, so the merge is correct.
361
411
  const style =
362
- molecule === undefined ? lookupCss(tokens.filter((token) => !isFeatureOnlyToken(token)).join(' '), state, undefined, interactState) : molecule
412
+ molecule === undefined
413
+ ? mergeStyleArray(lookupCss(tokens.filter((token) => !isFeatureOnlyToken(token)).join(' '), state, undefined, interactState))
414
+ : molecule
363
415
  const base = attachFeatures({ style }, tokens)
364
- resolvedCache.set(key, base)
416
+ cacheResolved(key, base)
365
417
  return userStyle === undefined || userStyle === null ? base : { ...base, style: withUserStyle(base.style, userStyle) }
366
418
  }
367
419
 
@@ -378,4 +430,9 @@ export function __resetResolveState(): void {
378
430
  cachedFor = -1
379
431
  }
380
432
 
433
+ /** Test-only — current resolved-cache entry count + its hard ceiling. */
434
+ export function __resolveCacheStats(): { size: number; max: number } {
435
+ return { size: resolvedCache.size, max: MAX_RESOLVED_CACHE }
436
+ }
437
+
381
438
  export {normalizeClassName} from '../core/normalize-classname'