mirror of
https://github.com/zhigang1992/nativewind.git
synced 2026-06-12 00:24:45 +08:00
fix: css variable switching with color scheme
This commit is contained in:
@@ -8,4 +8,3 @@ nextjs
|
||||
nativewind
|
||||
snackplayer
|
||||
dripsy
|
||||
zustand
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "expo-example",
|
||||
"version": "1.0.0",
|
||||
"main": "AppEntry.js",
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
|
||||
@@ -5,157 +5,176 @@ import { extractStyles } from "../src/postcss/extract";
|
||||
import { Atom, CreateOptions } from "../src/style-sheet";
|
||||
import nativePreset from "../src/tailwind";
|
||||
|
||||
const expectStyle = (style: string, config?: Partial<Config>) => {
|
||||
const createOptions = extractStyles({
|
||||
content: [],
|
||||
safelist: style.split(" "),
|
||||
presets: [nativePreset],
|
||||
...config,
|
||||
});
|
||||
const expectStyle = (style: string, config?: Partial<Config>, css?: string) => {
|
||||
const createOptions = extractStyles(
|
||||
{
|
||||
content: [],
|
||||
safelist: style.split(" "),
|
||||
presets: [nativePreset],
|
||||
...config,
|
||||
},
|
||||
`@tailwind components;@tailwind utilities;${css ?? ""}`
|
||||
);
|
||||
|
||||
console.log(createOptions);
|
||||
|
||||
return expect(createOptions);
|
||||
};
|
||||
|
||||
interface TailwindConfigWithCreateOptions {
|
||||
config: Partial<Config>;
|
||||
options: CreateOptions;
|
||||
}
|
||||
type OutputObject =
|
||||
| {
|
||||
css: string;
|
||||
config?: Partial<Config>;
|
||||
output: CreateOptions;
|
||||
}
|
||||
| {
|
||||
css?: string;
|
||||
config: Partial<Config>;
|
||||
output: CreateOptions;
|
||||
};
|
||||
|
||||
const cases: Record<
|
||||
string,
|
||||
Atom | CreateOptions | TailwindConfigWithCreateOptions
|
||||
> = {
|
||||
"text-apply": {
|
||||
config: {
|
||||
plugins: [
|
||||
plugin(function ({ addUtilities }) {
|
||||
addUtilities({
|
||||
".text-apply": {
|
||||
"@apply text-[black] dark:text-[white]": {},
|
||||
},
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
options: {
|
||||
"text-apply": {
|
||||
styles: [{ color: "black" }, { color: "white" }],
|
||||
atRules: {
|
||||
1: [["prefers-color-scheme", "dark"]],
|
||||
},
|
||||
topics: ["--color-scheme"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"text-media-query": {
|
||||
config: {
|
||||
plugins: [
|
||||
plugin(function ({ addUtilities }) {
|
||||
addUtilities({
|
||||
".text-media-query": {
|
||||
color: "black",
|
||||
},
|
||||
"@media(prefers-color-scheme: dark)": {
|
||||
".text-media-query": {
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
options: {
|
||||
"text-media-query": {
|
||||
styles: [{ color: "black" }, { color: "white" }],
|
||||
atRules: {
|
||||
1: [["prefers-color-scheme", "dark"]],
|
||||
},
|
||||
topics: ["--color-scheme"],
|
||||
},
|
||||
},
|
||||
},
|
||||
type CaseOutput = Atom | CreateOptions | OutputObject;
|
||||
|
||||
"gap-2": {
|
||||
"gap-2": {
|
||||
styles: [{ marginLeft: -8, marginTop: -8 }],
|
||||
childClasses: ["gap-2:children"],
|
||||
},
|
||||
"gap-2:children": {
|
||||
styles: [{ marginLeft: 8, marginTop: 8 }],
|
||||
},
|
||||
},
|
||||
const cases: Record<string, CaseOutput> = {
|
||||
// "text-apply": {
|
||||
// config: {
|
||||
// plugins: [
|
||||
// plugin(function ({ addUtilities }) {
|
||||
// addUtilities({
|
||||
// ".text-apply": {
|
||||
// "@apply text-[black] dark:text-[white]": {},
|
||||
// },
|
||||
// });
|
||||
// }),
|
||||
// ],
|
||||
// },
|
||||
// output: {
|
||||
// "text-apply": {
|
||||
// styles: [{ color: "black" }, { color: "white" }],
|
||||
// atRules: {
|
||||
// 1: [["prefers-color-scheme", "dark"]],
|
||||
// },
|
||||
// topics: ["--color-scheme"],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// "text-media-query": {
|
||||
// config: {
|
||||
// plugins: [
|
||||
// plugin(function ({ addUtilities }) {
|
||||
// addUtilities({
|
||||
// ".text-media-query": {
|
||||
// color: "black",
|
||||
// },
|
||||
// "@media(prefers-color-scheme: dark)": {
|
||||
// ".text-media-query": {
|
||||
// color: "white",
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// }),
|
||||
// ],
|
||||
// },
|
||||
// output: {
|
||||
// "text-media-query": {
|
||||
// styles: [{ color: "black" }, { color: "white" }],
|
||||
// atRules: {
|
||||
// 1: [["prefers-color-scheme", "dark"]],
|
||||
// },
|
||||
// topics: ["--color-scheme"],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
|
||||
// "gap-2": {
|
||||
// "gap-2": {
|
||||
// styles: [{ marginLeft: -8, marginTop: -8 }],
|
||||
// childClasses: ["gap-2:children"],
|
||||
// },
|
||||
// "gap-2:children": {
|
||||
// styles: [{ marginLeft: 8, marginTop: 8 }],
|
||||
// },
|
||||
// },
|
||||
"text-[color:hsl(var(--hue),var(--saturation),var(--lightness))]": {
|
||||
styles: [
|
||||
{
|
||||
color: {
|
||||
function: "inbuilt",
|
||||
values: [
|
||||
"hsl",
|
||||
{ function: "var", values: ["--hue"] },
|
||||
{ function: "var", values: ["--saturation"] },
|
||||
{ function: "var", values: ["--lightness"] },
|
||||
],
|
||||
},
|
||||
css: `:root { --hue: 255; }`,
|
||||
output: {
|
||||
":root": {
|
||||
variables: [{ "--hue": 255 }],
|
||||
},
|
||||
],
|
||||
topics: ["--hue", "--saturation", "--lightness"],
|
||||
},
|
||||
"w-screen": {
|
||||
styles: [{ width: { function: "vw", values: [100] } }],
|
||||
},
|
||||
"text-red-500": {
|
||||
styles: [{ color: "#ef4444" }],
|
||||
},
|
||||
"dark:text-red-500": {
|
||||
styles: [{ color: "#ef4444" }],
|
||||
atRules: { 0: [["prefers-color-scheme", "dark"]] },
|
||||
topics: ["--color-scheme"],
|
||||
},
|
||||
"hover:text-red-500": {
|
||||
styles: [{ color: "#ef4444" }],
|
||||
conditions: ["hover"],
|
||||
},
|
||||
"flex-1": {
|
||||
styles: [{ flexGrow: 1, flexShrink: 1, flexBasis: "0%" }],
|
||||
},
|
||||
"shadow-sm": {
|
||||
atRules: {
|
||||
0: [["platform", "android"]],
|
||||
1: [["platform", "ios"]],
|
||||
},
|
||||
styles: [
|
||||
{ elevation: 1.5, shadowColor: "black" },
|
||||
{
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowRadius: 2,
|
||||
shadowColor: "rgba(0, 0, 0, 0.1)",
|
||||
"text-[color:hsl(var(--hue),var(--saturation),var(--lightness))]": {
|
||||
styles: [
|
||||
{
|
||||
color: {
|
||||
function: "inbuilt",
|
||||
values: [
|
||||
"hsl",
|
||||
{ function: "var", values: ["--hue"] },
|
||||
{ function: "var", values: ["--saturation"] },
|
||||
{ function: "var", values: ["--lightness"] },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
topics: ["--hue", "--saturation", "--lightness"],
|
||||
},
|
||||
],
|
||||
},
|
||||
container: {
|
||||
styles: [
|
||||
{ width: "100%" },
|
||||
{ maxWidth: 640 },
|
||||
{ maxWidth: 768 },
|
||||
{ maxWidth: 1024 },
|
||||
{ maxWidth: 1280 },
|
||||
{ maxWidth: 1536 },
|
||||
],
|
||||
atRules: {
|
||||
1: [["min-width", 640]],
|
||||
2: [["min-width", 768]],
|
||||
3: [["min-width", 1024]],
|
||||
4: [["min-width", 1280]],
|
||||
5: [["min-width", 1536]],
|
||||
},
|
||||
topics: ["device-width"],
|
||||
},
|
||||
// "w-screen": {
|
||||
// styles: [{ width: { function: "vw", values: [100] } }],
|
||||
// },
|
||||
// "text-red-500": {
|
||||
// styles: [{ color: "#ef4444" }],
|
||||
// },
|
||||
// "dark:text-red-500": {
|
||||
// styles: [{ color: "#ef4444" }],
|
||||
// atRules: { 0: [["prefers-color-scheme", "dark"]] },
|
||||
// topics: ["--color-scheme"],
|
||||
// },
|
||||
// "hover:text-red-500": {
|
||||
// styles: [{ color: "#ef4444" }],
|
||||
// conditions: ["hover"],
|
||||
// },
|
||||
// "flex-1": {
|
||||
// styles: [{ flexGrow: 1, flexShrink: 1, flexBasis: "0%" }],
|
||||
// },
|
||||
// "shadow-sm": {
|
||||
// atRules: {
|
||||
// 0: [["platform", "android"]],
|
||||
// 1: [["platform", "ios"]],
|
||||
// },
|
||||
// styles: [
|
||||
// { elevation: 1.5, shadowColor: "black" },
|
||||
// {
|
||||
// shadowOffset: { width: 0, height: 1 },
|
||||
// shadowRadius: 2,
|
||||
// shadowColor: "rgba(0, 0, 0, 0.1)",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// container: {
|
||||
// styles: [
|
||||
// { width: "100%" },
|
||||
// { maxWidth: 640 },
|
||||
// { maxWidth: 768 },
|
||||
// { maxWidth: 1024 },
|
||||
// { maxWidth: 1280 },
|
||||
// { maxWidth: 1536 },
|
||||
// ],
|
||||
// atRules: {
|
||||
// 1: [["min-width", 640]],
|
||||
// 2: [["min-width", 768]],
|
||||
// 3: [["min-width", 1024]],
|
||||
// 4: [["min-width", 1280]],
|
||||
// 5: [["min-width", 1536]],
|
||||
// },
|
||||
// topics: ["device-width"],
|
||||
// },
|
||||
};
|
||||
|
||||
test.each(Object.entries(cases))("%s", (input, output) => {
|
||||
if ("config" in output) {
|
||||
expectStyle(input, output.config).toEqual(output.options);
|
||||
} else if ("styles" in output) {
|
||||
if (isOutputObject(output)) {
|
||||
expectStyle(input, output.config, output.css).toEqual(output.output);
|
||||
} else if (isAtom(output)) {
|
||||
expectStyle(input).toEqual({
|
||||
[input]: output,
|
||||
});
|
||||
@@ -164,6 +183,12 @@ test.each(Object.entries(cases))("%s", (input, output) => {
|
||||
}
|
||||
});
|
||||
|
||||
const isOutputObject = (output: CaseOutput): output is OutputObject => {
|
||||
return "output" in output;
|
||||
};
|
||||
|
||||
const isAtom = (output: CaseOutput): output is Atom => "styles" in output;
|
||||
|
||||
// "dark:text-red-500": {
|
||||
// styles: [{ color: "#ef4444" }],
|
||||
// atRules: { 0: [["colorScheme", "dark"]] },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render } from "@testing-library/react-native";
|
||||
import { render, act } from "@testing-library/react-native";
|
||||
import { NativeWindStyleSheet, styled } from "../src";
|
||||
import { extractStyles } from "../src/postcss/extract";
|
||||
import nativePreset from "../src/tailwind";
|
||||
@@ -8,13 +8,16 @@ afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function create(className: string) {
|
||||
function create(className: string, css?: string) {
|
||||
return NativeWindStyleSheet.create(
|
||||
extractStyles({
|
||||
content: [],
|
||||
safelist: [className],
|
||||
presets: [nativePreset],
|
||||
})
|
||||
extractStyles(
|
||||
{
|
||||
content: [],
|
||||
safelist: [className],
|
||||
presets: [nativePreset],
|
||||
},
|
||||
`@tailwind components;@tailwind utilities;${css ?? ""}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,9 +31,50 @@ test("should render", () => {
|
||||
|
||||
expect(MyComponent).toHaveBeenCalledWith(
|
||||
{
|
||||
children: undefined,
|
||||
style: [[{ color: "#ef4444" }]],
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
test("color scheme variables", () => {
|
||||
create(
|
||||
"text-[color:var(--test)]",
|
||||
`:root { --test: red; } .dark { --test: blue; }`
|
||||
);
|
||||
|
||||
const MyComponent = jest.fn();
|
||||
const StyledComponent = styled(MyComponent);
|
||||
|
||||
render(<StyledComponent className="text-[color:var(--test)]" />);
|
||||
|
||||
expect(MyComponent).toHaveBeenCalledWith(
|
||||
{
|
||||
style: [[{ color: "red" }]],
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
act(() => {
|
||||
NativeWindStyleSheet.toggleColorScheme();
|
||||
});
|
||||
|
||||
expect(MyComponent).toHaveBeenCalledWith(
|
||||
{
|
||||
style: [[{ color: "blue" }]],
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
act(() => {
|
||||
NativeWindStyleSheet.toggleColorScheme();
|
||||
});
|
||||
|
||||
expect(MyComponent).toHaveBeenCalledWith(
|
||||
{
|
||||
children: undefined,
|
||||
style: [[{ color: "red" }]],
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,9 @@ import { validProperties } from "./valid-styles";
|
||||
|
||||
import { TransformsStyle } from "react-native";
|
||||
|
||||
export type StylesAndTopics = Required<Pick<Atom, "styles" | "topics">>;
|
||||
export type StylesAndTopics = Required<
|
||||
Pick<Atom, "styles" | "topics" | "variables">
|
||||
>;
|
||||
|
||||
type InferArray<T> = T extends Array<infer K> ? K : never;
|
||||
type Transform = InferArray<NonNullable<TransformsStyle["transform"]>>;
|
||||
@@ -24,6 +26,7 @@ export function getDeclarations(block: Block) {
|
||||
const atom: StylesAndTopics = {
|
||||
styles: [],
|
||||
topics: [],
|
||||
variables: [],
|
||||
};
|
||||
|
||||
walk(block, {
|
||||
@@ -52,13 +55,19 @@ export function getDeclarations(block: Block) {
|
||||
return textShadow(atom, node);
|
||||
case "transform":
|
||||
return transform(atom, node);
|
||||
default:
|
||||
if (node.value.type === "Raw") return;
|
||||
return pushStyle(
|
||||
atom,
|
||||
node.property,
|
||||
node.value.children.toArray()[0]
|
||||
);
|
||||
default: {
|
||||
if (node.value.type === "Raw") {
|
||||
if (node.property.startsWith("--")) {
|
||||
atom.variables.push({ [node.property]: node.value.value.trim() });
|
||||
}
|
||||
} else {
|
||||
return pushStyle(
|
||||
atom,
|
||||
node.property,
|
||||
node.value.children.toArray()[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -72,8 +81,8 @@ export function getDeclarations(block: Block) {
|
||||
}
|
||||
|
||||
return {
|
||||
...atom,
|
||||
styles,
|
||||
topics: atom.topics,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,7 +93,9 @@ function pushStyle(
|
||||
) {
|
||||
if (!node) return;
|
||||
|
||||
const [value, topics = []] = parseStyleValue(node);
|
||||
const topics: string[] = [];
|
||||
// This mutates topics, theres probably a better way to write this
|
||||
const value = parseStyleValue(node, topics);
|
||||
|
||||
if (value === undefined || value === null) return;
|
||||
|
||||
@@ -102,59 +113,56 @@ function pushStyle(
|
||||
}
|
||||
|
||||
function parseStyleValue(
|
||||
node?: StyleValue | null,
|
||||
topics: string[] = []
|
||||
): [StyleValue, string[]] | [] {
|
||||
node: StyleValue | null | undefined,
|
||||
topics: string[]
|
||||
): StyleValue | StyleValue[] | undefined {
|
||||
if (!node) return [];
|
||||
|
||||
if (typeof node === "string") {
|
||||
return [node, topics];
|
||||
return node;
|
||||
}
|
||||
|
||||
if (typeof node === "number") {
|
||||
return [node, topics];
|
||||
return node;
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
return [node.map((n) => parseStyleValue(n, topics)[0]) as string[], topics];
|
||||
return node.map((n) => parseStyleValue(n, topics)) as StyleValue[];
|
||||
}
|
||||
|
||||
if ("function" in node) {
|
||||
return [node as StyleWithFunction, topics];
|
||||
return node;
|
||||
}
|
||||
|
||||
if ("type" in node) {
|
||||
switch (node.type) {
|
||||
case "Identifier":
|
||||
return [node.name, topics];
|
||||
return node.name;
|
||||
case "Number":
|
||||
return [Number.parseFloat(node.value.toString()), topics];
|
||||
return Number.parseFloat(node.value.toString());
|
||||
case "String":
|
||||
return [node.value, topics];
|
||||
return node.value;
|
||||
case "Hash":
|
||||
return [`#${node.value}`, topics];
|
||||
return `#${node.value}`;
|
||||
case "Percentage":
|
||||
return [`${node.value}%`, topics];
|
||||
return `${node.value}%`;
|
||||
case "Dimension":
|
||||
switch (node.unit) {
|
||||
case "px":
|
||||
return [Number.parseFloat(node.value.toString()), topics];
|
||||
return Number.parseFloat(node.value.toString());
|
||||
case "vw":
|
||||
case "vh":
|
||||
return [
|
||||
{
|
||||
function: node.unit,
|
||||
values: [Number.parseFloat(node.value.toString())],
|
||||
},
|
||||
topics,
|
||||
];
|
||||
return {
|
||||
function: node.unit,
|
||||
values: [Number.parseFloat(node.value.toString())],
|
||||
};
|
||||
default:
|
||||
return [`${node.value}${node.unit}`, topics];
|
||||
return `${node.value}${node.unit}`;
|
||||
}
|
||||
case "Function":
|
||||
switch (node.name) {
|
||||
case "pixelRatio":
|
||||
return [{ function: "pixelRatio", values: [] }, topics];
|
||||
return { function: "pixelRatio", values: [] };
|
||||
case "var": {
|
||||
const value = parseStyleValue(node.children.shift()?.data, topics);
|
||||
|
||||
@@ -162,13 +170,10 @@ function parseStyleValue(
|
||||
|
||||
topics.push(value);
|
||||
|
||||
return [
|
||||
{
|
||||
function: "var",
|
||||
values: [value],
|
||||
},
|
||||
topics,
|
||||
];
|
||||
return {
|
||||
function: "var",
|
||||
values: [value],
|
||||
};
|
||||
}
|
||||
default: {
|
||||
const values = node.children.toArray().flatMap((child) => {
|
||||
@@ -179,30 +184,24 @@ function parseStyleValue(
|
||||
(value) => typeof value === "object"
|
||||
);
|
||||
|
||||
return [
|
||||
hasDynamicValues
|
||||
? {
|
||||
function: "inbuilt",
|
||||
values: [node.name, ...(values as string[])],
|
||||
}
|
||||
: `${node.name}(${values.join(", ")})`,
|
||||
topics,
|
||||
];
|
||||
return hasDynamicValues
|
||||
? {
|
||||
function: "inbuilt",
|
||||
values: [node.name, ...(values as string[])],
|
||||
}
|
||||
: `${node.name}(${values.join(", ")})`;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
Object.fromEntries(
|
||||
Object.entries(node).map(([key, value]) => {
|
||||
return [key, parseStyleValue(value)];
|
||||
})
|
||||
) as unknown as Transform,
|
||||
topics,
|
||||
];
|
||||
return Object.fromEntries(
|
||||
Object.entries(node).map(([key, value]) => {
|
||||
return [key, parseStyleValue(value, topics)];
|
||||
})
|
||||
) as unknown as Transform;
|
||||
}
|
||||
|
||||
function setValue<T extends Record<string, unknown>>(
|
||||
@@ -818,7 +817,7 @@ function transform(atom: StylesAndTopics, node: Declaration) {
|
||||
case "translateX":
|
||||
case "translateY":
|
||||
case "matrix": {
|
||||
const [value] = parseStyleValue(child.children.toArray()[0]);
|
||||
const value = parseStyleValue(child.children.toArray()[0], []);
|
||||
transform.push({ [child.name]: value } as unknown as Transform);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ function addRule(
|
||||
const selectorList = node.prelude;
|
||||
if (selectorList.type === "Raw") return skip;
|
||||
|
||||
const { styles, topics: ruleTopics } = getDeclarations(node.block);
|
||||
const { styles, topics: ruleTopics, variables } = getDeclarations(node.block);
|
||||
|
||||
// eslint-disable-next-line unicorn/no-array-for-each
|
||||
selectorList.children.forEach((selectorNode) => {
|
||||
@@ -104,6 +104,17 @@ function addRule(
|
||||
|
||||
// Invalid selector, skip it
|
||||
if (!selector) return;
|
||||
|
||||
if (selector === ":root") {
|
||||
createOptions[selector] ??= { variables };
|
||||
return;
|
||||
}
|
||||
|
||||
if (selector === "dark") {
|
||||
createOptions[selector] ??= { variables };
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(styles).length === 0) return;
|
||||
|
||||
createOptions[selector] ??= { styles: [] };
|
||||
|
||||
@@ -25,6 +25,8 @@ export function getSelector(node: CssNode) {
|
||||
if (node.name === "children") {
|
||||
hasParent = true;
|
||||
tokens.push(`:${node.name}`);
|
||||
} else if (node.name === "root") {
|
||||
tokens.push(`:${node.name}`);
|
||||
} else {
|
||||
conditions.push(node.name);
|
||||
}
|
||||
|
||||
@@ -27,11 +27,17 @@ export type StyleWithFunction = {
|
||||
values: Array<StyleWithFunction | string | number>;
|
||||
};
|
||||
|
||||
export type VariableValue =
|
||||
| string
|
||||
| number
|
||||
| StyleWithFunction
|
||||
| OpaqueColorValue;
|
||||
|
||||
export interface Atom {
|
||||
styles?: AtomStyle[];
|
||||
atRules?: Record<number, Array<AtRuleTuple>>;
|
||||
conditions?: string[];
|
||||
customProperties?: string[];
|
||||
variables?: Array<Record<string, VariableValue>>;
|
||||
topics?: string[];
|
||||
topicSubscription?: () => void;
|
||||
childClasses?: string[];
|
||||
@@ -63,8 +69,7 @@ const createSubscriber =
|
||||
return () => listeners.delete(listener);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
let dimensionsListener: EmitterSubscription | undefined = undefined;
|
||||
let dimensionsListener: EmitterSubscription | undefined;
|
||||
|
||||
const atoms: Map<string, Atom> = new Map();
|
||||
const childClasses: Map<string, string> = new Map();
|
||||
@@ -92,9 +97,10 @@ const setStyles = createSetter(
|
||||
);
|
||||
const subscribeToStyles = createSubscriber(styleListeners);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let topicValues: Record<string, string | number> = {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let topicValues: Record<string, VariableValue> = {};
|
||||
let rootVariableValues: Record<string, VariableValue> = {};
|
||||
let darkRootVariableValues: Record<string, VariableValue> = {};
|
||||
|
||||
const topicValueListeners = new Set<Listener<typeof topicValues>>();
|
||||
const setTopicValues = createSetter(
|
||||
() => topicValues,
|
||||
@@ -165,12 +171,12 @@ export const NativeWindStyleSheet = {
|
||||
setColorScheme,
|
||||
setDirection,
|
||||
toggleColorScheme,
|
||||
setCustomProperties,
|
||||
setVariable: setVariables,
|
||||
setCustomProperties: setVariables,
|
||||
setDimensions,
|
||||
setDangerouslyCompileStyles: (callback: typeof dangerouslyCompileStyles) =>
|
||||
(dangerouslyCompileStyles = callback),
|
||||
};
|
||||
NativeWindStyleSheet.reset();
|
||||
|
||||
export type CreateOptions = Record<string, Atom>;
|
||||
|
||||
@@ -181,7 +187,35 @@ function create(options: CreateOptions) {
|
||||
|
||||
let newStyles: Record<string, Style[] | undefined> = {};
|
||||
|
||||
const root = options[":root"];
|
||||
if (root?.variables) {
|
||||
rootVariableValues = { ...rootVariableValues, ...root.variables[0] };
|
||||
}
|
||||
|
||||
const dark = options["dark"];
|
||||
if (dark?.variables) {
|
||||
darkRootVariableValues = {
|
||||
...rootVariableValues,
|
||||
...darkRootVariableValues,
|
||||
...dark.variables[0],
|
||||
};
|
||||
}
|
||||
|
||||
if (root || dark) {
|
||||
setTopicValues(
|
||||
getColorScheme() === "light" ? rootVariableValues : darkRootVariableValues
|
||||
);
|
||||
}
|
||||
|
||||
for (const [atomName, atom] of Object.entries(options)) {
|
||||
if (atomName === ":root") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (atomName === ".dark") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (atom.topics) {
|
||||
atom.topicSubscription = subscribeToTopics((values, oldValues) => {
|
||||
const topicChanged = atom.topics?.some((topic) => {
|
||||
@@ -218,7 +252,7 @@ function evaluate(name: string, atom: Atom) {
|
||||
|
||||
for (const [key, value] of Object.entries(styles)) {
|
||||
if (typeof value === "object" && "function" in value) {
|
||||
(styles as Record<string, unknown>)[key] = resolveFunction(value);
|
||||
(styles as Record<string, unknown>)[key] = resolveVariableValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,17 +275,31 @@ function evaluate(name: string, atom: Atom) {
|
||||
case "platform":
|
||||
return params === Platform.OS;
|
||||
case "width":
|
||||
return params === topicValues["device-width"];
|
||||
case "min-width":
|
||||
return (params ?? 0) >= topicValues["device-width"];
|
||||
case "max-width":
|
||||
return (params ?? 0) <= topicValues["device-width"];
|
||||
return params === resolveVariableValue(topicValues["device-width"]);
|
||||
case "min-width": {
|
||||
const value = resolveVariableValue(topicValues["device-width"]);
|
||||
if (typeof value !== "number") return false;
|
||||
return (params ?? 0) >= value;
|
||||
}
|
||||
case "max-width": {
|
||||
const value = resolveVariableValue(topicValues["device-width"]);
|
||||
if (typeof value !== "number") return false;
|
||||
return (params ?? 0) <= value;
|
||||
}
|
||||
case "height":
|
||||
return params === topicValues["device-height"];
|
||||
case "min-height":
|
||||
return (params ?? 0) >= topicValues["device-height"];
|
||||
case "max-height":
|
||||
return (params ?? 0) <= topicValues["device-height"];
|
||||
return (
|
||||
params === resolveVariableValue(topicValues["device-height"])
|
||||
);
|
||||
case "min-height": {
|
||||
const value = resolveVariableValue(topicValues["device-height"]);
|
||||
if (typeof value !== "number") return false;
|
||||
return (params ?? 0) >= value;
|
||||
}
|
||||
case "max-height": {
|
||||
const value = resolveVariableValue(topicValues["device-height"]);
|
||||
if (typeof value !== "number") return false;
|
||||
return (params ?? 0) <= value;
|
||||
}
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
@@ -284,14 +332,16 @@ function evaluate(name: string, atom: Atom) {
|
||||
return newStyles;
|
||||
}
|
||||
|
||||
function resolveFunction(
|
||||
style: StyleWithFunction | string | number
|
||||
function resolveVariableValue(
|
||||
style: VariableValue
|
||||
): string | number | OpaqueColorValue | undefined {
|
||||
if (typeof style !== "object" || !("function" in style)) {
|
||||
return style;
|
||||
}
|
||||
|
||||
const resolvedValues = style.values.map((value) => resolveFunction(value));
|
||||
const resolvedValues = style.values.map((value) =>
|
||||
resolveVariableValue(value)
|
||||
);
|
||||
|
||||
switch (style.function) {
|
||||
case "inbuilt": {
|
||||
@@ -311,7 +361,10 @@ function resolveFunction(
|
||||
case "var": {
|
||||
const [variable, defaultValue] = resolvedValues;
|
||||
if (typeof variable !== "string") return;
|
||||
return topicValues[variable] ?? defaultValue;
|
||||
const value = topicValues[variable];
|
||||
if (!value) return defaultValue;
|
||||
if (typeof value === "object" && "function" in value) return defaultValue;
|
||||
return value;
|
||||
}
|
||||
case "platformSelect": {
|
||||
const specifics = resolveSpecifics(resolvedValues);
|
||||
@@ -363,7 +416,7 @@ function resolveSpecifics(
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => {
|
||||
const [platform, other] = value.split("_");
|
||||
return [platform, resolveFunction(other)];
|
||||
return [platform, resolveVariableValue(other)];
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -475,27 +528,18 @@ function getColorScheme() {
|
||||
}
|
||||
|
||||
function setColorScheme(system?: ColorSchemeName | "system" | null) {
|
||||
const colorScheme =
|
||||
!system || system === "system"
|
||||
? Appearance.getColorScheme() || "light"
|
||||
: system;
|
||||
|
||||
setTopicValues({
|
||||
colorSchemeSystem: system ?? "system",
|
||||
colorScheme:
|
||||
!system || system === "system"
|
||||
? Appearance.getColorScheme() || "light"
|
||||
: system,
|
||||
colorScheme,
|
||||
...(colorScheme === "light" ? rootVariableValues : darkRootVariableValues),
|
||||
});
|
||||
}
|
||||
|
||||
function setDirection(direction: "ltr" | "rtl") {
|
||||
setTopicValues({
|
||||
i18nDirection: direction,
|
||||
});
|
||||
}
|
||||
|
||||
function setCustomProperties(
|
||||
properties: Record<`--${string}`, string | number>
|
||||
) {
|
||||
setTopicValues(properties);
|
||||
}
|
||||
|
||||
function toggleColorScheme() {
|
||||
return setTopicValues((state) => {
|
||||
const currentColor =
|
||||
@@ -503,15 +547,28 @@ function toggleColorScheme() {
|
||||
? Appearance.getColorScheme() || "light"
|
||||
: state["colorScheme"];
|
||||
|
||||
const newColor = currentColor === "light" ? "dark" : "light";
|
||||
const colorScheme = currentColor === "light" ? "dark" : "light";
|
||||
|
||||
return {
|
||||
colorScheme: newColor,
|
||||
colorSchemeSystem: newColor,
|
||||
colorScheme,
|
||||
colorSchemeSystem: colorScheme,
|
||||
...(colorScheme === "light"
|
||||
? rootVariableValues
|
||||
: darkRootVariableValues),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setDirection(direction: "ltr" | "rtl") {
|
||||
setTopicValues({
|
||||
i18nDirection: direction,
|
||||
});
|
||||
}
|
||||
|
||||
function setVariables(properties: Record<`--${string}`, string | number>) {
|
||||
setTopicValues(properties);
|
||||
}
|
||||
|
||||
topicValueListeners.add((topics) => {
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.nativewind_theme = topics["colorScheme"];
|
||||
@@ -565,3 +622,5 @@ function setDimensions(dimensions: Dimensions) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
NativeWindStyleSheet.reset();
|
||||
|
||||
99
release.md
99
release.md
@@ -1,99 +0,0 @@
|
||||
> This is a work in progress - these features are not final
|
||||
|
||||
NativeWind 3.0 is a major update for NativeWind that fixes a large variety of issues. For most users the upgrade will be simple and should only require a small configuration change.
|
||||
|
||||
## Why a new major version
|
||||
|
||||
NativeWind 3.0 adds many new features that require breaking changes to the compiled styles. Additionally it also streamlines the project setup across all platforms, which requires minor updates to projects configuration.
|
||||
|
||||
NativeWind 3.0:
|
||||
|
||||
- reduces the runtime code by XX%
|
||||
- only has two runtime dependencies (`react-is` and the `use-sync-external-store` shim
|
||||
|
||||
## New Features
|
||||
|
||||
### CSS Variables
|
||||
|
||||
One of the sore points of compiled styles is static theming. Dynamic theme values are required for variety of reasons are often used by existing Tailwind libraries.
|
||||
|
||||
### REM Units
|
||||
|
||||
Many developers use `rem` units to ensure consistent scaling across their application. NativeWind now mimics this behaviour using the CSS variable `--rem`, which defaults to `16`. Developers can change their either by adding a `font-size` to `:root` or by calling `NativeWindStyleSheet.setRem()`
|
||||
|
||||
### RTL Support
|
||||
|
||||
The `rtl` and `ltr` variants are now supported. Developers can quickly change this value via `NativeWindStyleSheet.setDirection()` or `NativeWindStyleSheet.toggleDirection()`
|
||||
|
||||
### Odd/Even/First/Last
|
||||
|
||||
The `odd/even/first/last` variants are now supported for styling children.
|
||||
|
||||
### Startup performance
|
||||
|
||||
NativeWind 3.0 uses deduplicated styles when compiling for production, significantly improving startup time
|
||||
|
||||
### Cache issues
|
||||
|
||||
A number of caching issues, expecially during local development have been fixed. There are still some edge cases, but you shouldn't need to reset your babel cache as often.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- `group`/`group-isolate`/`parent` now work as expected
|
||||
- Fixed edge cases where modifiers and variants did not apply correctly
|
||||
|
||||
## Experimental Features
|
||||
|
||||
### CSS Styles
|
||||
|
||||
As stated in the projects goals, NativeWind is not trying to build a full featured css engine, just enough to make using Tailwind comfortable. However a side-effect of the new project setup is that user-supplied css can be added to the NativeWind compiler. This allows developers to write custom CSS classes and use features like `@apply` without learning Tailwind's plugin system.
|
||||
|
||||
This will be an indefinitely experimental feature.
|
||||
|
||||
## New project setup
|
||||
|
||||
NativeWind previously had suble differences in it's setup for React Native and Web. This often causes confusion for React Native developers who were not familiar with Tailwind's web setup. Additionally, developers who wanted to tinker with NativeWind's internals found that they were locked out of various options.
|
||||
|
||||
As one of NativeWind's goals is to "align with Tailwind CSS", NativeWind 3.0 introduces a new project setup that is closer to Tailwind CSS's setup and exposes its internals a bit better.
|
||||
|
||||
For most web frameworks, Tailwind CSS doesn't "just work" and requires a "@tailwind" at-rule to trigger its compilation. NativeWind now requires the same trigger
|
||||
|
||||
For e
|
||||
|
||||
```diff
|
||||
// global.css
|
||||
+ @tailwind base;
|
||||
+ @tailwind components;
|
||||
+ @tailwind utilities;
|
||||
|
||||
// App.tsx
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
+ import "./styles.css";
|
||||
|
||||
export function Test() {
|
||||
return (
|
||||
<View className="container">
|
||||
<Text>Hello world!</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
While it may seem usual for a React Native project to import a `.css` file, it's a concept web developers have been familiar with for sometime.
|
||||
|
||||
Additionally, NativeWind's plugins & configuration is now exposed as single Tailwind preset. Adding the preset is now required for all projects.
|
||||
|
||||
```diff
|
||||
// tailwind.config.js
|
||||
+ const nativewind = require("nativewind/tailwind")
|
||||
|
||||
module.exports = {
|
||||
content: ["./App.{js,jsx,ts,tsx}", "./<custom directory>/**/*.{js,jsx,ts,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
+ presets: [nativewind],
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user