Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ Web runtime:

- Web keeps styles in CSS and passes `{ $$css: true, tailwind: className }` through RNW style arrays.
- `getWebStyles` uses a hidden DOM element to compute style values when a JS value is needed, such as color extraction or `useResolveClassNames`.
- `CSSListener` tracks active CSS rules and media queries, then notifies subscribers when class-dependent media rules change.
- `CSSListener` tracks active CSS rules and media queries, then notifies subscribers when class-dependent media rules change. Candidate rules are cached by class string and invalidated when stylesheets are processed or media rules are toggled; computed values and selector matching remain live.
- `ScopedTheme` renders a `div` with the theme class and `display: contents` on web.
- `LayoutDirection` renders a contents-style wrapper with `direction`/`dir` semantics so RTL/LTR variants can be scoped to a subtree.
- `ScopedVariables` renders a `display: contents` wrapper and sets its variables as inline custom properties on that wrapper, so the real DOM cascade resolves `var(--name)` to the scoped value for every descendant (numbers become px). During JS reads (`getWebVariable` / `useResolveClassNames`) it also applies the variables to the hidden `dummyParent`, then clears them.
- `ScopedVariables` renders a `display: contents` wrapper and sets its variables as inline custom properties on that wrapper, so the real DOM cascade resolves `var(--name)` to the scoped value for every descendant (numbers become px). During JS reads (`getWebVariable` / `useResolveClassNames`) it also applies the variables to the private hidden `dummyParent`. Each read compares variable values against the applied inline properties, including changes made in place to the same variables object. Unchanged values avoid writes; switching scopes removes stale properties and applies changed values.
- Dynamic CSS variable updates are written into a generated `#uniwind-dynamic-styles` style element.

Shared runtime:
Expand Down
27 changes: 27 additions & 0 deletions packages/uniwind/src/core/web/cssListener.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { StyleDependency } from '../../common/consts'
import { UniwindListener } from '../listener'

const MAX_CLASS_NAME_CACHE_SIZE = 500

class CSSListenerBuilder {
activeRules = new Set<CSSStyleRule>()
private classNameRules = new Map<string, Array<CSSStyleRule>>()
private classNameMediaQueryListeners = new Map<string, MediaQueryList>()
private listeners = new Map<MediaQueryList, Set<VoidFunction>>()
private registeredRulesMediaQueries = new Map<string, MediaQueryList>()
Expand Down Expand Up @@ -47,6 +50,28 @@ class CSSListenerBuilder {
})
}

getRulesForClassName(className: string) {
const cached = this.classNameRules.get(className)

if (cached) {
this.classNameRules.delete(className)
this.classNameRules.set(className, cached)

return cached
}

const selectors = className.split(/\s+/).filter(Boolean).map(cls => `.${CSS.escape(cls)}`)
const rules = Array.from(this.activeRules).filter(rule => selectors.some(cls => rule.selectorText.includes(cls)))

if (this.classNameRules.size >= MAX_CLASS_NAME_CACHE_SIZE) {
this.classNameRules.delete(this.classNameRules.keys().next().value!)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

this.classNameRules.set(className, rules)
Comment thread
greptile-apps[bot] marked this conversation as resolved.

return rules
}

getSnapshot(classNames: string) {
const mediaQueries = new Set(
classNames
Expand Down Expand Up @@ -122,6 +147,7 @@ class CSSListenerBuilder {
}

private initialize() {
this.classNameRules.clear()
this.pendingInitialization = undefined
this.pruneStaleRules()

Expand Down Expand Up @@ -252,6 +278,7 @@ class CSSListenerBuilder {
}

private toggleRule(mqList: MediaQueryList, rule: CSSStyleRule) {
this.classNameRules.clear()
if (mqList.matches && this.isRuleLive(rule)) {
this.activeRules.add(rule)
} else {
Expand Down
72 changes: 34 additions & 38 deletions packages/uniwind/src/core/web/getWebStyles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generateDataSet } from '../../components/web/generateDataSet'
import type { RNStyle, UniwindContextType } from '../types'
import type { CSSVariables, RNStyle, UniwindContextType } from '../types'
import { CSSListener } from './cssListener'
import { parseCSSValue, toWebValue } from './webUtils'

Expand All @@ -17,22 +17,32 @@ if (dummyParent && dummy) {
dummyParent.appendChild(dummy)
}

// Applies scoped variables to dummyParent so they cascade to dummy during style
// computation. Returns a disposer that removes them
// Keep the private probe in the current scope; only changed variables invalidate its styles.
const applyScopedVariables = (uniwindContext: UniwindContextType) => {
if (!dummyParent || uniwindContext.variables === null) {
return () => {}
if (!dummyParent) {
return
}

const names = Object.keys(uniwindContext.variables)
const variables: CSSVariables = uniwindContext.variables ?? {}
const style = dummyParent.style

Object.entries(uniwindContext.variables).forEach(([name, value]) => {
dummyParent.style.setProperty(name, toWebValue(value))
Array.from(style).forEach(name => {
if (name.startsWith('--') && !Object.prototype.hasOwnProperty.call(variables, name)) {
style.removeProperty(name)
}
})

return () => {
names.forEach(name => dummyParent.style.removeProperty(name))
}
Object.entries(variables).forEach(([name, value]) => {
if (!name.startsWith('--')) {
return
}

const next = toWebValue(value)

if (style.getPropertyValue(name) !== next) {
style.setProperty(name, next)
}
})
Comment thread
eserdeiro marked this conversation as resolved.
}

const getActiveStylesForClass = (className: string) => {
Expand All @@ -42,16 +52,10 @@ const getActiveStylesForClass = (className: string) => {
return extractedStyles
}

const classNames = className.split(/\s+/).filter(Boolean)
const computedStyles = window.getComputedStyle(dummy)

CSSListener.activeRules.forEach(rule => {
CSSListener.getRulesForClassName(className).forEach(rule => {
const selector = rule.selectorText
const mightMatch = classNames.some((cls) => selector.includes(`.${CSS.escape(cls)}`))

if (!mightMatch) {
return
}

// element.matches() throws errors if it sees pseudo-elements like ::before
// So we strip them out safely just for the matching test
Expand Down Expand Up @@ -96,13 +100,12 @@ export const getWebStyles = (
dummyParent?.removeAttribute('dir')
}

const disposeScopedVariables = applyScopedVariables(uniwindContext)
applyScopedVariables(uniwindContext)
dummy.className = className

try {
dummy.className = className

const dataSet = generateDataSet(componentProps ?? {})
const dataSet = generateDataSet(componentProps ?? {})

try {
if (dataSet) {
Object.entries(dataSet).forEach(([key, value]) => {
if (value === false || value === undefined) {
Expand All @@ -115,12 +118,6 @@ export const getWebStyles = (

const computedStyles = getActiveStylesForClass(className)

if (dataSet) {
Object.keys(dataSet).forEach(key => {
delete dummy.dataset[key]
})
}

return Object.fromEntries(
Object.entries(computedStyles)
.map(([key, value]) => {
Expand All @@ -135,7 +132,11 @@ export const getWebStyles = (
}),
)
} finally {
disposeScopedVariables()
if (dataSet) {
Object.keys(dataSet).forEach(key => {
delete dummy.dataset[key]
})
}
}
}

Expand All @@ -156,13 +157,8 @@ export const getWebVariable = (name: string, uniwindContext: UniwindContextType)
dummyParent.removeAttribute('dir')
}

const disposeScopedVariables = applyScopedVariables(uniwindContext)
applyScopedVariables(uniwindContext)
const variable = window.getComputedStyle(dummyParent).getPropertyValue(name)

try {
const variable = window.getComputedStyle(dummyParent).getPropertyValue(name)

return parseCSSValue(variable)
} finally {
disposeScopedVariables()
}
return parseCSSValue(variable)
}