@niibase/uniwind 1.4.0 → 1.4.1

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.
@@ -34,6 +34,16 @@ const UniwindRuntime = exports.UniwindRuntime = {
34
34
  steps: _reactNativeReanimated.steps,
35
35
  linear: _reactNativeReanimated.linear,
36
36
  lightDark: () => "",
37
- parseColor: _nativeUtils.parseColor
37
+ parseColor: _nativeUtils.parseColor,
38
+ platformSelect: (android, ios, other) => {
39
+ return _reactNative.Platform.select(Boolean(other) ? {
40
+ android,
41
+ ios,
42
+ default: other
43
+ } : {
44
+ android,
45
+ default: ios
46
+ });
47
+ }
38
48
  };
39
49
  UniwindRuntime.lightDark = _nativeUtils.lightDark.bind(UniwindRuntime);
@@ -13,6 +13,15 @@ const ONE_PX = {
13
13
  }
14
14
  };
15
15
  class FunctionVisitor {
16
+ platformSelect(fn) {
17
+ return fn.arguments.at(0) ?? {
18
+ type: "token",
19
+ value: {
20
+ type: "ident",
21
+ value: "initial"
22
+ }
23
+ };
24
+ }
16
25
  pixelRatio(fn) {
17
26
  return {
18
27
  type: "function",
@@ -16,9 +16,8 @@ class RuleVisitor {
16
16
  };
17
17
  style = styleRule => {
18
18
  const firstSelector = styleRule.value.selectors.at(0)?.at(0);
19
- const secondSelector = styleRule.value.selectors.at(0)?.at(1);
20
- if (this.currentLayerName === "theme" && firstSelector?.type === "nesting" && secondSelector?.type === "pseudo-class" && secondSelector.kind === "where") {
21
- return this.processThemeStyle(styleRule, secondSelector);
19
+ if (this.currentLayerName === "theme" && firstSelector?.type === "pseudo-class" && firstSelector.kind === "root") {
20
+ return this.removeNulls(this.processThemeRoot(styleRule));
22
21
  }
23
22
  if (firstSelector?.type === "class") {
24
23
  return this.processClassStyle(styleRule, firstSelector);
@@ -29,14 +28,44 @@ class RuleVisitor {
29
28
  this.processedClassNames.clear();
30
29
  this.processedVariables.clear();
31
30
  }
31
+ processThemeRoot(styleRule) {
32
+ const themeScopedRules = styleRule.value.rules?.filter(rule => {
33
+ if (rule.type !== "style") {
34
+ return false;
35
+ }
36
+ const firstSelector = rule.value.selectors.at(0)?.at(0);
37
+ const secondSelector = rule.value.selectors.at(0)?.at(1);
38
+ return firstSelector?.type === "nesting" && secondSelector?.type === "pseudo-class" && secondSelector.kind === "where";
39
+ }) ?? [];
40
+ const nonThemeRules = styleRule.value.rules?.filter(rule => !themeScopedRules.includes(rule));
41
+ const processedThemeScopedRules = themeScopedRules.map(rule => {
42
+ if (rule.type !== "style") {
43
+ return rule;
44
+ }
45
+ const secondSelector = rule.value.selectors.at(0)?.at(1);
46
+ if (secondSelector?.type === "pseudo-class" && secondSelector.kind === "where") {
47
+ return this.processThemeStyle(rule, secondSelector);
48
+ }
49
+ return rule;
50
+ });
51
+ return [{
52
+ type: "style",
53
+ value: {
54
+ loc: styleRule.value.loc,
55
+ selectors: styleRule.value.selectors,
56
+ rules: nonThemeRules,
57
+ declarations: styleRule.value.declarations
58
+ }
59
+ }, ...processedThemeScopedRules];
60
+ }
32
61
  processThemeStyle(styleRule, secondSelector) {
33
62
  const whereSelector = secondSelector.selectors.at(0)?.at(0);
34
63
  if (whereSelector?.type !== "class") {
35
- return;
64
+ return styleRule;
36
65
  }
37
66
  const selectedVariant = this.themes.find(theme => whereSelector.name === theme);
38
67
  if (selectedVariant === void 0 || this.processedVariables.has(selectedVariant)) {
39
- return;
68
+ return styleRule;
40
69
  }
41
70
  this.processedVariables.add(selectedVariant);
42
71
  return {
@@ -74,5 +103,20 @@ class RuleVisitor {
74
103
  }
75
104
  };
76
105
  }
106
+ // Fixes lightningcss serialization bug
107
+ removeNulls(value) {
108
+ if (Array.isArray(value)) {
109
+ return value.map(v => this.removeNulls(v));
110
+ }
111
+ if (typeof value === "object" && value !== null) {
112
+ return Object.fromEntries(Object.entries(value).filter(([_, value2]) => {
113
+ if (value2 === null) {
114
+ return false;
115
+ }
116
+ return true;
117
+ }).map(([key, value2]) => [key, this.removeNulls(value2)]));
118
+ }
119
+ return value;
120
+ }
77
121
  }
78
122
  exports.RuleVisitor = RuleVisitor;
@@ -7,7 +7,7 @@ const node = require('@tailwindcss/node');
7
7
  const oxide = require('@tailwindcss/oxide');
8
8
  const lightningcss = require('lightningcss');
9
9
  const types = require('../shared/uniwind.BZIuaszw.cjs');
10
- const stringifyThemes = require('../shared/uniwind.CyACT0sD.cjs');
10
+ const stringifyThemes = require('../shared/uniwind.D7C2Zt-r.cjs');
11
11
  const culori = require('culori');
12
12
 
13
13
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
@@ -1118,6 +1118,9 @@ class Functions {
1118
1118
  if (fn.name === "hairlineWidth") {
1119
1119
  return "rt.hairlineWidth";
1120
1120
  }
1121
+ if (fn.name === "platformSelect") {
1122
+ return `rt.platformSelect(${this.Processor.CSS.processValue(fn.arguments)})`;
1123
+ }
1121
1124
  if (fn.name === "pixelRatio") {
1122
1125
  if (fn.arguments.length === 0) {
1123
1126
  return "rt.pixelRatio(1)";
@@ -5,7 +5,7 @@ import { compile } from '@tailwindcss/node';
5
5
  import { Scanner } from '@tailwindcss/oxide';
6
6
  import { transform as transform$1 } from 'lightningcss';
7
7
  import { P as Platform } from '../shared/uniwind.CyoRUwOj.mjs';
8
- import { L as Logger, U as UniwindCSSVisitor, s as stringifyThemes, b as buildDtsFile, a as buildCSS } from '../shared/uniwind.D-ahjOrG.mjs';
8
+ import { L as Logger, U as UniwindCSSVisitor, s as stringifyThemes, b as buildDtsFile, a as buildCSS } from '../shared/uniwind.B_j3NcHy.mjs';
9
9
  import { converter, parse, formatHex, formatHex8 } from 'culori';
10
10
 
11
11
  const parseStringValue = (value) => {
@@ -1111,6 +1111,9 @@ class Functions {
1111
1111
  if (fn.name === "hairlineWidth") {
1112
1112
  return "rt.hairlineWidth";
1113
1113
  }
1114
+ if (fn.name === "platformSelect") {
1115
+ return `rt.platformSelect(${this.Processor.CSS.processValue(fn.arguments)})`;
1116
+ }
1114
1117
  if (fn.name === "pixelRatio") {
1115
1118
  if (fn.arguments.length === 0) {
1116
1119
  return "rt.pixelRatio(1)";
@@ -1,4 +1,4 @@
1
- import { Appearance, Dimensions, I18nManager, PixelRatio, StyleSheet } from "react-native";
1
+ import { Appearance, Dimensions, I18nManager, PixelRatio, Platform, StyleSheet } from "react-native";
2
2
  import { cubicBezier, linear, steps } from "react-native-reanimated";
3
3
  import { initialWindowMetrics } from "react-native-safe-area-context";
4
4
  import { ColorScheme, Orientation } from "../../types.js";
@@ -23,6 +23,9 @@ export const UniwindRuntime = {
23
23
  steps,
24
24
  linear,
25
25
  lightDark: () => "",
26
- parseColor
26
+ parseColor,
27
+ platformSelect: (android, ios, other) => {
28
+ return Platform.select(Boolean(other) ? { android, ios, default: other } : { android, default: ios });
29
+ }
27
30
  };
28
31
  UniwindRuntime.lightDark = lightDark.bind(UniwindRuntime);
@@ -57,6 +57,7 @@ export type UniwindRuntime = {
57
57
  linear: (...points: ControlPoint[]) => LinearEasing;
58
58
  lightDark: (light: string, dark: string) => string;
59
59
  parseColor: (type: string, color: string) => string;
60
+ platformSelect: (android: string, ios: string, other?: string) => string;
60
61
  };
61
62
  export type RNStyle = ViewStyle & TextStyle & ImageStyle & {
62
63
  accentColor?: string;
@@ -1,6 +1,7 @@
1
1
  import { Function as LightningCSSFunction, TokenOrValue } from 'lightningcss';
2
2
  export declare class FunctionVisitor {
3
3
  [name: string]: (fn: LightningCSSFunction) => TokenOrValue;
4
+ platformSelect(fn: LightningCSSFunction): TokenOrValue;
4
5
  pixelRatio(fn: LightningCSSFunction): TokenOrValue;
5
6
  fontScale(fn: LightningCSSFunction): TokenOrValue;
6
7
  hairlineWidth(): TokenOrValue;
@@ -3,6 +3,9 @@ const ONE_PX = {
3
3
  value: { type: "dimension", unit: "px", value: 1 }
4
4
  };
5
5
  export class FunctionVisitor {
6
+ platformSelect(fn) {
7
+ return fn.arguments.at(0) ?? { type: "token", value: { type: "ident", value: "initial" } };
8
+ }
6
9
  pixelRatio(fn) {
7
10
  return {
8
11
  type: "function",
@@ -16,9 +16,11 @@ export declare class RuleVisitor implements LightningRuleVisitors {
16
16
  }>) => void;
17
17
  style: (styleRule: Extract<LightningRuleVisitor, {
18
18
  type: "style";
19
- }>) => void | ReturnedRule;
19
+ }>) => void | ReturnedRule | ReturnedRule[];
20
20
  cleanup(): void;
21
+ private processThemeRoot;
21
22
  private processThemeStyle;
22
23
  private processClassStyle;
24
+ private removeNulls;
23
25
  }
24
26
  export {};
@@ -10,9 +10,8 @@ export class RuleVisitor {
10
10
  };
11
11
  style = (styleRule) => {
12
12
  const firstSelector = styleRule.value.selectors.at(0)?.at(0);
13
- const secondSelector = styleRule.value.selectors.at(0)?.at(1);
14
- if (this.currentLayerName === "theme" && firstSelector?.type === "nesting" && secondSelector?.type === "pseudo-class" && secondSelector.kind === "where") {
15
- return this.processThemeStyle(styleRule, secondSelector);
13
+ if (this.currentLayerName === "theme" && firstSelector?.type === "pseudo-class" && firstSelector.kind === "root") {
14
+ return this.removeNulls(this.processThemeRoot(styleRule));
16
15
  }
17
16
  if (firstSelector?.type === "class") {
18
17
  return this.processClassStyle(styleRule, firstSelector);
@@ -23,14 +22,47 @@ export class RuleVisitor {
23
22
  this.processedClassNames.clear();
24
23
  this.processedVariables.clear();
25
24
  }
25
+ processThemeRoot(styleRule) {
26
+ const themeScopedRules = styleRule.value.rules?.filter((rule) => {
27
+ if (rule.type !== "style") {
28
+ return false;
29
+ }
30
+ const firstSelector = rule.value.selectors.at(0)?.at(0);
31
+ const secondSelector = rule.value.selectors.at(0)?.at(1);
32
+ return firstSelector?.type === "nesting" && secondSelector?.type === "pseudo-class" && secondSelector.kind === "where";
33
+ }) ?? [];
34
+ const nonThemeRules = styleRule.value.rules?.filter((rule) => !themeScopedRules.includes(rule));
35
+ const processedThemeScopedRules = themeScopedRules.map((rule) => {
36
+ if (rule.type !== "style") {
37
+ return rule;
38
+ }
39
+ const secondSelector = rule.value.selectors.at(0)?.at(1);
40
+ if (secondSelector?.type === "pseudo-class" && secondSelector.kind === "where") {
41
+ return this.processThemeStyle(rule, secondSelector);
42
+ }
43
+ return rule;
44
+ });
45
+ return [
46
+ {
47
+ type: "style",
48
+ value: {
49
+ loc: styleRule.value.loc,
50
+ selectors: styleRule.value.selectors,
51
+ rules: nonThemeRules,
52
+ declarations: styleRule.value.declarations
53
+ }
54
+ },
55
+ ...processedThemeScopedRules
56
+ ];
57
+ }
26
58
  processThemeStyle(styleRule, secondSelector) {
27
59
  const whereSelector = secondSelector.selectors.at(0)?.at(0);
28
60
  if (whereSelector?.type !== "class") {
29
- return;
61
+ return styleRule;
30
62
  }
31
63
  const selectedVariant = this.themes.find((theme) => whereSelector.name === theme);
32
64
  if (selectedVariant === void 0 || this.processedVariables.has(selectedVariant)) {
33
- return;
65
+ return styleRule;
34
66
  }
35
67
  this.processedVariables.add(selectedVariant);
36
68
  return {
@@ -59,4 +91,21 @@ export class RuleVisitor {
59
91
  }
60
92
  };
61
93
  }
94
+ // Fixes lightningcss serialization bug
95
+ removeNulls(value) {
96
+ if (Array.isArray(value)) {
97
+ return value.map((v) => this.removeNulls(v));
98
+ }
99
+ if (typeof value === "object" && value !== null) {
100
+ return Object.fromEntries(
101
+ Object.entries(value).filter(([_, value2]) => {
102
+ if (value2 === null) {
103
+ return false;
104
+ }
105
+ return true;
106
+ }).map(([key, value2]) => [key, this.removeNulls(value2)])
107
+ );
108
+ }
109
+ return value;
110
+ }
62
111
  }
@@ -44,6 +44,9 @@ const ONE_PX = {
44
44
  value: { type: "dimension", unit: "px", value: 1 }
45
45
  };
46
46
  class FunctionVisitor {
47
+ platformSelect(fn) {
48
+ return fn.arguments.at(0) ?? { type: "token", value: { type: "ident", value: "initial" } };
49
+ }
47
50
  pixelRatio(fn) {
48
51
  return {
49
52
  type: "function",
@@ -90,9 +93,8 @@ class RuleVisitor {
90
93
  };
91
94
  style = (styleRule) => {
92
95
  const firstSelector = styleRule.value.selectors.at(0)?.at(0);
93
- const secondSelector = styleRule.value.selectors.at(0)?.at(1);
94
- if (this.currentLayerName === "theme" && firstSelector?.type === "nesting" && secondSelector?.type === "pseudo-class" && secondSelector.kind === "where") {
95
- return this.processThemeStyle(styleRule, secondSelector);
96
+ if (this.currentLayerName === "theme" && firstSelector?.type === "pseudo-class" && firstSelector.kind === "root") {
97
+ return this.removeNulls(this.processThemeRoot(styleRule));
96
98
  }
97
99
  if (firstSelector?.type === "class") {
98
100
  return this.processClassStyle(styleRule, firstSelector);
@@ -103,14 +105,47 @@ class RuleVisitor {
103
105
  this.processedClassNames.clear();
104
106
  this.processedVariables.clear();
105
107
  }
108
+ processThemeRoot(styleRule) {
109
+ const themeScopedRules = styleRule.value.rules?.filter((rule) => {
110
+ if (rule.type !== "style") {
111
+ return false;
112
+ }
113
+ const firstSelector = rule.value.selectors.at(0)?.at(0);
114
+ const secondSelector = rule.value.selectors.at(0)?.at(1);
115
+ return firstSelector?.type === "nesting" && secondSelector?.type === "pseudo-class" && secondSelector.kind === "where";
116
+ }) ?? [];
117
+ const nonThemeRules = styleRule.value.rules?.filter((rule) => !themeScopedRules.includes(rule));
118
+ const processedThemeScopedRules = themeScopedRules.map((rule) => {
119
+ if (rule.type !== "style") {
120
+ return rule;
121
+ }
122
+ const secondSelector = rule.value.selectors.at(0)?.at(1);
123
+ if (secondSelector?.type === "pseudo-class" && secondSelector.kind === "where") {
124
+ return this.processThemeStyle(rule, secondSelector);
125
+ }
126
+ return rule;
127
+ });
128
+ return [
129
+ {
130
+ type: "style",
131
+ value: {
132
+ loc: styleRule.value.loc,
133
+ selectors: styleRule.value.selectors,
134
+ rules: nonThemeRules,
135
+ declarations: styleRule.value.declarations
136
+ }
137
+ },
138
+ ...processedThemeScopedRules
139
+ ];
140
+ }
106
141
  processThemeStyle(styleRule, secondSelector) {
107
142
  const whereSelector = secondSelector.selectors.at(0)?.at(0);
108
143
  if (whereSelector?.type !== "class") {
109
- return;
144
+ return styleRule;
110
145
  }
111
146
  const selectedVariant = this.themes.find((theme) => whereSelector.name === theme);
112
147
  if (selectedVariant === void 0 || this.processedVariables.has(selectedVariant)) {
113
- return;
148
+ return styleRule;
114
149
  }
115
150
  this.processedVariables.add(selectedVariant);
116
151
  return {
@@ -139,6 +174,23 @@ class RuleVisitor {
139
174
  }
140
175
  };
141
176
  }
177
+ // Fixes lightningcss serialization bug
178
+ removeNulls(value) {
179
+ if (Array.isArray(value)) {
180
+ return value.map((v) => this.removeNulls(v));
181
+ }
182
+ if (typeof value === "object" && value !== null) {
183
+ return Object.fromEntries(
184
+ Object.entries(value).filter(([_, value2]) => {
185
+ if (value2 === null) {
186
+ return false;
187
+ }
188
+ return true;
189
+ }).map(([key, value2]) => [key, this.removeNulls(value2)])
190
+ );
191
+ }
192
+ return value;
193
+ }
142
194
  }
143
195
 
144
196
  class UniwindCSSVisitor {
@@ -51,6 +51,9 @@ const ONE_PX = {
51
51
  value: { type: "dimension", unit: "px", value: 1 }
52
52
  };
53
53
  class FunctionVisitor {
54
+ platformSelect(fn) {
55
+ return fn.arguments.at(0) ?? { type: "token", value: { type: "ident", value: "initial" } };
56
+ }
54
57
  pixelRatio(fn) {
55
58
  return {
56
59
  type: "function",
@@ -97,9 +100,8 @@ class RuleVisitor {
97
100
  };
98
101
  style = (styleRule) => {
99
102
  const firstSelector = styleRule.value.selectors.at(0)?.at(0);
100
- const secondSelector = styleRule.value.selectors.at(0)?.at(1);
101
- if (this.currentLayerName === "theme" && firstSelector?.type === "nesting" && secondSelector?.type === "pseudo-class" && secondSelector.kind === "where") {
102
- return this.processThemeStyle(styleRule, secondSelector);
103
+ if (this.currentLayerName === "theme" && firstSelector?.type === "pseudo-class" && firstSelector.kind === "root") {
104
+ return this.removeNulls(this.processThemeRoot(styleRule));
103
105
  }
104
106
  if (firstSelector?.type === "class") {
105
107
  return this.processClassStyle(styleRule, firstSelector);
@@ -110,14 +112,47 @@ class RuleVisitor {
110
112
  this.processedClassNames.clear();
111
113
  this.processedVariables.clear();
112
114
  }
115
+ processThemeRoot(styleRule) {
116
+ const themeScopedRules = styleRule.value.rules?.filter((rule) => {
117
+ if (rule.type !== "style") {
118
+ return false;
119
+ }
120
+ const firstSelector = rule.value.selectors.at(0)?.at(0);
121
+ const secondSelector = rule.value.selectors.at(0)?.at(1);
122
+ return firstSelector?.type === "nesting" && secondSelector?.type === "pseudo-class" && secondSelector.kind === "where";
123
+ }) ?? [];
124
+ const nonThemeRules = styleRule.value.rules?.filter((rule) => !themeScopedRules.includes(rule));
125
+ const processedThemeScopedRules = themeScopedRules.map((rule) => {
126
+ if (rule.type !== "style") {
127
+ return rule;
128
+ }
129
+ const secondSelector = rule.value.selectors.at(0)?.at(1);
130
+ if (secondSelector?.type === "pseudo-class" && secondSelector.kind === "where") {
131
+ return this.processThemeStyle(rule, secondSelector);
132
+ }
133
+ return rule;
134
+ });
135
+ return [
136
+ {
137
+ type: "style",
138
+ value: {
139
+ loc: styleRule.value.loc,
140
+ selectors: styleRule.value.selectors,
141
+ rules: nonThemeRules,
142
+ declarations: styleRule.value.declarations
143
+ }
144
+ },
145
+ ...processedThemeScopedRules
146
+ ];
147
+ }
113
148
  processThemeStyle(styleRule, secondSelector) {
114
149
  const whereSelector = secondSelector.selectors.at(0)?.at(0);
115
150
  if (whereSelector?.type !== "class") {
116
- return;
151
+ return styleRule;
117
152
  }
118
153
  const selectedVariant = this.themes.find((theme) => whereSelector.name === theme);
119
154
  if (selectedVariant === void 0 || this.processedVariables.has(selectedVariant)) {
120
- return;
155
+ return styleRule;
121
156
  }
122
157
  this.processedVariables.add(selectedVariant);
123
158
  return {
@@ -146,6 +181,23 @@ class RuleVisitor {
146
181
  }
147
182
  };
148
183
  }
184
+ // Fixes lightningcss serialization bug
185
+ removeNulls(value) {
186
+ if (Array.isArray(value)) {
187
+ return value.map((v) => this.removeNulls(v));
188
+ }
189
+ if (typeof value === "object" && value !== null) {
190
+ return Object.fromEntries(
191
+ Object.entries(value).filter(([_, value2]) => {
192
+ if (value2 === null) {
193
+ return false;
194
+ }
195
+ return true;
196
+ }).map(([key, value2]) => [key, this.removeNulls(value2)])
197
+ );
198
+ }
199
+ return value;
200
+ }
149
201
  }
150
202
 
151
203
  class UniwindCSSVisitor {
@@ -3,7 +3,7 @@
3
3
  const node = require('@tailwindcss/node');
4
4
  const path = require('path');
5
5
  const common = require('../shared/uniwind.nl8746mK.cjs');
6
- const stringifyThemes = require('../shared/uniwind.CyACT0sD.cjs');
6
+ const stringifyThemes = require('../shared/uniwind.D7C2Zt-r.cjs');
7
7
  require('fs');
8
8
  require('lightningcss');
9
9
 
@@ -1,7 +1,7 @@
1
1
  import { normalizePath } from '@tailwindcss/node';
2
2
  import path from 'path';
3
3
  import { u as uniq, n as name } from '../shared/uniwind.F-0-Rr--.mjs';
4
- import { s as stringifyThemes, U as UniwindCSSVisitor, a as buildCSS, b as buildDtsFile } from '../shared/uniwind.D-ahjOrG.mjs';
4
+ import { s as stringifyThemes, U as UniwindCSSVisitor, a as buildCSS, b as buildDtsFile } from '../shared/uniwind.B_j3NcHy.mjs';
5
5
  import 'fs';
6
6
  import 'lightningcss';
7
7
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@niibase/uniwind",
4
- "version": "1.4.0",
4
+ "version": "1.4.1",
5
5
  "description": "The fastest Tailwind bindings for React Native with Reanimated 4 support",
6
6
  "homepage": "https://uniwind.dev",
7
7
  "author": "Unistack",
8
8
  "type": "module",
9
- "repository": "https://github.com/divineniiquaye/uniwind",
9
+ "repository": "https://github.com/divineniiquaye/uniwind-oss",
10
10
  "sideEffects": false,
11
11
  "scripts": {
12
12
  "precommit": "bun lint",
@@ -25,6 +25,7 @@
25
25
  "keywords": [
26
26
  "unistyles",
27
27
  "react-native-unistyles",
28
+ "uniwind",
28
29
  "react-native",
29
30
  "react",
30
31
  "native",
package/readme.md CHANGED
@@ -15,7 +15,7 @@
15
15
  [![npm downloads](https://img.shields.io/npm/dt/uniwind?style=for-the-badge)](https://www.npmjs.com/package/uniwind)
16
16
  [![License: MIT](https://img.shields.io/badge/License-MIT-44CD11.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
17
17
 
18
- > **Note:** This is a fork of uniwind that makes pro subscription features available for free (except for Unistyles support). Install from `@niibase/uniwind` to get these features.
18
+ > **Note:** This is a fork of uniwind that uses **react-native-reanimated** (v4.0.0+) to provide CSS-like animations and transitions using Tailwind classes. Updates are frequently released with original uniwind package with versions in-sync.
19
19
 
20
20
  ## Installation
21
21
 
@@ -24,7 +24,7 @@
24
24
  bun add @niibase/uniwind tailwindcss react-native-safe-area-context react-native-reanimated
25
25
 
26
26
  # Or install from the original package
27
- bun add uniwind tailwindcss react-native-safe-area-context react-native-reanimated
27
+ bun add uniwind tailwindcss react-native-safe-area-context
28
28
 
29
29
  # Or install @niibase/uniwind as alias for uniwind
30
30
  bun add uniwind@npm:@niibase/uniwind tailwindcss react-native-safe-area-context react-native-reanimated
@@ -41,8 +41,8 @@ Then follow [installation guides](https://docs.uniwind.dev/quickstart)
41
41
 
42
42
  ## Features
43
43
 
44
- - ⚛️ Out‑of‑the‑box `className` bindings for every React Native component
45
- - ⚡ Styles are computed at build time for maximum performance
44
+ - ⚛️ Out‑of‑the‑box `className` bindings for every React Native component
45
+ - ⚡ Styles are computed at build time for maximum performance
46
46
  - 🎬 **CSS animations and transitions** powered by Reanimated 4
47
47
  - 🌙 Dark mode and 🎨 fully customizable themes
48
48
  - 🧩 Pseudo‑classes support — `focus`, `active`, `disabled`, and more
@@ -50,14 +50,6 @@ Then follow [installation guides](https://docs.uniwind.dev/quickstart)
50
50
  - 🧰 Use custom CSS properties directly in React Native
51
51
  - 🔥 And [much more](https://docs.uniwind.dev/api/use-uniwind)
52
52
 
53
- ## Roadmap
54
-
55
- - [x] **Reanimated 4 support** - Full compatibility with React Native Reanimated v4 ✅
56
- - [ ] **Babel support to extend beyond React Native components** - Enable className support for custom components and third-party libraries
57
- - [ ] **Unistyles (future)** - Integration with Unistyles for enhanced styling capabilities
58
-
59
- > **Note:** Once Babel support is completed, the likelihood of adding Unistyles support is high.
60
-
61
53
  ## Contributing
62
54
 
63
55
  Contributions are welcome!
@@ -1,4 +1,4 @@
1
- import { Appearance, Dimensions, I18nManager, PixelRatio, StyleSheet } from 'react-native'
1
+ import { Appearance, Dimensions, I18nManager, PixelRatio, Platform, StyleSheet } from 'react-native'
2
2
  import { cubicBezier, linear, steps } from 'react-native-reanimated'
3
3
  import { initialWindowMetrics } from 'react-native-safe-area-context'
4
4
  import { ColorScheme, Orientation } from '../../types'
@@ -27,6 +27,10 @@ export const UniwindRuntime = {
27
27
  linear,
28
28
  lightDark: () => '',
29
29
  parseColor,
30
+ platformSelect: (android: string, ios: string, other?: string) => {
31
+ // eslint-disable-next-line no-extra-boolean-cast
32
+ return Platform.select(Boolean(other) ? { android, ios, default: other } : { android, default: ios })
33
+ },
30
34
  } as UniwindRuntimeType
31
35
 
32
36
  UniwindRuntime.lightDark = lightDark.bind(UniwindRuntime)
package/src/core/types.ts CHANGED
@@ -62,6 +62,7 @@ export type UniwindRuntime = {
62
62
  linear: (...points: ControlPoint[]) => LinearEasing
63
63
  lightDark: (light: string, dark: string) => string
64
64
  parseColor: (type: string, color: string) => string
65
+ platformSelect: (android: string, ios: string, other?: string) => string
65
66
  }
66
67
 
67
68
  export type RNStyle = ViewStyle & TextStyle & ImageStyle & {
@@ -8,6 +8,10 @@ const ONE_PX = {
8
8
  export class FunctionVisitor {
9
9
  [name: string]: (fn: LightningCSSFunction) => TokenOrValue
10
10
 
11
+ platformSelect(fn: LightningCSSFunction): TokenOrValue {
12
+ return fn.arguments.at(0) ?? { type: 'token', value: { type: 'ident', value: 'initial' } }
13
+ }
14
+
11
15
  pixelRatio(fn: LightningCSSFunction): TokenOrValue {
12
16
  return {
13
17
  type: 'function',
@@ -20,13 +20,9 @@ export class RuleVisitor implements LightningRuleVisitors {
20
20
 
21
21
  style = (styleRule: Extract<LightningRuleVisitor, { type: 'style' }>) => {
22
22
  const firstSelector = styleRule.value.selectors.at(0)?.at(0)
23
- const secondSelector = styleRule.value.selectors.at(0)?.at(1)
24
23
 
25
- if (
26
- this.currentLayerName === 'theme' && firstSelector?.type === 'nesting' && secondSelector?.type === 'pseudo-class'
27
- && secondSelector.kind === 'where'
28
- ) {
29
- return this.processThemeStyle(styleRule, secondSelector)
24
+ if (this.currentLayerName === 'theme' && firstSelector?.type === 'pseudo-class' && firstSelector.kind === 'root') {
25
+ return this.removeNulls(this.processThemeRoot(styleRule)) as Array<ReturnedRule>
30
26
  }
31
27
 
32
28
  if (firstSelector?.type === 'class') {
@@ -40,20 +36,60 @@ export class RuleVisitor implements LightningRuleVisitors {
40
36
  this.processedVariables.clear()
41
37
  }
42
38
 
39
+ private processThemeRoot(styleRule: Extract<LightningRuleVisitor, { type: 'style' }>): Array<ReturnedRule> {
40
+ const themeScopedRules = styleRule.value.rules?.filter(rule => {
41
+ if (rule.type !== 'style') {
42
+ return false
43
+ }
44
+
45
+ const firstSelector = rule.value.selectors.at(0)?.at(0)
46
+ const secondSelector = rule.value.selectors.at(0)?.at(1)
47
+
48
+ return firstSelector?.type === 'nesting' && secondSelector?.type === 'pseudo-class' && secondSelector.kind === 'where'
49
+ }) ?? []
50
+ const nonThemeRules = styleRule.value.rules?.filter(rule => !themeScopedRules.includes(rule))
51
+ const processedThemeScopedRules = themeScopedRules.map(rule => {
52
+ if (rule.type !== 'style') {
53
+ return rule
54
+ }
55
+
56
+ const secondSelector = rule.value.selectors.at(0)?.at(1)
57
+
58
+ if (secondSelector?.type === 'pseudo-class' && secondSelector.kind === 'where') {
59
+ return this.processThemeStyle(rule, secondSelector)
60
+ }
61
+
62
+ return rule
63
+ })
64
+
65
+ return [
66
+ {
67
+ type: 'style',
68
+ value: {
69
+ loc: styleRule.value.loc,
70
+ selectors: styleRule.value.selectors,
71
+ rules: nonThemeRules,
72
+ declarations: styleRule.value.declarations,
73
+ },
74
+ },
75
+ ...processedThemeScopedRules,
76
+ ]
77
+ }
78
+
43
79
  private processThemeStyle(
44
80
  styleRule: Extract<LightningRuleVisitor, { type: 'style' }>,
45
81
  secondSelector: Extract<SelectorComponent, { type: 'pseudo-class'; kind: 'where' }>,
46
- ): ReturnedRule | void {
82
+ ): ReturnedRule {
47
83
  const whereSelector = secondSelector.selectors.at(0)?.at(0)
48
84
 
49
85
  if (whereSelector?.type !== 'class') {
50
- return
86
+ return styleRule
51
87
  }
52
88
 
53
89
  const selectedVariant = this.themes.find(theme => whereSelector.name === theme)
54
90
 
55
91
  if (selectedVariant === undefined || this.processedVariables.has(selectedVariant)) {
56
- return
92
+ return styleRule
57
93
  }
58
94
 
59
95
  this.processedVariables.add(selectedVariant)
@@ -93,4 +129,27 @@ export class RuleVisitor implements LightningRuleVisitors {
93
129
  },
94
130
  }
95
131
  }
132
+
133
+ // Fixes lightningcss serialization bug
134
+ private removeNulls(value: unknown): unknown {
135
+ if (Array.isArray(value)) {
136
+ return value.map(v => this.removeNulls(v))
137
+ }
138
+
139
+ if (typeof value === 'object' && value !== null) {
140
+ return Object.fromEntries(
141
+ Object.entries(value)
142
+ .filter(([_, value]) => {
143
+ if (value === null) {
144
+ return false
145
+ }
146
+
147
+ return true
148
+ })
149
+ .map(([key, value]) => [key, this.removeNulls(value)]),
150
+ )
151
+ }
152
+
153
+ return value
154
+ }
96
155
  }
@@ -162,6 +162,10 @@ export class Functions {
162
162
  return 'rt.hairlineWidth'
163
163
  }
164
164
 
165
+ if (fn.name === 'platformSelect') {
166
+ return `rt.platformSelect(${this.Processor.CSS.processValue(fn.arguments)})`
167
+ }
168
+
165
169
  if (fn.name === 'pixelRatio') {
166
170
  if (fn.arguments.length === 0) {
167
171
  return 'rt.pixelRatio(1)'