From 97acf372fa09dfd35b3227552fc57186533a64d9 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Mon, 16 Feb 2026 12:27:54 +0100 Subject: [PATCH 01/13] feat: scoped theme --- .../ScopedTheme/ScopedTheme.native.tsx | 15 +++++++++++++++ .../src/components/ScopedTheme/ScopedTheme.tsx | 17 +++++++++++++++++ .../uniwind/src/components/ScopedTheme/index.ts | 1 + packages/uniwind/src/core/context.ts | 6 ++++++ packages/uniwind/src/index.ts | 1 + 5 files changed, 40 insertions(+) create mode 100644 packages/uniwind/src/components/ScopedTheme/ScopedTheme.native.tsx create mode 100644 packages/uniwind/src/components/ScopedTheme/ScopedTheme.tsx create mode 100644 packages/uniwind/src/components/ScopedTheme/index.ts create mode 100644 packages/uniwind/src/core/context.ts diff --git a/packages/uniwind/src/components/ScopedTheme/ScopedTheme.native.tsx b/packages/uniwind/src/components/ScopedTheme/ScopedTheme.native.tsx new file mode 100644 index 00000000..caa3ff73 --- /dev/null +++ b/packages/uniwind/src/components/ScopedTheme/ScopedTheme.native.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { UniwindContext } from '../../core/context' +import { ThemeName } from '../../core/types' + +type ScopedThemeProps = { + theme: ThemeName +} + +export const ScopedTheme: React.FC> = ({ theme, children }) => { + return ( + + {children} + + ) +} diff --git a/packages/uniwind/src/components/ScopedTheme/ScopedTheme.tsx b/packages/uniwind/src/components/ScopedTheme/ScopedTheme.tsx new file mode 100644 index 00000000..9aac523a --- /dev/null +++ b/packages/uniwind/src/components/ScopedTheme/ScopedTheme.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { UniwindContext } from '../../core/context' +import { ThemeName } from '../../core/types' + +type ScopedThemeProps = { + theme: ThemeName +} + +export const ScopedTheme: React.FC> = ({ theme, children }) => { + return ( + +
+ {children} +
+
+ ) +} diff --git a/packages/uniwind/src/components/ScopedTheme/index.ts b/packages/uniwind/src/components/ScopedTheme/index.ts new file mode 100644 index 00000000..9c9e340b --- /dev/null +++ b/packages/uniwind/src/components/ScopedTheme/index.ts @@ -0,0 +1 @@ +export * from './ScopedTheme' diff --git a/packages/uniwind/src/core/context.ts b/packages/uniwind/src/core/context.ts new file mode 100644 index 00000000..3d234b9b --- /dev/null +++ b/packages/uniwind/src/core/context.ts @@ -0,0 +1,6 @@ +import { createContext } from 'react' +import { ThemeName } from './types' + +export const UniwindContext = createContext({ + scopedTheme: null as ThemeName | null, +}) diff --git a/packages/uniwind/src/index.ts b/packages/uniwind/src/index.ts index f57b0c1c..daeba02c 100644 --- a/packages/uniwind/src/index.ts +++ b/packages/uniwind/src/index.ts @@ -1,3 +1,4 @@ +export * from './components/ScopedTheme' export { Uniwind } from './core' export { withUniwind } from './hoc' export type { ApplyUniwind, ApplyUniwindOptions } from './hoc/types' From d61a52784dc71bad412a6da69ca01f984afe2648 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Tue, 17 Feb 2026 07:41:55 +0100 Subject: [PATCH 02/13] chore: move variant variables to the scope --- packages/uniwind/src/metro/polyfillWeb.ts | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/packages/uniwind/src/metro/polyfillWeb.ts b/packages/uniwind/src/metro/polyfillWeb.ts index 82a04271..2ddc6ceb 100644 --- a/packages/uniwind/src/metro/polyfillWeb.ts +++ b/packages/uniwind/src/metro/polyfillWeb.ts @@ -7,6 +7,73 @@ export const polyfillWeb = (css: string) => { filename: 'uniwind.css', visitor: { Function: processFunctions, + Rule: { + 'layer-block': layer => { + if (layer.value.name?.at(0) !== 'theme') { + return + } + + const firstRule = layer.value.rules.at(0) + + if (firstRule?.type !== 'style') { + return + } + + const firstSelector = firstRule.value.selectors.at(0)?.at(0) + + if (firstSelector?.type !== 'pseudo-class' || firstSelector.kind !== 'root') { + return + } + + const firstNestedRule = firstRule.value.rules?.at(0) + + if (firstNestedRule?.type !== 'style') { + return + } + + const firstNestedSelector = firstNestedRule.value.selectors.at(0)?.at(0) + + if (firstNestedSelector?.type !== 'nesting') { + return + } + + firstRule.value.rules?.forEach((rule) => { + if (rule.type !== 'style') { + return + } + + const variantSelector = rule.value.selectors.at(0)?.at(1) + + if (variantSelector?.type !== 'pseudo-class' || variantSelector.kind !== 'where') { + return + } + + const variant = variantSelector.selectors.at(0)?.at(0) + + if (variant?.type !== 'class') { + return + } + + layer.value.rules.push({ + type: 'scope', + value: { + scopeStart: [[variant]], + loc: { column: 0, line: 0, source_index: 0 }, + rules: [{ + type: 'style', + value: { + loc: { column: 0, line: 0, source_index: 0 }, + selectors: [[{ type: 'pseudo-class', kind: 'scope' }]], + declarations: rule.value.declarations, + }, + }], + }, + }) + }) + + return layer + }, + }, }, }) From 5733bfefc1bf0107ccf9e2638b479127e1600add Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Wed, 18 Feb 2026 11:46:10 +0100 Subject: [PATCH 03/13] feat: move variant selectors to scope --- apps/expo-example/App.tsx | 658 +------------------ bun.lock | 70 +- packages/uniwind/package.json | 2 +- packages/uniwind/src/metro/compileVirtual.ts | 2 +- packages/uniwind/src/metro/polyfillWeb.ts | 71 +- 5 files changed, 87 insertions(+), 716 deletions(-) diff --git a/apps/expo-example/App.tsx b/apps/expo-example/App.tsx index fb29cfa2..ab77189f 100644 --- a/apps/expo-example/App.tsx +++ b/apps/expo-example/App.tsx @@ -1,655 +1,21 @@ import './global.css' import React from 'react' import { ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native' +import { ScopedTheme } from 'uniwind' const TailwindTestPage = () => { return ( - - {/* Layout & Flexbox */} - - Layout & Flexbox - - - - - - - - - - - - - - - - - - - - - - {/* Spacing */} - - Spacing - - - - p-1 - - - p-2 - - - p-4 - - - p-8 - - - - - - m-1 - - - m-2 - - - m-4 - - - - - - pt-4 pb-2 pl-6 pr-8 - - - - - {/* Sizing */} - - Sizing - - - - - - - - - - - - - - - - - - min-w-0 min-h-0 max-w-xs max-h-24 - - - - - {/* Typography */} - - Typography - - - text-xs: The quick brown fox - text-sm: The quick brown fox - text-base: The quick brown fox - text-lg: The quick brown fox - text-xl: The quick brown fox - text-2xl: The quick brown fox - text-3xl: The quick brown fox - - - - font-thin: Thin weight - font-light: Light weight - font-normal: Normal weight - font-medium: Medium weight - font-semibold: Semibold weight - font-bold: Bold weight - font-extrabold: Extra bold weight - - - - text-left: Left aligned text - text-center: Center aligned text - text-right: Right aligned text - - text-justify: Justified text that should wrap to multiple lines to demonstrate the justify alignment behavior in React Native. - - - - - italic: Italic text style - not-italic: Not italic text style - underline: Underlined text - line-through: Line through text - no-underline: No underline text - - - - uppercase: uppercase text - LOWERCASE: lowercase text - capitalize: capitalize text - normal-case: Normal case text - - - - - leading-none: Line height none. This text should have tight line spacing when it wraps to multiple lines. - - - leading-tight: Line height tight. This text should have tight line spacing when it wraps to multiple lines. - - - leading-normal: Line height normal. This text should have normal line spacing when it wraps to multiple lines. - - - leading-relaxed: Line height relaxed. This text should have relaxed line spacing when it wraps to multiple lines. - - - leading-loose: Line height loose. This text should have loose line spacing when it wraps to multiple lines. - - - - - {/* Colors */} - - Colors - - - text-red-500 - text-blue-500 - text-green-500 - text-yellow-500 - text-purple-500 - text-pink-500 - text-indigo-500 - text-gray-500 - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* Borders */} - - Borders - - - - border border-gray-300 - - - border-2 border-red-500 - - - border-4 border-blue-500 - - - border-8 border-green-500 - - - - - - border-t-4 border-red-500 - - - border-r-4 border-blue-500 - - - border-b-4 border-green-500 - - - border-l-4 border-yellow-500 - - - - - - rounded-none - - - rounded-sm - - - rounded - - - rounded-md - - - rounded-lg - - - rounded-xl - - - rounded-2xl - - - rounded-3xl - - - rounded-full - - - - - - rounded-tl-lg - - - rounded-tr-lg - - - rounded-bl-lg - - - rounded-br-lg - - - - - {/* Effects */} - - Effects - - - - opacity-100 - - - opacity-75 - - - opacity-50 - - - opacity-25 - - - opacity-0 (invisible) - - - - - {/* Positioning */} - - Positioning - - - - top-0 left-0 - - - top-0 right-0 - - - bottom-0 left-0 - - - bottom-0 right-0 - - - centered - - - - - - z-10 (higher) - - - z-0 (lower) - - - - - {/* Interactive Elements */} - - Interactive Elements - - - - TouchableOpacity Button - - - - Green Button - - - - Rounded Button - - - - Outlined Button - - - - - - - - - - - - {/* Transform */} - - Transform - - - - rotate-12 - - - -rotate-12 - - - rotate-45 - - - - - - scale-75 - - - scale-110 - - - scale-125 - - - - - - translate-x-4 - - - translate-y-4 - - - translate-x-4 translate-y-4 - - - - - - translate-10 rotate-12 scale-125 - - - - - {/* Images */} - - Images - - - - w-full h-48 (Image placeholder) - - - w-32 h-32 rounded-full - - - w-24 h-24 rounded-lg - - - - - {/* Overflow */} - - Overflow - - - - - overflow-hidden: This is a very long text that should be clipped by the container because it exceeds the width and height - limits. - - - - overflow-visible: This text might overflow the container boundaries. - - - - - {/* Shadows */} - - Shadows - - - - shadow-sm - - - shadow (default) - - - shadow-md - - - shadow-lg - - - shadow-xl - - - shadow-2xl - - - shadow-none - - - - {/* Colored Shadows */} - - Colored Shadows - - shadow-lg shadow-red-500/50 - - - shadow-lg shadow-blue-500/50 - - - shadow-lg shadow-green-500/50 - - - shadow-lg shadow-purple-500/50 - - - shadow-xl shadow-black/25 - - - - {/* Shadow with different elements */} - - Shadows on Different Elements - - - Button with shadow-lg - - - - Card with shadow-xl and border - This looks like a material design card - - - - - {/* Ring System */} - - Ring System - - - Ring Widths - - ring-1 ring-gray-300 - - - ring-2 ring-gray-400 - - - ring-4 ring-gray-500 - - - ring-8 ring-gray-600 - - - ring (default) ring-gray-500 - - - - - Ring Colors - - ring-4 ring-red-500 - - - ring-4 ring-blue-500 - - - ring-4 ring-green-500 - - - ring-4 ring-yellow-500 - - - ring-4 ring-purple-500 - - - ring-4 ring-pink-500 - - - - - Ring Offset - - ring-offset-1 ring-offset-white - - - ring-offset-2 ring-offset-white - - - ring-offset-4 ring-offset-white - - - ring-offset-8 ring-offset-white - - - - - Ring Offset Colors - - ring-offset-gray-100 - - - ring-offset-red-200 - - - ring-offset-yellow-200 - - - - {/* Ring on Interactive Elements */} - - Ring on Interactive Elements - - - Button with Ring - - - - - - Outlined Button with Ring - - - - Card with Ring Border - This creates a nice focus or selection state - - - - {/* Ring Inset */} - - Ring Inset - - ring-inset ring-blue-500 - - - ring-inset ring-red-500 - - - ring-8 ring-inset ring-green-500 - - - - - {/* Gradients */} - - Gradients - - - Linear to bottom - - Linear to right 3 colors - - Linear to bottom left - - Linear 150 deg - - Linear custom multiple colors - - - - + + + + + + + + + + + ) } diff --git a/bun.lock b/bun.lock index 671bb1ed..f5e4b23c 100644 --- a/bun.lock +++ b/bun.lock @@ -126,12 +126,12 @@ }, "packages/uniwind": { "name": "uniwind", - "version": "1.3.0", + "version": "1.3.1", "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "culori": "4.0.2", - "lightningcss": "1.30.2", + "lightningcss": "1.30.1", }, "devDependencies": { "@react-native/babel-preset": "0.83.0", @@ -1896,29 +1896,29 @@ "lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], - "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -2822,6 +2822,8 @@ "@expo/metro-config/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], + "@expo/metro-config/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + "@expo/metro-config/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@expo/metro-config/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], @@ -2924,6 +2926,8 @@ "@react-navigation/native-stack/@react-navigation/elements": ["@react-navigation/elements@2.8.0", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-uvSOkYOF7wfgkt57cl+6fZ2vQgTiYYyJleZzuWthPKHK4nDq2M4sc9SSzgK9GS9UCJFRBErNtl3S+/ErtrwREw=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="], @@ -3312,6 +3316,26 @@ "@expo/metro-config/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], + "@expo/metro-config/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "@expo/metro-config/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "@expo/metro-config/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "@expo/metro-config/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "@expo/metro-config/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "@expo/metro-config/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "@expo/metro-config/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "@expo/metro-config/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "@expo/metro-config/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "@expo/metro-config/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "@expo/metro-config/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@expo/metro/metro/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], @@ -3404,6 +3428,26 @@ "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "@testing-library/dom/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], diff --git a/packages/uniwind/package.json b/packages/uniwind/package.json index a48b2bb0..468983ab 100644 --- a/packages/uniwind/package.json +++ b/packages/uniwind/package.json @@ -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", diff --git a/packages/uniwind/src/metro/compileVirtual.ts b/packages/uniwind/src/metro/compileVirtual.ts index 2947a413..d69e0c94 100644 --- a/packages/uniwind/src/metro/compileVirtual.ts +++ b/packages/uniwind/src/metro/compileVirtual.ts @@ -36,7 +36,7 @@ export const compileVirtual = async ({ css, cssPath, platform, themes, polyfills const tailwindCSS = compiler.build(candidates ?? scanner.scan()) if (platform === Platform.Web) { - return polyfillWeb(tailwindCSS) + return polyfillWeb(tailwindCSS, themes) } const Processor = new ProcessorBuilder(themes, polyfills) diff --git a/packages/uniwind/src/metro/polyfillWeb.ts b/packages/uniwind/src/metro/polyfillWeb.ts index 2ddc6ceb..bda1ba4f 100644 --- a/packages/uniwind/src/metro/polyfillWeb.ts +++ b/packages/uniwind/src/metro/polyfillWeb.ts @@ -1,77 +1,38 @@ import { transform } from 'lightningcss' import { processFunctions } from '../css/processFunctions' -export const polyfillWeb = (css: string) => { +export const polyfillWeb = (css: string, themes: Array) => { + const processedClassNames = new Set() + const result = transform({ code: Buffer.from(css), filename: 'uniwind.css', visitor: { Function: processFunctions, Rule: { - 'layer-block': layer => { - if (layer.value.name?.at(0) !== 'theme') { - return - } - - const firstRule = layer.value.rules.at(0) - - if (firstRule?.type !== 'style') { - return - } - - const firstSelector = firstRule.value.selectors.at(0)?.at(0) + style: styleRule => { + const firstSelector = styleRule.value.selectors.at(0)?.at(0) - if (firstSelector?.type !== 'pseudo-class' || firstSelector.kind !== 'root') { + if (firstSelector?.type !== 'class') { return } - const firstNestedRule = firstRule.value.rules?.at(0) + const selectedVariant = themes.find(theme => firstSelector.name.includes(`${theme}:`)) - if (firstNestedRule?.type !== 'style') { + if (selectedVariant === undefined || processedClassNames.has(selectedVariant)) { return } - const firstNestedSelector = firstNestedRule.value.selectors.at(0)?.at(0) + processedClassNames.add(selectedVariant) - if (firstNestedSelector?.type !== 'nesting') { - return + return { + type: 'scope', + value: { + loc: styleRule.value.loc, + scopeStart: [[{ type: 'class', name: selectedVariant }]], + rules: [styleRule], + }, } - - firstRule.value.rules?.forEach((rule) => { - if (rule.type !== 'style') { - return - } - - const variantSelector = rule.value.selectors.at(0)?.at(1) - - if (variantSelector?.type !== 'pseudo-class' || variantSelector.kind !== 'where') { - return - } - - const variant = variantSelector.selectors.at(0)?.at(0) - - if (variant?.type !== 'class') { - return - } - - layer.value.rules.push({ - type: 'scope', - value: { - scopeStart: [[variant]], - loc: { column: 0, line: 0, source_index: 0 }, - rules: [{ - type: 'style', - value: { - loc: { column: 0, line: 0, source_index: 0 }, - selectors: [[{ type: 'pseudo-class', kind: 'scope' }]], - declarations: rule.value.declarations, - }, - }], - }, - }) - }) - - return layer }, }, }, From f1e8751576ca6fe94e19c15666048761e86b6cec Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Thu, 19 Feb 2026 11:12:48 +0100 Subject: [PATCH 04/13] feat: web visitors to support scoped themes --- apps/expo-example/App.tsx | 45 ++++++++-- .../function-visitor.ts} | 24 +++-- packages/uniwind/src/css-visitor/index.ts | 1 + .../uniwind/src/css-visitor/rule-visitor.ts | 90 +++++++++++++++++++ packages/uniwind/src/css-visitor/visitor.ts | 13 +++ packages/uniwind/src/metro/compileVirtual.ts | 9 +- packages/uniwind/src/metro/polyfillWeb.ts | 42 --------- packages/uniwind/src/vite/vite.ts | 6 +- 8 files changed, 164 insertions(+), 66 deletions(-) rename packages/uniwind/src/{css/processFunctions.ts => css-visitor/function-visitor.ts} (69%) create mode 100644 packages/uniwind/src/css-visitor/index.ts create mode 100644 packages/uniwind/src/css-visitor/rule-visitor.ts create mode 100644 packages/uniwind/src/css-visitor/visitor.ts delete mode 100644 packages/uniwind/src/metro/polyfillWeb.ts diff --git a/apps/expo-example/App.tsx b/apps/expo-example/App.tsx index ab77189f..ddb1253d 100644 --- a/apps/expo-example/App.tsx +++ b/apps/expo-example/App.tsx @@ -1,22 +1,49 @@ import './global.css' import React from 'react' -import { ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native' +import { Text, View } from 'react-native' import { ScopedTheme } from 'uniwind' -const TailwindTestPage = () => { +const App = () => { return ( - + + ScopedTheme Nesting + + {/* Root: Light */} - + + Light Theme + + {/* Nested: Dark */} + + + Nested Dark Theme + + {/* Nested: Premium */} + + + Nested Premium Theme + + + + + + + {/* Parallel Root: Dark */} - - - - + + Dark Theme Root + + {/* Nested: Light */} + + + Nested Light Theme + + + ) } -export default TailwindTestPage +export default App diff --git a/packages/uniwind/src/css/processFunctions.ts b/packages/uniwind/src/css-visitor/function-visitor.ts similarity index 69% rename from packages/uniwind/src/css/processFunctions.ts rename to packages/uniwind/src/css-visitor/function-visitor.ts index 8bfbd0e5..b96409d2 100644 --- a/packages/uniwind/src/css/processFunctions.ts +++ b/packages/uniwind/src/css-visitor/function-visitor.ts @@ -1,12 +1,14 @@ -import { CustomAtRules, TokenOrValue, Visitor } from 'lightningcss' +import { Function as LightningCSSFunction, TokenOrValue } from 'lightningcss' const ONE_PX = { type: 'token', value: { type: 'dimension', unit: 'px', value: 1 }, } satisfies TokenOrValue -export const processFunctions: Visitor['Function'] = { - pixelRatio: (fn) => { +export class FunctionVisitor { + [name: string]: (fn: LightningCSSFunction) => TokenOrValue + + pixelRatio(fn: LightningCSSFunction): TokenOrValue { return { type: 'function', value: { @@ -17,9 +19,10 @@ export const processFunctions: Visitor['Function'] = { ONE_PX, ], }, - } satisfies TokenOrValue - }, - fontScale: (fn) => { + } + } + + fontScale(fn: LightningCSSFunction): TokenOrValue { return { type: 'function', value: { @@ -33,7 +36,10 @@ export const processFunctions: Visitor['Function'] = { }, ], }, - } satisfies TokenOrValue - }, - hairlineWidth: () => ONE_PX, + } + } + + hairlineWidth(): TokenOrValue { + return ONE_PX + } } diff --git a/packages/uniwind/src/css-visitor/index.ts b/packages/uniwind/src/css-visitor/index.ts new file mode 100644 index 00000000..6d161302 --- /dev/null +++ b/packages/uniwind/src/css-visitor/index.ts @@ -0,0 +1 @@ +export * from './visitor' diff --git a/packages/uniwind/src/css-visitor/rule-visitor.ts b/packages/uniwind/src/css-visitor/rule-visitor.ts new file mode 100644 index 00000000..c0c679da --- /dev/null +++ b/packages/uniwind/src/css-visitor/rule-visitor.ts @@ -0,0 +1,90 @@ +import { ReturnedDeclaration, ReturnedMediaQuery, ReturnedRule, Rule, SelectorComponent } from 'lightningcss' + +type LightningRuleVisitor = Rule +type LightingRuleVisitors = Partial< + { + [K in LightningRuleVisitor['type']]: (rule: Extract) => ReturnedRule | Array | void + } +> + +export class RuleVisitor implements LightingRuleVisitors { + processedClassNames = new Set() + processedVariables = new Set() + currentLayerName = '' + + constructor(private readonly themes: Array) {} + + 'layer-block' = (layer: Extract) => { + this.currentLayerName = layer.value.name?.join('') ?? '' + } + + style = (styleRule: Extract) => { + const firstSelector = styleRule.value.selectors.at(0)?.at(0) + const secondSelector = styleRule.value.selectors.at(0)?.at(1) + + if ( + this.currentLayerName === 'theme' && firstSelector?.type === 'nesting' && secondSelector?.type === 'pseudo-class' + && secondSelector.kind === 'where' + ) { + return this.processThemeStyle(styleRule, secondSelector) + } + + if (firstSelector?.type === 'class') { + return this.processClassStyle(styleRule, firstSelector) + } + } + + private processThemeStyle( + styleRule: Extract, + secondSelector: Extract, + ): ReturnedRule | void { + const whereSelector = secondSelector.selectors.at(0)?.at(0) + + if (whereSelector?.type !== 'class') { + return + } + + const selectedVariant = this.themes.find(theme => whereSelector.name === theme) + + if (selectedVariant === undefined || this.processedVariables.has(selectedVariant)) { + return + } + + this.processedVariables.add(selectedVariant) + + return { + type: 'style' as const, + value: { + loc: styleRule.value.loc, + selectors: [[{ type: 'class' as const, name: selectedVariant }]], + declarations: styleRule.value.declarations, + rules: styleRule.value.rules, + }, + } + } + + private processClassStyle( + styleRule: Extract, + firstSelector: Extract, + ): ReturnedRule | void { + const selectedVariant = this.themes.find(theme => firstSelector.name.includes(`${theme}:`)) + + if (selectedVariant === undefined || this.processedClassNames.has(firstSelector.name)) { + return + } + + this.processedClassNames.add(firstSelector.name) + + return { + type: 'scope', + value: { + loc: styleRule.value.loc, + rules: [styleRule], + scopeStart: [[{ type: 'class', name: selectedVariant }]], + scopeEnd: this.themes + .filter(theme => theme !== selectedVariant) + .map(theme => [{ type: 'class', name: theme }]), + }, + } + } +} diff --git a/packages/uniwind/src/css-visitor/visitor.ts b/packages/uniwind/src/css-visitor/visitor.ts new file mode 100644 index 00000000..3fdcec79 --- /dev/null +++ b/packages/uniwind/src/css-visitor/visitor.ts @@ -0,0 +1,13 @@ +import { CustomAtRules, Visitor } from 'lightningcss' +import { FunctionVisitor } from './function-visitor' +import { RuleVisitor } from './rule-visitor' + +export class UniwindCSSVisitor implements Visitor { + Function: Visitor['Function'] + Rule: Visitor['Rule'] + + constructor(private readonly themes: Array) { + this.Function = new FunctionVisitor() + this.Rule = new RuleVisitor(this.themes) + } +} diff --git a/packages/uniwind/src/metro/compileVirtual.ts b/packages/uniwind/src/metro/compileVirtual.ts index d69e0c94..1da5a7eb 100644 --- a/packages/uniwind/src/metro/compileVirtual.ts +++ b/packages/uniwind/src/metro/compileVirtual.ts @@ -1,9 +1,10 @@ import { compile } from '@tailwindcss/node' import { Scanner } from '@tailwindcss/oxide' +import { transform } from 'lightningcss' import path from 'path' +import { UniwindCSSVisitor } from '../css-visitor' import { addMetaToStylesTemplate } from './addMetaToStylesTemplate' import { Logger } from './logger' -import { polyfillWeb } from './polyfillWeb' import { ProcessorBuilder } from './processor' import { Platform, Polyfills } from './types' import { serializeJSObject } from './utils' @@ -36,7 +37,11 @@ export const compileVirtual = async ({ css, cssPath, platform, themes, polyfills const tailwindCSS = compiler.build(candidates ?? scanner.scan()) if (platform === Platform.Web) { - return polyfillWeb(tailwindCSS, themes) + return transform({ + code: Buffer.from(tailwindCSS), + filename: 'uniwind.css', + visitor: new UniwindCSSVisitor(themes), + }).code.toString() } const Processor = new ProcessorBuilder(themes, polyfills) diff --git a/packages/uniwind/src/metro/polyfillWeb.ts b/packages/uniwind/src/metro/polyfillWeb.ts deleted file mode 100644 index bda1ba4f..00000000 --- a/packages/uniwind/src/metro/polyfillWeb.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { transform } from 'lightningcss' -import { processFunctions } from '../css/processFunctions' - -export const polyfillWeb = (css: string, themes: Array) => { - const processedClassNames = new Set() - - const result = transform({ - code: Buffer.from(css), - filename: 'uniwind.css', - visitor: { - Function: processFunctions, - Rule: { - style: styleRule => { - const firstSelector = styleRule.value.selectors.at(0)?.at(0) - - if (firstSelector?.type !== 'class') { - return - } - - const selectedVariant = themes.find(theme => firstSelector.name.includes(`${theme}:`)) - - if (selectedVariant === undefined || processedClassNames.has(selectedVariant)) { - return - } - - processedClassNames.add(selectedVariant) - - return { - type: 'scope', - value: { - loc: styleRule.value.loc, - scopeStart: [[{ type: 'class', name: selectedVariant }]], - rules: [styleRule], - }, - } - }, - }, - }, - }) - - return result.code.toString() -} diff --git a/packages/uniwind/src/vite/vite.ts b/packages/uniwind/src/vite/vite.ts index f1da16ad..31267b8f 100644 --- a/packages/uniwind/src/vite/vite.ts +++ b/packages/uniwind/src/vite/vite.ts @@ -3,7 +3,7 @@ import path from 'path' import type { Plugin } from 'vite' import { name as UNIWIND_PACKAGE_NAME } from '../../package.json' import { buildCSS } from '../css' -import { processFunctions } from '../css/processFunctions' +import { UniwindCSSVisitor } from '../css-visitor' import { uniq } from '../metro/utils' import { buildDtsFile } from '../utils/buildDtsFile' import { stringifyThemes } from '../utils/stringifyThemes' @@ -52,9 +52,7 @@ export const uniwind = ({ css: { transformer: 'lightningcss', lightningcss: { - visitor: { - Function: processFunctions, - }, + visitor: new UniwindCSSVisitor(themes), }, }, optimizeDeps: { From b3b47e5c0bcaedf88194d0dbcae66027e553a78a Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Thu, 19 Feb 2026 11:50:15 +0100 Subject: [PATCH 05/13] feat: web scoped themes for hooks and hoc --- packages/uniwind/src/core/web/getWebStyles.ts | 35 ++++++++++++++++--- packages/uniwind/src/hoc/withUniwind.tsx | 11 ++++-- .../hooks/useCSSVariable/getVariableValue.ts | 20 ++--------- .../hooks/useCSSVariable/useCSSVariable.ts | 18 +++++----- .../uniwind/src/hooks/useResolveClassNames.ts | 8 +++-- packages/uniwind/tests/consts.ts | 7 ++++ .../tests/web/hoc/withUniwind.test.tsx | 4 +-- 7 files changed, 65 insertions(+), 38 deletions(-) diff --git a/packages/uniwind/src/core/web/getWebStyles.ts b/packages/uniwind/src/core/web/getWebStyles.ts index 91e40079..95451137 100644 --- a/packages/uniwind/src/core/web/getWebStyles.ts +++ b/packages/uniwind/src/core/web/getWebStyles.ts @@ -1,14 +1,19 @@ +import { UniwindContext } from '../../core/context' import { RNStyle } from '../types' import { parseCSSValue } from './parseCSSValue' -const dummy = typeof document !== 'undefined' +const dummyParent = typeof document !== 'undefined' ? Object.assign(document.createElement('div'), { style: 'display: none', }) : null +const dummy = typeof document !== 'undefined' + ? document.createElement('div') + : null -if (dummy) { - document.body.appendChild(dummy) +if (dummyParent && dummy) { + document.body.appendChild(dummyParent) + dummyParent.appendChild(dummy) } const getComputedStyles = () => { @@ -47,7 +52,7 @@ const getObjectDifference = (obj1: T, obj2: T): T => { return diff } -export const getWebStyles = (className?: string): RNStyle => { +export const getWebStyles = (className: string | undefined, uniwindContext: React.ContextType): RNStyle => { if (className === undefined) { return {} } @@ -56,6 +61,12 @@ export const getWebStyles = (className?: string): RNStyle => { return {} } + if (uniwindContext.scopedTheme !== null) { + dummyParent?.setAttribute('class', uniwindContext.scopedTheme) + } else { + dummyParent?.removeAttribute('class') + } + dummy.className = className const computedStyles = getObjectDifference(initialStyles, getComputedStyles()) @@ -74,3 +85,19 @@ export const getWebStyles = (className?: string): RNStyle => { }), ) } + +export const getWebVariable = (name: string, uniwindContext: React.ContextType) => { + if (!dummyParent) { + return undefined + } + + if (uniwindContext.scopedTheme !== null) { + dummyParent.setAttribute('class', uniwindContext.scopedTheme) + } else { + dummyParent.removeAttribute('class') + } + + const variable = window.getComputedStyle(dummyParent).getPropertyValue(name) + + return parseCSSValue(variable) +} diff --git a/packages/uniwind/src/hoc/withUniwind.tsx b/packages/uniwind/src/hoc/withUniwind.tsx index 676697bc..2b1d951f 100644 --- a/packages/uniwind/src/hoc/withUniwind.tsx +++ b/packages/uniwind/src/hoc/withUniwind.tsx @@ -1,4 +1,5 @@ -import { ComponentProps, useLayoutEffect, useReducer } from 'react' +import { ComponentProps, useContext, useLayoutEffect, useReducer } from 'react' +import { UniwindContext } from '../core/context' import { CSSListener, formatColor, getWebStyles } from '../core/web' import { AnyObject, Component, OptionMapping, WithUniwind } from './types' import { classToColor, classToStyle, isClassProperty, isColorClassProperty, isStyleProperty } from './withUniwindUtils' @@ -14,6 +15,8 @@ export const withUniwind: WithUniwind = < : withAutoUniwind(Component) const withAutoUniwind = (Component: Component) => (props: AnyObject) => { + const uniwindContext = useContext(UniwindContext) + const { classNames, generatedProps } = Object.entries(props).reduce((acc, [propName, propValue]) => { if (isColorClassProperty(propName)) { const colorProp = classToColor(propName) @@ -23,7 +26,7 @@ const withAutoUniwind = (Component: Component) => (props: AnyObject) } const className = propValue - const color = getWebStyles(className).accentColor + const color = getWebStyles(className, uniwindContext).accentColor acc.generatedProps[colorProp] = color !== undefined ? formatColor(color) @@ -69,6 +72,8 @@ const withAutoUniwind = (Component: Component) => (props: AnyObject) } const withManualUniwind = (Component: Component, options: Record) => (props: AnyObject) => { + const uniwindContext = useContext(UniwindContext) + const { generatedProps, classNames } = Object.entries(options).reduce((acc, [propName, option]) => { const className = props[option.fromClassName] @@ -82,7 +87,7 @@ const withManualUniwind = (Component: Component, options: Record { - if (!documentStyles) { - return undefined - } - - const value = documentStyles.getPropertyValue(name).trim() - - if (value === '') { - return undefined - } - - return parseCSSValue(value) -} +export const getVariableValue = getWebVariable diff --git a/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts b/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts index 069daf3e..79cdb7d8 100644 --- a/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts +++ b/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts @@ -1,13 +1,14 @@ -import { useLayoutEffect, useRef, useState } from 'react' +import { useContext, useLayoutEffect, useRef, useState } from 'react' +import { UniwindContext } from '../../core/context' import { UniwindListener } from '../../core/listener' import { Logger } from '../../core/logger' import { StyleDependency } from '../../types' import { getVariableValue } from './getVariableValue' -const getValue = (name: string | Array) => +const getValue = (name: string | Array, uniwindContext: React.ContextType) => Array.isArray(name) - ? name.map(getVariableValue) - : getVariableValue(name) + ? name.map(name => getVariableValue(name, uniwindContext)) + : getVariableValue(name, uniwindContext) const arrayEquals = (a: Array, b: Array) => { if (a.length !== b.length) { @@ -42,7 +43,8 @@ type UseCSSVariable = { * @returns Value / Values of the CSS variable. On web it is always a string (1rem, #ff0000, etc.), but on native it can be a string or a number (16px, #ff0000) */ export const useCSSVariable: UseCSSVariable = (name: string | Array) => { - const [value, setValue] = useState(getValue(name)) + const uniwindContext = useContext(UniwindContext) + const [value, setValue] = useState(getValue(name, uniwindContext)) const nameRef = useRef(name) useLayoutEffect(() => { @@ -51,20 +53,20 @@ export const useCSSVariable: UseCSSVariable = (name: string | Array) => return } - setValue(getValue(name)) + setValue(getValue(name, uniwindContext)) nameRef.current = name return } if (name !== nameRef.current) { - setValue(getValue(name)) + setValue(getValue(name, uniwindContext)) nameRef.current = name } }, [name]) useLayoutEffect(() => { - const updateValue = () => setValue(getValue(nameRef.current)) + const updateValue = () => setValue(getValue(nameRef.current, uniwindContext)) const dispose = UniwindListener.subscribe( updateValue, [StyleDependency.Theme, StyleDependency.Variables], diff --git a/packages/uniwind/src/hooks/useResolveClassNames.ts b/packages/uniwind/src/hooks/useResolveClassNames.ts index 9150d88b..59cc7e5f 100644 --- a/packages/uniwind/src/hooks/useResolveClassNames.ts +++ b/packages/uniwind/src/hooks/useResolveClassNames.ts @@ -1,13 +1,15 @@ -import { useLayoutEffect, useReducer } from 'react' +import { useContext, useLayoutEffect, useReducer } from 'react' +import { UniwindContext } from '../core/context' import { RNStyle } from '../core/types' import { CSSListener, getWebStyles } from '../core/web' const emptyState = {} as RNStyle export const useResolveClassNames = (className: string) => { + const uniwindContext = useContext(UniwindContext) const [styles, recreate] = useReducer( - () => className !== '' ? getWebStyles(className) : emptyState, - className !== '' ? getWebStyles(className) : emptyState, + () => className !== '' ? getWebStyles(className, uniwindContext) : emptyState, + className !== '' ? getWebStyles(className, uniwindContext) : emptyState, ) useLayoutEffect(() => { diff --git a/packages/uniwind/tests/consts.ts b/packages/uniwind/tests/consts.ts index 69980a2c..2bca8d82 100644 --- a/packages/uniwind/tests/consts.ts +++ b/packages/uniwind/tests/consts.ts @@ -1,3 +1,6 @@ +import * as React from 'react' +import type { UniwindContext } from '../src/core/context' + export const TW_RED_500 = '#fb2c36' export const TW_GREEN_500 = '#00c950' export const TW_BLUE_500 = '#2b7fff' @@ -9,3 +12,7 @@ export const SAFE_AREA_INSET_BOTTOM = 42 export const SCREEN_WIDTH = 390 export const SCREEN_HEIGHT = 844 + +export const UNIWIND_CONTEXT_MOCK = { + scopedTheme: null, +} satisfies React.ContextType diff --git a/packages/uniwind/tests/web/hoc/withUniwind.test.tsx b/packages/uniwind/tests/web/hoc/withUniwind.test.tsx index ed41dd52..15f35552 100644 --- a/packages/uniwind/tests/web/hoc/withUniwind.test.tsx +++ b/packages/uniwind/tests/web/hoc/withUniwind.test.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { ActivityIndicator, ActivityIndicatorProps } from 'react-native' import * as webCore from '../../../src/core/web' import { withUniwind } from '../../../src/hoc/withUniwind' -import { TW_BLUE_500, TW_RED_500 } from '../../consts' +import { TW_BLUE_500, TW_RED_500, UNIWIND_CONTEXT_MOCK } from '../../consts' const Component: React.FC = (props) => @@ -42,7 +42,7 @@ describe('withUniwind', () => { render() - expect(mockGetWebStyles).toHaveBeenCalledWith('accent-red-500') + expect(mockGetWebStyles).toHaveBeenCalledWith('accent-red-500', UNIWIND_CONTEXT_MOCK) const receivedProps = ComponentWithSpy.mock.calls[0][0] From c62b005befae0cf07f03212b8e2ade8df6f402b0 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Thu, 19 Feb 2026 14:28:22 +0100 Subject: [PATCH 06/13] feat: native scoped theme --- .../src/components/native/Pressable.tsx | 3 + .../uniwind/src/components/native/useStyle.ts | 4 +- .../uniwind/src/core/config/config.native.ts | 19 +--- packages/uniwind/src/core/context.ts | 4 +- packages/uniwind/src/core/native/store.ts | 94 +++++++++++-------- packages/uniwind/src/core/types.ts | 4 + packages/uniwind/src/core/web/getWebStyles.ts | 7 +- .../uniwind/src/hoc/withUniwind.native.tsx | 11 ++- packages/uniwind/src/hoc/withUniwind.tsx | 8 +- .../useCSSVariable/getVariableValue.native.ts | 6 +- .../hooks/useCSSVariable/useCSSVariable.ts | 9 +- .../src/hooks/useResolveClassNames.native.ts | 6 +- .../uniwind/src/hooks/useResolveClassNames.ts | 6 +- packages/uniwind/tests/consts.ts | 5 +- 14 files changed, 102 insertions(+), 84 deletions(-) diff --git a/packages/uniwind/src/components/native/Pressable.tsx b/packages/uniwind/src/components/native/Pressable.tsx index 3c220a57..b582da49 100644 --- a/packages/uniwind/src/components/native/Pressable.tsx +++ b/packages/uniwind/src/components/native/Pressable.tsx @@ -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' @@ -11,6 +12,7 @@ export const Pressable = copyComponentProperties(RNPressable, (props: PressableP isDisabled: Boolean(props.disabled), }, ) + const uniwindContext = useUniwindContext() return ( , 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) { diff --git a/packages/uniwind/src/core/config/config.native.ts b/packages/uniwind/src/core/config/config.native.ts index fb783f4c..88bcd6c0 100644 --- a/packages/uniwind/src/core/config/config.native.ts +++ b/packages/uniwind/src/core/config/config.native.ts @@ -36,23 +36,11 @@ 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, { + Object.defineProperty(UniwindStore.vars[theme], varName, { configurable: true, enumerable: true, - get: () => value, + get: getValue, }) - UniwindStore.runtimeThemeVariables.set(theme, runtimeThemeVariables) }) if (theme === this.currentTheme) { @@ -70,12 +58,11 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase { protected __reinit(generateStyleSheetCallback: GenerateStyleSheetsCallback, themes: Array) { super.__reinit(generateStyleSheetCallback, themes) - UniwindStore.reinit(generateStyleSheetCallback) + UniwindStore.reinit(generateStyleSheetCallback, themes) } protected onThemeChange() { UniwindStore.runtime.currentThemeName = this.currentTheme - UniwindStore.reinit() } } diff --git a/packages/uniwind/src/core/context.ts b/packages/uniwind/src/core/context.ts index 3d234b9b..a08dec16 100644 --- a/packages/uniwind/src/core/context.ts +++ b/packages/uniwind/src/core/context.ts @@ -1,6 +1,8 @@ -import { createContext } from 'react' +import { createContext, useContext } from 'react' import { ThemeName } from './types' export const UniwindContext = createContext({ scopedTheme: null as ThemeName | null, }) + +export const useUniwindContext = () => useContext(UniwindContext) diff --git a/packages/uniwind/src/core/native/store.ts b/packages/uniwind/src/core/native/store.ts index 62c6e337..a48ffa75 100644 --- a/packages/uniwind/src/core/native/store.ts +++ b/packages/uniwind/src/core/native/store.ts @@ -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' @@ -17,30 +17,38 @@ const emptyState: StylesResult = { styles: {}, dependencies: [], dependencySum: class UniwindStoreBuilder { runtime = UniwindRuntime - vars = {} as Record - runtimeThemeVariables = new Map() + vars = {} as Record> private stylesheet = {} as StyleSheets - private cache = new Map() - private generateStyleSheetCallbackResult: ReturnType | null = null - - getStyles(className: string | undefined, componentProps?: Record, state?: ComponentState): StylesResult { + private cache = {} as Record> + + getStyles( + className: string | undefined, + componentProps: Record | 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 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 }, ) @@ -49,47 +57,49 @@ class UniwindStoreBuilder { return result } - reinit = (generateStyleSheetCallback?: GenerateStyleSheetsCallback) => { - const config = generateStyleSheetCallback?.(this.runtime) ?? this.generateStyleSheetCallbackResult - - if (!config) { - return - } - + reinit = (generateStyleSheetCallback: GenerateStyleSheetsCallback, themes: Array) => { + 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, state?: ComponentState) { + private resolveStyles( + classNames: string, + componentProps: Record | undefined, + state: ComponentState | undefined, + uniwindContext: UniwindContextType, + ) { const result = {} as Record - let vars = this.vars + // At this point we're sure that theme is correct + const theme = uniwindContext.scopedTheme ?? this.runtime.currentThemeName + let vars = this.vars[theme]! + const originalVars = vars let hasDataAttributes = false const dependencies = new Set() let dependencySum = 0 const bestBreakpoints = new Map() + const isScopedTheme = uniwindContext.scopedTheme !== null for (const className of classNames.split(' ')) { if (!(className in this.stylesheet)) { @@ -99,6 +109,10 @@ class UniwindStoreBuilder { for (const style of this.stylesheet[className] as Array