fix: css variable switching with color scheme

This commit is contained in:
Mark Lawlor
2022-09-27 21:22:27 +10:00
parent 1b1a58b26e
commit 61d4e2fcce
9 changed files with 388 additions and 348 deletions

View File

@@ -8,4 +8,3 @@ nextjs
nativewind
snackplayer
dripsy
zustand

View File

@@ -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",

View File

@@ -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"]] },

View File

@@ -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" }]],
},
{}
);
});

View File

@@ -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);
}
}

View File

@@ -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: [] };

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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],
}
```