Skip to content
Merged
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
19 changes: 19 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Exclude HTML
*.html linguist-detectable=false
*.htm linguist-detectable=false

# Exclude Kotlin
*.kt linguist-detectable=false
*.kts linguist-detectable=false

# Exclude JavaScript
*.js linguist-detectable=false
*.jsx linguist-detectable=false
*.mjs linguist-detectable=false
*.cjs linguist-detectable=false

# Exclude Ruby
*.rb linguist-detectable=false

# Exclude Swift
*.swift linguist-detectable=false
70 changes: 57 additions & 13 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/uniwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"@tailwindcss/node": "4.1.17",
"@tailwindcss/oxide": "4.1.17",
"culori": "4.0.2",
"lightningcss": "1.30.2"
"lightningcss": "1.30.1"
},
"peerDependencies": {
"react": ">=19.0.0",
Expand Down
17 changes: 17 additions & 0 deletions packages/uniwind/src/components/ScopedTheme/ScopedTheme.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { useMemo } from 'react'
import { UniwindContext } from '../../core/context'
import { ThemeName, UniwindContextType } from '../../core/types'

type ScopedThemeProps = {
theme: ThemeName
}

export const ScopedTheme: React.FC<React.PropsWithChildren<ScopedThemeProps>> = ({ theme, children }) => {
const value = useMemo<UniwindContextType>(() => ({ scopedTheme: theme }), [theme])

return (
<UniwindContext.Provider value={value}>
{children}
</UniwindContext.Provider>
)
}
19 changes: 19 additions & 0 deletions packages/uniwind/src/components/ScopedTheme/ScopedTheme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { useMemo } from 'react'
import { UniwindContext } from '../../core/context'
import type { ThemeName, UniwindContextType } from '../../core/types'

type ScopedThemeProps = {
theme: ThemeName
}

export const ScopedTheme: React.FC<React.PropsWithChildren<ScopedThemeProps>> = ({ theme, children }) => {
const value = useMemo<UniwindContextType>(() => ({ scopedTheme: theme }), [theme])

return (
<UniwindContext.Provider value={value}>
<div className={theme} style={{ display: 'contents' }}>
{children}
</div>
</UniwindContext.Provider>
)
}
1 change: 1 addition & 0 deletions packages/uniwind/src/components/ScopedTheme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ScopedTheme'
3 changes: 3 additions & 0 deletions packages/uniwind/src/components/native/Pressable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Pressable as RNPressable, PressableProps } from 'react-native'
import { useUniwindContext } from '../../core/context'
import { UniwindStore } from '../../core/native'
import { copyComponentProperties } from '../utils'
import { useStyle } from './useStyle'
Expand All @@ -11,6 +12,7 @@ export const Pressable = copyComponentProperties(RNPressable, (props: PressableP
isDisabled: Boolean(props.disabled),
},
)
const uniwindContext = useUniwindContext()

return (
<RNPressable
Expand All @@ -22,6 +24,7 @@ export const Pressable = copyComponentProperties(RNPressable, (props: PressableP
props.className,
props,
{ isDisabled: Boolean(props.disabled), isPressed: true },
uniwindContext,
).styles,
typeof props.style === 'function' ? props.style(state) : props.style,
]
Expand Down
4 changes: 3 additions & 1 deletion packages/uniwind/src/components/native/useStyle.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useLayoutEffect, useReducer } from 'react'
import { useUniwindContext } from '../../core/context'
import { UniwindListener } from '../../core/listener'
import { UniwindStore } from '../../core/native'
import { ComponentState } from '../../core/types'

export const useStyle = (className: string | undefined, componentProps: Record<string, any>, state?: ComponentState) => {
'use no memo'
const uniwindContext = useUniwindContext()
const [_, rerender] = useReducer(() => ({}), {})
const styleState = UniwindStore.getStyles(className, componentProps, state)
const styleState = UniwindStore.getStyles(className, componentProps, state, uniwindContext)

useLayoutEffect(() => {
if (__DEV__ || styleState.dependencies.length > 0) {
Expand Down
20 changes: 4 additions & 16 deletions packages/uniwind/src/core/config/config.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,12 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase {
return varValue
}

const value = getValue()
const runtimeThemeVariables = UniwindStore.runtimeThemeVariables.get(theme) ?? {}

if (theme === this.currentTheme) {
Object.defineProperty(UniwindStore.vars, varName, {
configurable: true,
enumerable: true,
get: () => value,
})
}

Object.defineProperty(runtimeThemeVariables, varName, {
UniwindStore.vars[theme] ??= {}
Object.defineProperty(UniwindStore.vars[theme], varName, {
configurable: true,
enumerable: true,
get: () => value,
get: getValue,
})
UniwindStore.runtimeThemeVariables.set(theme, runtimeThemeVariables)
})

if (theme === this.currentTheme) {
Expand All @@ -70,12 +59,11 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase {

protected __reinit(generateStyleSheetCallback: GenerateStyleSheetsCallback, themes: Array<string>) {
super.__reinit(generateStyleSheetCallback, themes)
UniwindStore.reinit(generateStyleSheetCallback)
UniwindStore.reinit(generateStyleSheetCallback, themes)
}

protected onThemeChange() {
UniwindStore.runtime.currentThemeName = this.currentTheme
UniwindStore.reinit()
}
}

Expand Down
10 changes: 10 additions & 0 deletions packages/uniwind/src/core/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createContext, useContext } from 'react'
import type { ThemeName } from './types'

export const UniwindContext = createContext({
scopedTheme: null as ThemeName | null,
})

export const useUniwindContext = () => useContext(UniwindContext)

UniwindContext.displayName = 'UniwindContext'
97 changes: 56 additions & 41 deletions packages/uniwind/src/core/native/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Dimensions, Platform } from 'react-native'
import { Orientation, StyleDependency } from '../../types'
import { UniwindListener } from '../listener'
import { ComponentState, CSSVariables, GenerateStyleSheetsCallback, RNStyle, Style, StyleSheets, ThemeName } from '../types'
import { ComponentState, GenerateStyleSheetsCallback, RNStyle, Style, StyleSheets, ThemeName, UniwindContextType } from '../types'
import { cloneWithAccessors } from './native-utils'
import { parseBoxShadow, parseFontVariant, parseTextShadowMutation, parseTransformsMutation, resolveGradient } from './parsers'
import { UniwindRuntime } from './runtime'
Expand All @@ -17,30 +17,39 @@ const emptyState: StylesResult = { styles: {}, dependencies: [], dependencySum:

class UniwindStoreBuilder {
runtime = UniwindRuntime
vars = {} as Record<string, unknown>
runtimeThemeVariables = new Map<ThemeName, CSSVariables>()
vars = {} as Record<ThemeName, Record<string, unknown>>
private stylesheet = {} as StyleSheets
private cache = new Map<string, StylesResult>()
private generateStyleSheetCallbackResult: ReturnType<GenerateStyleSheetsCallback> | null = null

getStyles(className: string | undefined, componentProps?: Record<string, any>, state?: ComponentState): StylesResult {
private cache = {} as Record<ThemeName, Map<string, StylesResult>>

getStyles(
className: string | undefined,
componentProps: Record<string, any> | undefined,
state: ComponentState | undefined,
uniwindContext: UniwindContextType,
): StylesResult {
if (className === undefined || className === '') {
return emptyState
}

const cacheKey = `${className}${state?.isDisabled ?? false}${state?.isFocused ?? false}${state?.isPressed ?? false}`
const isScopedTheme = uniwindContext.scopedTheme !== null
const cacheKey = `${className}${state?.isDisabled ?? false}${state?.isFocused ?? false}${state?.isPressed ?? false}${isScopedTheme}`
const cache = this.cache[uniwindContext.scopedTheme ?? this.runtime.currentThemeName]

if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!
if (!cache) {
return emptyState
}

const result = this.resolveStyles(className, componentProps, state)
if (cache.has(cacheKey)) {
return cache.get(cacheKey)!
}

const result = this.resolveStyles(className, componentProps, state, uniwindContext)

// Don't cache styles that depend on data attributes
if (!result.hasDataAttributes) {
this.cache.set(cacheKey, result)
cache.set(cacheKey, result)
UniwindListener.subscribe(
() => this.cache.delete(cacheKey),
() => cache.delete(cacheKey),
result.dependencies,
{ once: true },
)
Expand All @@ -49,47 +58,49 @@ class UniwindStoreBuilder {
return result
}

reinit = (generateStyleSheetCallback?: GenerateStyleSheetsCallback) => {
const config = generateStyleSheetCallback?.(this.runtime) ?? this.generateStyleSheetCallbackResult

if (!config) {
return
}

reinit = (generateStyleSheetCallback: GenerateStyleSheetsCallback, themes: Array<string>) => {
const config = generateStyleSheetCallback(this.runtime)
const { scopedVars, stylesheet, vars } = config

this.generateStyleSheetCallbackResult = config
this.stylesheet = stylesheet
this.vars = vars

const themeVars = scopedVars[`__uniwind-theme-${this.runtime.currentThemeName}`]
const platformVars = scopedVars[`__uniwind-platform-${Platform.OS}`]
const runtimeThemeVars = this.runtimeThemeVariables.get(this.runtime.currentThemeName)

if (themeVars) {
Object.defineProperties(this.vars, Object.getOwnPropertyDescriptors(themeVars))
}

if (platformVars) {
Object.defineProperties(this.vars, Object.getOwnPropertyDescriptors(platformVars))
Object.defineProperties(vars, Object.getOwnPropertyDescriptors(platformVars))
}

if (runtimeThemeVars) {
Object.defineProperties(this.vars, Object.getOwnPropertyDescriptors(runtimeThemeVars))
}
this.stylesheet = stylesheet
this.vars = Object.fromEntries(themes.map(theme => {
const clonedVars = cloneWithAccessors(vars)
const themeVars = scopedVars[`__uniwind-theme-${theme}`]

if (__DEV__ && generateStyleSheetCallback) {
if (themeVars) {
Object.defineProperties(clonedVars, Object.getOwnPropertyDescriptors(themeVars))
}

return [theme, clonedVars]
}))
this.cache = Object.fromEntries(themes.map(theme => [theme, new Map()]))

if (__DEV__) {
UniwindListener.notifyAll()
}
}

private resolveStyles(classNames: string, componentProps?: Record<string, any>, state?: ComponentState) {
private resolveStyles(
classNames: string,
componentProps: Record<string, any> | undefined,
state: ComponentState | undefined,
uniwindContext: UniwindContextType,
) {
const result = {} as Record<string, any>
let vars = this.vars
const theme = uniwindContext.scopedTheme ?? this.runtime.currentThemeName
// At this point we're sure that theme is correct
let vars = this.vars[theme]!
const originalVars = vars
let hasDataAttributes = false
const dependencies = new Set<StyleDependency>()
let dependencySum = 0
const bestBreakpoints = new Map<string, Style>()
const isScopedTheme = uniwindContext.scopedTheme !== null

for (const className of classNames.split(' ')) {
if (!(className in this.stylesheet)) {
Expand All @@ -99,6 +110,10 @@ class UniwindStoreBuilder {
for (const style of this.stylesheet[className] as Array<Style>) {
if (style.dependencies) {
style.dependencies.forEach(dep => {
if (dep === StyleDependency.Theme && isScopedTheme) {
return
}

dependencies.add(dep)
dependencySum |= 1 << dep
})
Expand All @@ -111,7 +126,7 @@ class UniwindStoreBuilder {
if (
style.minWidth > this.runtime.screen.width
|| style.maxWidth < this.runtime.screen.width
|| (style.theme !== null && this.runtime.currentThemeName !== style.theme)
|| (style.theme !== null && theme !== style.theme)
|| (style.orientation !== null && this.runtime.orientation !== style.orientation)
|| (style.rtl !== null && this.runtime.rtl !== style.rtl)
|| (style.active !== null && state?.isPressed !== style.active)
Expand All @@ -138,8 +153,8 @@ class UniwindStoreBuilder {

if (property[0] === '-') {
// Clone vars object if we are adding inline variables
if (vars === this.vars) {
vars = cloneWithAccessors(this.vars)
if (vars === originalVars) {
vars = cloneWithAccessors(originalVars)
}

Object.defineProperty(vars, property, {
Expand Down
4 changes: 4 additions & 0 deletions packages/uniwind/src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react'
import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native'
import { ColorScheme, Orientation, StyleDependency, UniwindConfig } from '../types'
import type { UniwindContext } from './context'

export type Style = {
entries: Array<[string, () => unknown]>
Expand Down Expand Up @@ -92,3 +94,5 @@ export type ComponentState = {
}

export type CSSVariables = Record<string, string | number>

export type UniwindContextType = React.ContextType<typeof UniwindContext>
Loading